作为频繁用 POI 做导出功能的实习生,整理了这份「高频方法速查表 + 实战避坑指南」,涵盖 Workbook、Sheet、Row、Cell、样式、合并等核心操作,直接复制能用,遇到问题快速对应排查,效率翻倍~

一、POI 核心操作速查(按组件分类)

1. 基础准备(依赖 + Workbook 创建)

操作场景 核心代码 说明
引入 POI 依赖(pom.xml) xml <!-- 支持.xlsx(Excel 2007+) --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency> 稳定版本 4.1.2,兼容性最好,避免高版本坑
创建常规 Workbook(小数据) Workbook workbook = new XSSFWorkbook(); 数据量 < 1 万条用,优点:功能全;缺点:内存占用高
创建流式 Workbook(大数据) Workbook workbook = new SXSSFWorkbook(100); 数据量 > 1 万条用(避免 OOM),括号内是内存缓存行数(默认 100)
关闭资源(自动关闭) try (Workbook workbook = new SXSSFWorkbook(100); OutputStream os = response.getOutputStream()) { // 操作逻辑 } 用 try-with-resources 自动关闭,无需手动 close,避免内存泄漏

2. Sheet(工作表)操作

操作场景 核心代码 说明
创建 Sheet Sheet sheet = workbook.createSheet("11月考勤表"); 工作表名称最多 31 字符,不含特殊符号(/:*?"<>
设置默认列宽 sheet.setDefaultColumnWidth(15); 单位是 “字符数”,15 = 常规列宽,适配大多数文本
单独设置列宽 sheet.setColumnWidth(2, 20 * 256); 精准设置(单位是 1/256 字符宽度),公式:目标宽度 ×256(如 20 字符 = 20×256)
冻结窗格(冻结前 3 行) sheet.createFreezePane(0, 3); 参数:冻结列数、冻结行数(0 = 不冻结),适配多标题场景
获取 Sheet Sheet sheet = workbook.getSheetAt(0); 按索引获取(0 = 第一个工作表)

3. Row(行)操作

操作场景 核心代码 说明
创建行(索引从 0 开始) Row row = sheet.createRow(0); 0 = 第一行,创建时需按顺序(避免跳行导致空行)
设置行高(磅) row.setHeightInPoints(20); 单位是 “磅”,20 = 标题行高,18 = 数据行高,直观易计算
获取行 Row row = sheet.getRow(1); 若行未创建,返回 null,需先判断非空

4. Cell(单元格)操作

操作场景 核心代码 说明
创建单元格(索引从 0 开始) Cell cell = row.createCell(0); 0 = 第一列,与表头顺序对应
设置字符串值 cell.setCellValue("张三"); 文本、状态等字符串类型数据
设置数字值 cell.setCellValue(1001); 工号、年龄等数字类型(无需转字符串)
设置日期值(关键) Date date = new Date(); cell.setCellValue(date); // 配合日期样式 cell.setCellStyle(dateStyle); 必须绑定日期样式,否则显示为数字(如 45321)
设置布尔值(转中文) cell.setCellValue(isLate ? "是" : "否"); 避免直接填 true/false,转中文更友好

5. 样式(CellStyle)操作(高频)

操作场景 核心代码 说明
创建标题样式(加粗 + 居中) Font titleFont = workbook.createFont(); titleFont.setBold(true); titleFont.setFontName("微软雅黑"); titleFont.setFontHeightInPoints((short)12); CellStyle titleStyle = workbook.createCellStyle(); titleStyle.setFont(titleFont); titleStyle.setAlignment(HorizontalAlignment.CENTER); titleStyle.setVerticalAlignment(VerticalAlignment.CENTER); 标题专用,加粗 + 水平 / 垂直居中,提升可读性
创建日期样式(格式化) DataFormat dataFormat = workbook.createDataFormat(); CellStyle dateStyle = workbook.createCellStyle(); dateStyle.setDataFormat(dataFormat.getFormat("yyyy-MM-dd HH:mm:ss")); 绑定到日期单元格,避免日期显示为数字
合并单元格样式(关键) style.setVerticalAlignment(VerticalAlignment.CENTER); 行合并必须加,否则数据显示在顶部,超丑
设置边框(所有单元格) style.setBorderTop(BorderStyle.THIN); style.setBorderBottom(BorderStyle.THIN); style.setBorderLeft(BorderStyle.THIN); style.setBorderRight(BorderStyle.THIN); 加边框后报表更规整,避免数据粘连

6. 合并单元格操作(行 / 列 / 复杂)

操作场景 核心代码 说明
列合并(同一行多列) CellRangeAddress colMerge = new CellRangeAddress(0,0,0,3); sheet.addMergedRegion(colMerge); 参数:firstRow,lastRow,firstCol,lastCol(行 0-0 = 同一行,列 0-3=4 列合并)
行合并(同一列多行) CellRangeAddress rowMerge = new CellRangeAddress(2,4,0,0); sheet.addMergedRegion(rowMerge); 行 2-4=3 行合并,列 0-0 = 同一列(员工 ID 列常用)
复杂合并(多行多列) CellRangeAddress complexMerge = new CellRangeAddress(2,4,0,1); sheet.addMergedRegion(complexMerge); 行 2-4 + 列 0-1=3 行 2 列合并(员工 ID + 姓名合并)

7. 导出响应设置(前端下载)

操作场景 核心代码 说明
设置响应头(.xlsx 格式) response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=考勤表.xlsx"); response.setCharacterEncoding("UTF-8"); 必须正确设置,否则前端下载的文件损坏或无法识别
写入响应流 workbook.write(outputStream); 配合 try-with-resources,自动关闭流

二、POI 避坑指南(实习生实战踩坑总结)

1. 数据显示类坑

坑点描述 原因分析 解决方案
日期显示为数字(45321) 日期单元格未设置「日期样式」,POI 默认存储为时间戳 给日期单元格绑定 dateStyle(参考样式操作速查表)
合并后数据不显示 数据填到了被合并的「非首行 / 非首列」单元格(合并后只有首行首列有效) 只给 CellRangeAddress 的 firstRow+firstCol 单元格填数据(i==0 时填)
数据显示在合并单元格顶部 行合并未设置垂直居中对齐 样式中强制加setVerticalAlignment(VerticalAlignment.CENTER)
中文乱码(文件名 / 内容) 响应头未设 UTF-8 编码,或文件名未处理中文 response.setCharacterEncoding("UTF-8"),文件名用 URLEncoder.encode

2. 格式错乱类坑

坑点描述 原因分析 解决方案
列宽不合适,数据显示不全 未设置列宽,或列宽单位算错(POI 列宽单位是 1/256 字符) setDefaultColumnWidth(15)设默认,日期列用20*256加宽
合并范围错位(多 / 漏合并) 结束行 / 列计算错误,公式:结束索引 = 起始索引 + 数量 - 1 行合并:mergeEndRow = mergeStartRow + 记录数 -1(如 3 条记录 = 2+3-1=4)
标题与数据列对齐乱 主列数统计错误,导致日期子列起始位置偏移 用变量mainColCount记录主列数(如 11 列),日期列起始 = mainColCount
冻结窗格后标题消失 冻结行数未适配标题行数(如 3 行标题只冻结 2 行) 冻结行数 = 标题总行数(3 行标题用createFreezePane(0,3)

3. 性能 / 报错类坑

坑点描述 原因分析 解决方案
大数据量导出 OOM(内存溢出) 用了 XSSFWorkbook,所有数据加载到内存 换成 SXSSFWorkbook,配合数据库分页查询(每页 1000-5000 条)
样式过多导致报错(The maximum number of cell styles was exceeded) 每个单元格创建一个 CellStyle(POI 限制 64000 个样式) 复用样式(标题 / 文本 / 日期各创建一个,所有单元格共用)
流未关闭导致内存泄漏 手动创建的 Workbook/OutputStream 未关闭 用 try-with-resources 自动关闭(推荐),或在 finally 中关闭
Excel 文件损坏无法打开 响应头 Content-Type 设置错误,或流未写完就关闭 严格按速查表设置响应头,确保workbook.write(outputStream)执行完再关闭

4. 逻辑类坑

坑点描述 原因分析 解决方案
同一员工行合并混乱 数据未分组,同一员工的记录不连续 按 “姓名 + 工号” 分组(避免重名),确保记录连续:Collectors.groupingBy(att->att.getEmpName()+"-"+att.getEmpNo())
日期子列对应错误 日期列起始位置计算错误(未按 “每个日期占 3 列” 偏移) 日期列起始 = mainColCount + (日期 - 1)3(如 1 日 = 11+03=11 列)
空行过多 创建 Row 时跳行(如直接创建 row (5),前面 row (1-4) 未创建) 按顺序创建行,循环递增行索引,不跳行

三、快速复用模板(核心代码片段)

1. 日期样式模板(直接复制)

// 创建日期样式(避免日期显示为数字)
private static CellStyle createDateStyle(Workbook workbook) {
    CellStyle style = workbook.createCellStyle();
    // 绑定字体
    Font font = workbook.createFont();
    font.setFontName("微软雅黑");
    font.setFontHeightInPoints((short)11);
    style.setFont(font);
    // 日期格式化
    DataFormat dataFormat = workbook.createDataFormat();
    style.setDataFormat(dataFormat.getFormat("yyyy-MM-dd HH:mm:ss"));
    // 居中+边框
    style.setAlignment(HorizontalAlignment.CENTER);
    style.setVerticalAlignment(VerticalAlignment.CENTER);
    style.setBorderTop(BorderStyle.THIN);
    style.setBorderBottom(BorderStyle.THIN);
    style.setBorderLeft(BorderStyle.THIN);
    style.setBorderRight(BorderStyle.THIN);
    return style;
}

2. 行合并核心模板(同一员工多记录)

// 数据分组(按姓名+工号)
Map<String, List<MyAttendance>> empGroup = attendanceList.stream()
        .collect(Collectors.groupingBy(att -> att.getEmpName() + "-" + att.getEmpNo()));

// 遍历分组填充数据
int currentRow = 3; // 数据行从第4行开始(前3行标题)
for (Map.Entry<String, List<MyAttendance>> entry : empGroup.entrySet()) {
    List<MyAttendance> empAtts = entry.getValue();
    int recordCount = empAtts.size();
    int mergeStartRow = currentRow;
    int mergeEndRow = currentRow + recordCount - 1;

    // 合并主列(0-10列,共11列)
    for (int col = 0; col < 11; col++) {
        CellRangeAddress mergeRange = new CellRangeAddress(mergeStartRow, mergeEndRow, col, col);
        sheet.addMergedRegion(mergeRange);
    }

    // 填充每条记录
    for (int i = 0; i < recordCount; i++) {
        MyAttendance att = empAtts.get(i);
        Row row = sheet.createRow(currentRow);
        // 主列只在首行填数据
        if (i == 0) {
            row.createCell(0).setCellValue(att.getMonth());
            row.createCell(1).setCellValue(att.getShopName());
            // ... 其他主列
        }
        // 日期子列填当日数据
        int dateCol = 11 + (att.getDate() - 1) * 3;
        row.createCell(dateCol).setCellValue(att.getCheckResult());
        // ... 其他子列
        currentRow++;
    }
}
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐