作为频繁用 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++;
}
}
所有评论(0)