Apache POI Excel 导出样式美化实战指南
一、概述
使用 Apache POI 导出 Excel 时,默认样式非常简陋(无边框、无背景色、列宽不自适应)。在实际项目中,导出给用户的 Excel 文件需要具备良好的可读性,包括表头美化、数据对齐、边框、列宽自适应等。
本文以 XSSFWorkbook(.xlsx 格式)为例,系统介绍 POI 中样式相关 API 的使用方法。
二、POI 样式体系结构
Workbook(工作簿) ├── Font(字体)--- 控制文字样式 ├── CellStyle(单元格样式)--- 控制单元格外观 ├── Sheet(工作表) │ ├── Row(行)--- 控制行高 │ │ └── Cell(单元格)--- 绑定 CellStyle │ └── 列宽设置
关键关系:
Font被CellStyle引用CellStyle被Cell引用- 一个
CellStyle可以被多个Cell共用(推荐复用,减少内存开销)
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
三、核心 API 详解
3.1 字体(Font)
java
Font font = workbook.createFont();
font.setBold(true); // 加粗
font.setItalic(true); // 斜体
font.setFontHeightInPoints((short) 12); // 字号(磅)
font.setFontName("微软雅黑"); // 字体名称
font.setColor(IndexedColors.WHITE.getIndex()); // 字体颜色
font.setUnderline(Font.U_SINGLE); // 下划线
font.setStrikeout(true); // 删除线
3.2 单元格样式(CellStyle)
背景填充
java
CellStyle style = workbook.createCellStyle();
// 设置背景色(必须先设置前景色,再设置填充模式)
style.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
常用颜色:
| 颜色常量 | 效果 |
|---|---|
IndexedColors.ROYAL_BLUE |
深蓝色(适合表头) |
IndexedColors.LIGHT_BLUE |
浅蓝色 |
IndexedColors.GREY_25_PERCENT |
浅灰色(适合交替行) |
IndexedColors.LIGHT_YELLOW |
浅黄色 |
IndexedColors.WHITE |
白色 |
IndexedColors.RED |
红色(适合错误标记) |
对齐方式
java
// 水平对齐
style.setAlignment(HorizontalAlignment.CENTER); // 居中
style.setAlignment(HorizontalAlignment.LEFT); // 左对齐
style.setAlignment(HorizontalAlignment.RIGHT); // 右对齐
// 垂直对齐
style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直居中
style.setVerticalAlignment(VerticalAlignment.TOP); // 顶部对齐
style.setVerticalAlignment(VerticalAlignment.BOTTOM); // 底部对齐
边框
java
// 四边边框(细线)
style.setBorderTop(BorderStyle.THIN);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
// 边框颜色(可选,默认黑色)
style.setTopBorderColor(IndexedColors.BLACK.getIndex());
style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
style.setRightBorderColor(IndexedColors.BLACK.getIndex());
边框样式:
| 常量 | 效果 |
|---|---|
BorderStyle.THIN |
细线(最常用) |
BorderStyle.MEDIUM |
中等粗线 |
BorderStyle.THICK |
粗线 |
BorderStyle.DASHED |
虚线 |
BorderStyle.DOTTED |
点线 |
BorderStyle.NONE |
无边框 |
自动换行
java
style.setWrapText(true); // 内容超出列宽时自动换行
绑定字体
java
style.setFont(font); // 将 Font 对象绑定到 CellStyle
3.3 行高
java
Row row = sheet.createRow(0);
row.setHeightInPoints(20); // 行高20磅
// 或
row.setHeight((short) (20 * 20)); // 单位是 1/20 磅
3.4 列宽
java
// 固定列宽(单位是 1/256 个字符宽度)
sheet.setColumnWidth(0, 5000); // 第0列宽度约20个字符
// 自适应列宽(根据内容自动调整)
sheet.autoSizeColumn(0);
// 自适应 + 额外余量(推荐)
sheet.autoSizeColumn(0);
int width = sheet.getColumnWidth(0);
sheet.setColumnWidth(0, Math.max(width + 512, 4000)); // 至少4000宽度
注意 :autoSizeColumn 对中文支持不够好,可能偏窄,建议加 512~1024 的余量。
四、完整示例
java
public String exportEmployee(List<EmployeeExportDto> dataList) {
XSSFWorkbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("员工信息");
// ===== 1. 定义表头样式 =====
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerFont.setColor(IndexedColors.WHITE.getIndex());
headerFont.setFontHeightInPoints((short) 11);
headerFont.setFontName("微软雅黑");
CellStyle headerStyle = workbook.createCellStyle();
headerStyle.setFont(headerFont);
headerStyle.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
headerStyle.setBorderTop(BorderStyle.THIN);
headerStyle.setBorderBottom(BorderStyle.THIN);
headerStyle.setBorderLeft(BorderStyle.THIN);
headerStyle.setBorderRight(BorderStyle.THIN);
// ===== 2. 定义数据行样式 =====
Font dataFont = workbook.createFont();
dataFont.setFontHeightInPoints((short) 10);
dataFont.setFontName("微软雅黑");
CellStyle dataStyle = workbook.createCellStyle();
dataStyle.setFont(dataFont);
dataStyle.setAlignment(HorizontalAlignment.LEFT);
dataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
dataStyle.setWrapText(true);
dataStyle.setBorderTop(BorderStyle.THIN);
dataStyle.setBorderBottom(BorderStyle.THIN);
dataStyle.setBorderLeft(BorderStyle.THIN);
dataStyle.setBorderRight(BorderStyle.THIN);
// ===== 3. 创建表头行 =====
String[] headers = {"工号", "姓名", "部门", "手机号", "状态", "操作人", "创建时间"};
Row headerRow = sheet.createRow(0);
headerRow.setHeightInPoints(22);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
}
// ===== 4. 填充数据行 =====
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < dataList.size(); i++) {
EmployeeExportDto dto = dataList.get(i);
Row row = sheet.createRow(i + 1);
row.setHeightInPoints(18);
createStyledCell(row, 0, dto.getStaffNo(), dataStyle);
createStyledCell(row, 1, dto.getStaffName(), dataStyle);
createStyledCell(row, 2, dto.getDeptName(), dataStyle);
createStyledCell(row, 3, dto.getPhone(), dataStyle);
createStyledCell(row, 4, dto.getStatusName(), dataStyle);
createStyledCell(row, 5, dto.getOperatorName(), dataStyle);
createStyledCell(row, 6,
dto.getCreateTime() != null ? sdf.format(dto.getCreateTime()) : "", dataStyle);
}
// ===== 5. 设置列宽自适应 =====
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
int currentWidth = sheet.getColumnWidth(i);
sheet.setColumnWidth(i, Math.max(currentWidth + 512, 4000));
}
// ===== 6. 写入文件并上传 =====
// ...省略文件操作代码
return ossUrl;
}
/**
* 创建带样式的单元格.
*/
private void createStyledCell(Row row, int colIndex, String value, CellStyle style) {
Cell cell = row.createCell(colIndex);
cell.setCellValue(value != null ? value : "");
cell.setCellStyle(style);
}
五、样式复用原则
5.1 为什么要复用 CellStyle
POI 中每个 Workbook 最多支持约 64000 个 CellStyle。如果每个 Cell 都 new 一个 CellStyle,数据量大时会报错:
java.lang.IllegalStateException: The maximum number of Cell Styles was exceeded.
5.2 正确做法
java
// 正确:在循环外创建样式,循环内复用
CellStyle dataStyle = workbook.createCellStyle();
// ... 设置样式属性
for (int i = 0; i < dataList.size(); i++) {
Row row = sheet.createRow(i + 1);
Cell cell = row.createCell(0);
cell.setCellStyle(dataStyle); // 复用同一个样式对象
}
// 错误:在循环内创建样式
for (int i = 0; i < dataList.size(); i++) {
CellStyle style = workbook.createCellStyle(); // 每行创建新样式,浪费资源
// ...
}
5.3 需要多种样式时
如果需要交替行背景色等不同样式,预先创建有限个样式对象:
java
// 创建两种数据行样式(白色背景 + 浅灰背景)
CellStyle evenStyle = createDataStyle(workbook, IndexedColors.WHITE);
CellStyle oddStyle = createDataStyle(workbook, IndexedColors.GREY_25_PERCENT);
for (int i = 0; i < dataList.size(); i++) {
CellStyle style = (i % 2 == 0) ? evenStyle : oddStyle;
// 使用对应样式
}
六、常见样式模板
6.1 标准表头(蓝底白字)
java
Font headerFont = workbook.createFont();
headerFont.setBold(true);
headerFont.setColor(IndexedColors.WHITE.getIndex());
headerFont.setFontHeightInPoints((short) 11);
CellStyle headerStyle = workbook.createCellStyle();
headerStyle.setFont(headerFont);
headerStyle.setFillForegroundColor(IndexedColors.ROYAL_BLUE.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
headerStyle.setBorderTop(BorderStyle.THIN);
headerStyle.setBorderBottom(BorderStyle.THIN);
headerStyle.setBorderLeft(BorderStyle.THIN);
headerStyle.setBorderRight(BorderStyle.THIN);
6.2 标准数据行(左对齐+换行+边框)
java
Font dataFont = workbook.createFont();
dataFont.setFontHeightInPoints((short) 10);
CellStyle dataStyle = workbook.createCellStyle();
dataStyle.setFont(dataFont);
dataStyle.setAlignment(HorizontalAlignment.LEFT);
dataStyle.setVerticalAlignment(VerticalAlignment.CENTER);
dataStyle.setWrapText(true);
dataStyle.setBorderTop(BorderStyle.THIN);
dataStyle.setBorderBottom(BorderStyle.THIN);
dataStyle.setBorderLeft(BorderStyle.THIN);
dataStyle.setBorderRight(BorderStyle.THIN);
6.3 数字列(右对齐)
java
CellStyle numberStyle = workbook.createCellStyle();
numberStyle.cloneStyleFrom(dataStyle); // 基于数据行样式克隆
numberStyle.setAlignment(HorizontalAlignment.RIGHT);
// 可选:设置数字格式
DataFormat format = workbook.createDataFormat();
numberStyle.setDataFormat(format.getFormat("#,##0.00"));
6.4 错误标记行(红色字体)
java
Font errorFont = workbook.createFont();
errorFont.setColor(IndexedColors.RED.getIndex());
errorFont.setFontHeightInPoints((short) 10);
CellStyle errorStyle = workbook.createCellStyle();
errorStyle.setFont(errorFont);
errorStyle.setAlignment(HorizontalAlignment.LEFT);
errorStyle.setBorderTop(BorderStyle.THIN);
errorStyle.setBorderBottom(BorderStyle.THIN);
errorStyle.setBorderLeft(BorderStyle.THIN);
errorStyle.setBorderRight(BorderStyle.THIN);
七、列宽自适应的坑
7.1 中文字符宽度不准
autoSizeColumn 对中文计算宽度偏窄,因为 POI 默认按英文字符宽度计算。
解决:自适应后加余量
java
sheet.autoSizeColumn(i);
int width = sheet.getColumnWidth(i);
sheet.setColumnWidth(i, (int) (width * 1.2)); // 增加20%
7.2 大数据量性能问题
autoSizeColumn 需要遍历该列所有行来计算最大宽度,数据量大时很慢。
解决:数据量 > 1万行时改用固定列宽
java
if (dataList.size() > 10000) {
// 固定列宽
int[] columnWidths = {4000, 5000, 6000, 5000, 4000, 5000, 6000};
for (int i = 0; i < columnWidths.length; i++) {
sheet.setColumnWidth(i, columnWidths[i]);
}
} else {
// 自适应
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
sheet.setColumnWidth(i, Math.max(sheet.getColumnWidth(i) + 512, 4000));
}
}
八、最佳实践清单
- 样式对象在循环外创建:避免超出 64000 个 CellStyle 限制
- 表头和数据行使用不同样式:表头加粗+背景色,数据行左对齐+换行
- 所有单元格设置边框:提升可读性
- 表头行高适当加大:建议 20-22pt
- 数据行开启自动换行:防止长文本被截断
- 列宽自适应后加余量:中文内容需要额外 512-1024 的宽度
- null 值转空字符串:避免 Cell 显示 "null"
- 封装 createStyledCell 方法:减少重复代码
- 数字列右对齐:符合阅读习惯
- 大数据量用固定列宽:autoSizeColumn 性能差