简介:本资源面向Java后端开发者与需要实现数据导出功能的程序员,聚焦使用Apache POI在Java中导出Excel这一常见需求。内容围绕从数据库查询数据、构建工作簿、设置表头、填充数据行到保存文件的完整流程展开,并给出Maven依赖配置与实体类映射思路,适合具备一定Java与JDBC基础、希望快速落地导出模块的中级开发者参考。压缩包共519个文件,约567KB,以xml配置、class字节码、java源码及sample示例文件为主,另含少量yml、log等辅助文件,整体结构便于按模块查阅与复用。目前已有388人学习下载。资源提供了可直接借鉴的导出方法实现与扩展方向,如样式设置、数据验证与公式计算,能帮助读者减少重复造轮子的时间,快速搭建稳定的Excel导出能力。
1. 从一份 poi_export_excel.zip 说起:Java 导出 Excel 到底能省多少事
后台管理系统里最容易被低估的活儿,就是导出 Excel。产品经理一句“加个导出按钮”,落到开发身上往往是一整天的折腾:表头合并、单元格样式、大数据量分页、内存溢出,随便一个都能让人加班到深夜。我见过不少项目,导出功能上线第一天就被运营吐槽“下载下来打不开”“数字变成科学计数法”“日期全是乱码”。这份 poi_export_excel.zip 就是冲着这些痛点来的——它把 Java 生态里最成熟的 Apache POI 封装成一套可直接复用的导出工具,覆盖从简单列表到复杂多级表头的常见场景。如果你正在做管理后台、报表系统,或者单纯想搞清楚 POI 的 Workbook、Sheet、Cell 到底怎么配合,这份源码值得拉下来跑一遍。它不依赖任何重型框架,一个 main 方法就能看到导出效果,对新手友好,对老手也能当脚手架用。
2. 技术选型与目录结构:为什么是 POI 而不是 EasyExcel
2.1 POI 与 EasyExcel 的取舍逻辑
Java 导出 Excel 绕不开两个名字:Apache POI 和阿里开源的 EasyExcel。EasyExcel 主打低内存、注解驱动,写起来确实清爽,但它的抽象层比较厚,遇到复杂表头合并、自定义样式、公式计算时,反而要绕回 POI 的 API。这份源码选择 POI 作为底层,原因很直接:POI 是 Excel 文件格式的“原教旨”实现,HSSF 管 .xls,XSSF 管 .xlsx,SXSSF 管超大文件流式写入,三套 API 覆盖了从几百行到百万行的全部场景。源码里没有引入 EasyExcel,就是为了让使用者看清每一行数据是怎么落到 Cell 里的,而不是被注解和监听器遮住视线。
常见做法是:中小数据量(万行以内)用 XSSFWorkbook,内存占用可接受;数据量再大就切 SXSSFWorkbook,它会在磁盘上滚动临时文件,避免 OOM。这份源码把两种 Workbook 的创建逻辑都封装在工厂类里,调用方只需要传一个类型枚举。
2.2 目录结构一览
解压后是一个标准的 Maven 工程,结构不复杂,但分层清晰:
poi_export_excel/ ├── pom.xml ├── src/main/java/ │ ├── com/example/export/ │ │ ├── ExcelExportUtil.java // 核心导出工具类 │ │ ├── ExcelExportConfig.java // 导出配置(表头、样式、sheet名) │ │ ├── ExcelWorkbookFactory.java // Workbook 创建工厂 │ │ ├── ExcelStyleBuilder.java // 单元格样式构建器 │ │ └── model/ │ │ └── ExportColumn.java // 列定义模型 │ └── com/example/demo/ │ └── ExportDemo.java // 可运行的示例入口 └── src/main/resources/ └── template/ // 预留的模板文件目录pom.xml 里核心依赖只有两个:org.apache.poi:poi-ooxml和org.apache.poi:poi,版本建议用 5.x 系列,对 .xlsx 的支持更完整。另外引入了commons-lang3做字符串判空,slf4j-api做日志门面。没有 Spring、没有 MyBatis,保持最小依赖,方便嵌入任何项目。
2.3 核心模型 ExportColumn 的设计
导出功能最怕的是“列定义散落在各处”。这份源码把每一列抽象成 ExportColumn,字段包括:列标题、字段名(对应数据对象的属性)、列宽、对齐方式、是否自动换行、日期格式。这样表头顺序、样式、数据映射全部由配置驱动,新增一列只需要加一个 ExportColumn 对象,不用改导出逻辑。
public class ExportColumn { private String title; // 表头显示文字 private String fieldName; // 数据对象中的属性名,反射取值 private int width; // 列宽,单位是 1/256 字符宽 private HorizontalAlignment align; // 对齐方式 private boolean wrapText; // 是否自动换行 private String dateFormat; // 日期格式化,如 yyyy-MM-dd HH:mm:ss // 构造方法、getter/setter 省略 }参数说明:width这个单位容易踩坑,POI 里setColumnWidth传的是 1/256 个字符宽度,比如想设 20 个字符宽,要传20 * 256。源码里在 ExcelExportUtil 做了换算,调用方直接写 20 就行。fieldName用反射取值,所以数据对象可以是任意 POJO,不要求实现特定接口。
3. 核心导出链路:从 List 到 Excel 的完整代码拆解
3.1 入口方法 export 的签名与职责
ExcelExportUtil 对外只暴露一个 export 方法,签名如下:
public static <T> void export( List<T> data, List<ExportColumn> columns, ExcelExportConfig config, OutputStream out) throws IOException四个参数分别对应:数据列表、列定义、导出配置(sheet 名、是否冻结首行、Workbook 类型)、输出流。方法内部按“创建 Workbook → 创建 Sheet → 写表头 → 写数据 → 写流 → 关闭资源”的顺序执行。把 OutputStream 作为参数传入,是为了让调用方自己决定写到文件还是 HTTP 响应,Web 场景下直接传response.getOutputStream()即可。
3.2 创建 Workbook 与 Sheet
Workbook workbook = ExcelWorkbookFactory.create(config.getWorkbookType()); Sheet sheet = workbook.createSheet(config.getSheetName()); // 冻结首行,方便滚动查看 if (config.isFreezeHeader()) { sheet.createFreezePane(0, 1); }ExcelWorkbookFactory 根据枚举返回 HSSFWorkbook、XSSFWorkbook 或 SXSSFWorkbook。SXSSFWorkbook 需要额外设置new SXSSFWorkbook(100),表示保留最近 100 行在内存,其余刷到磁盘。这个 100 是经验值,行数越多可以调小,但太小会导致频繁 IO。冻结窗格用createFreezePane(0, 1),第一个参数是列分割,第二个是行分割,0,1 表示冻结第一行。
3.3 写表头与样式构建
表头样式通过 ExcelStyleBuilder 统一构建,避免每个 Cell 重复创建 CellStyle(POI 对 CellStyle 数量有限制,.xls 最多 4000 个,.xlsx 虽然宽松但也不宜滥用)。
CellStyle headerStyle = ExcelStyleBuilder.buildHeaderStyle(workbook); Row headerRow = sheet.createRow(0); for (int i = 0; i < columns.size(); i++) { ExportColumn col = columns.get(i); Cell cell = headerRow.createCell(i); cell.setCellValue(col.getTitle()); cell.setCellStyle(headerStyle); sheet.setColumnWidth(i, col.getWidth() * 256); // 字符宽转 POI 单位 }逻辑说明:表头固定在第 0 行,列宽在这里一次性设置。buildHeaderStyle内部设置了加粗、居中、背景色、边框。注意 CellStyle 必须由 Workbook 创建,不能跨 Workbook 复用,否则会抛异常。
3.4 反射填充数据行
for (int rowIdx = 0; rowIdx < data.size(); rowIdx++) { T item = data.get(rowIdx); Row row = sheet.createRow(rowIdx + 1); // 从第 1 行开始,避开表头 for (int colIdx = 0; colIdx < columns.size(); colIdx++) { ExportColumn col = columns.get(colIdx); Cell cell = row.createCell(colIdx); Object value = ReflectUtil.getFieldValue(item, col.getFieldName()); fillCell(cell, value, col, workbook); } }ReflectUtil.getFieldValue是源码里自己封装的反射工具,支持从父类字段取值,也支持嵌套属性(如user.name)。fillCell根据值的类型分派:String 直接 setCellValue;Number 走 setCellValue(double);Date 先格式化再写入;null 写空字符串。日期格式从 ExportColumn 的 dateFormat 取,如果为空则用默认的yyyy-MM-dd。
3.5 输出与资源关闭
try { workbook.write(out); out.flush(); } finally { workbook.close(); // SXSSFWorkbook 会清理临时文件 }注意 SXSSFWorkbook 的 close 会删除磁盘临时文件,不调用会导致临时文件堆积。另外workbook.write之后不要立即关 OutputStream,Web 场景下由容器管理。如果是写文件,用 try-with-resources 包住 FileOutputStream。
4. 避坑与排查:导出 Excel 时最容易翻车的五个点
4.1 现象:下载的文件打开提示“格式不正确”
原因通常是 Content-Type 或文件名编码不对。Web 导出时如果响应头写成application/octet-stream但文件名含中文没做 URL 编码,浏览器会截断文件名导致扩展名丢失。解决:设置response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),文件名用URLEncoder.encode(fileName, "UTF-8")处理,并加上response.setHeader("Content-Disposition", "attachment;filename=" + encodedName)。
4.2 现象:大数据量导出时 OOM
原因是用 XSSFWorkbook 一次性把所有行放在内存。解决:数据超过 5 万行时切换到 SXSSFWorkbook,并设置合适的行窗口。源码里通过 ExcelExportConfig 的 workbookType 字段控制,调用方根据数据量选择。另外注意 SXSSF 不支持getRow读取已刷盘的行,如果导出逻辑里有回读操作要避开。
4.3 现象:数字变成科学计数法或日期变成数字
原因是 Cell 类型没设对。POI 默认 createCell 是 Blank 类型,setCellValue(String) 会变成文本,setCellValue(double) 才是数字。日期如果直接 setCellValue(Date) 而不设 DataFormat,Excel 会显示成数字序列。解决:在 fillCell 里对 Date 类型先cell.setCellValue(formattedString)或者设置CellStyle.setDataFormat(workbook.createDataFormat().getFormat("yyyy-MM-dd"))再 setCellValue(date)。源码采用前者,更直观。
4.4 现象:表头合并后样式错乱
原因是对合并区域内的非左上角单元格也设置了样式。POI 的合并单元格addMergedRegion只保留左上角的值和样式,其他单元格的值会被忽略但样式可能残留。解决:合并前先创建所有单元格,只给左上角设值和样式,其余留空。源码里多级表头的处理逻辑是先算好合并区间,再统一写值。
4.5 现象:SXSSFWorkbook 写完文件后临时文件没删
原因是没调用workbook.dispose()或close()。SXSSF 会在java.io.tmpdir下建临时文件,JVM 退出前不清理会占满磁盘。解决:在 finally 块里调用workbook.close(),它会触发 dispose。如果用的是 POI 4.x 以下版本,需要手动调dispose()。
5. 二次开发与改造:把这套工具接进你的项目
5.1 接入 Spring Boot 的两种姿势
第一种是当工具类用:把 ExcelExportUtil 和 model 包直接拷进项目,在 Controller 里组装 List 和 columns 后调用 export。适合导出场景不多的项目。第二种是做成 Starter:把 ExcelExportUtil 注册成 Bean,通过@ConfigurationProperties读取默认配置(如默认 sheet 名、默认列宽),再用自定义注解@ExcelColumn标注在 DTO 字段上,启动时扫描注解生成 ExportColumn 列表。这样业务代码只需要返回 List ,导出逻辑完全解耦。
5.2 扩展多级表头与合并单元格
源码目前只支持单行表头。要支持多级表头,需要在 ExcelExportConfig 里增加一个headerDepth字段,然后改造写表头的逻辑:先根据 columns 的层级关系算出每个表头单元格的起止行列,再调用sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol))。注意合并区域的坐标是 0-based,且不能重叠。一个实用技巧是:把表头定义成一个树形结构,每个节点带 title 和 children,递归写入。
5.3 用模板导出替代硬编码样式
如果样式要求复杂(比如带公司 logo、固定页脚、多个 sheet),硬编码 CellStyle 会很痛苦。更优雅的做法是准备一个 .xlsx 模板文件,用workbook = WorkbookFactory.create(new FileInputStream(template))加载,然后定位到指定 sheet 和起始行,只填充数据不动样式。源码的 resources/template 目录就是为此预留的。改造时注意:模板文件不要放在 jar 包内用getResourceAsStream读,因为 POI 需要随机访问,流式读取大模板可能失败,建议解压到临时文件再读。
5.4 验证改造是否成功的三个检查点
改完之后别急着上线,按这三步验一遍:第一,用wb.getNumberOfSheets()和sheet.getLastRowNum()确认 sheet 数和行数符合预期;第二,用cell.getCellType()检查关键列的类型,数字列必须是 NUMERIC,日期列必须是 NUMERIC 且带 DataFormat;第三,用 Excel 打开后按 Ctrl+End 看是否跳到了预期区域,如果跳到了很远的空白单元格,说明有多余的样式或空行被创建了。
5.5 一个我踩过的坑
早些年做报表导出,为了图省事在循环里workbook.createCellStyle(),结果 .xls 格式导出 3000 行就抛IllegalStateException: The maximum number of cell styles was exceeded。后来改成在循环外创建好所有样式,循环内只cell.setCellStyle(style),问题消失。从那以后我每次写导出逻辑,都强制把 CellStyle 的创建提到循环外,并且用workbook.getNumCellStyles()打印一下数量,超过 100 就警觉。希望这个习惯也能帮到你。
本文还有配套的精品资源,点击获取