Apache POI Excel 导出样式美化实战指南

Apache POI Excel 导出样式美化实战指南

一、概述

使用 Apache POI 导出 Excel 时,默认样式非常简陋(无边框、无背景色、列宽不自适应)。在实际项目中,导出给用户的 Excel 文件需要具备良好的可读性,包括表头美化、数据对齐、边框、列宽自适应等。

本文以 XSSFWorkbook(.xlsx 格式)为例,系统介绍 POI 中样式相关 API 的使用方法。


二、POI 样式体系结构

Workbook(工作簿) ├── Font(字体)--- 控制文字样式 ├── CellStyle(单元格样式)--- 控制单元格外观 ├── Sheet(工作表) │ ├── Row(行)--- 控制行高 │ │ └── Cell(单元格)--- 绑定 CellStyle │ └── 列宽设置

关键关系

  • FontCellStyle 引用
  • CellStyleCell 引用
  • 一个 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));
    }
}

八、最佳实践清单

  1. 样式对象在循环外创建:避免超出 64000 个 CellStyle 限制
  2. 表头和数据行使用不同样式:表头加粗+背景色,数据行左对齐+换行
  3. 所有单元格设置边框:提升可读性
  4. 表头行高适当加大:建议 20-22pt
  5. 数据行开启自动换行:防止长文本被截断
  6. 列宽自适应后加余量:中文内容需要额外 512-1024 的宽度
  7. null 值转空字符串:避免 Cell 显示 "null"
  8. 封装 createStyledCell 方法:减少重复代码
  9. 数字列右对齐:符合阅读习惯
  10. 大数据量用固定列宽:autoSizeColumn 性能差