EasyExcel写入操作详解:从原理到实践,解决大数据量Excel导出难题

EasyExcel写入操作详解:从原理到实践,解决大数据量Excel导出难题 1. 项目概述为什么我们需要关注EasyExcel的Write操作如果你在日常工作中处理过Excel导出大概率经历过内存溢出OOM的噩梦或者被POI那复杂的API和缓慢的性能折磨过。我第一次用传统Apache POI处理一个十几万行的数据导出时服务直接卡死查了半天日志才发现是堆内存被撑爆了。自那以后我就开始寻找更优雅的解决方案直到遇到了EasyExcel。这个标题“easyExcel用法之write写一”看似简单但它背后直指了一个高频、痛点明确的场景如何高效、稳定、省心地实现数据导出。EasyExcel是阿里巴巴开源的一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。它的“写”Write功能正是其核心价值所在。与传统的读写方式不同EasyExcel的写操作主打“省内存”和“易用性”。它底层默认使用SXSSF模式也就是流式写入数据是一行一行刷到磁盘的而不是一次性在内存中构建整个Excel对象。这意味着理论上你可以导出无限量的数据只要你的磁盘够大。这对于后台管理系统生成报表、数据仓库导出查询结果、金融系统生成对账单等场景简直是救命稻草。那么这个“write一”具体要解决什么问题呢我认为核心是**“快速上手并规避基础陷阱”**。很多人拿到工具照着最简单的demo跑通就以为万事大吉结果一上生产环境遇到复杂表头、自定义格式、大数据量就各种抓瞎。这篇文章我就结合自己趟过的坑从最基础的写入讲起拆解每一步的细节和原理让你不仅能“写得出来”更能“写得漂亮”、“写得稳健”。无论你是刚刚接触EasyExcel还是用过但总感觉有些地方不顺手相信接下来的内容都能给你带来实实在在的收获。2. 核心思路与模型设计理解EasyExcel的写入哲学要玩转EasyExcel的写操作不能只停留在调用API的层面得先理解它的设计思想。和传统POI“创建Workbook - 创建Sheet - 创建Row - 创建Cell - 塞数据”的 imperative命令式编程模型不同EasyExcel采用了一种更声明式、基于注解和回调的模型。这种转变正是其易用性和高性能的基石。2.1 注解驱动用POJO定义Excel的“模样”EasyExcel的核心思路是面向对象映射。你的数据不再是一个个需要手动摆放的单元格值而是一个个有意义的Java对象。Excel的表头、列顺序、数据类型都通过注解定义在你的实体类POJO上。这是写入操作的起点也是最容易出错的环节。Data // 使用Lombok简化代码 public class UserExportDTO { ExcelProperty(value 用户ID, index 0) private Long id; ExcelProperty(value 用户姓名, index 1) private String name; ExcelProperty(value 入职日期, index 2) DateTimeFormat(yyyy年MM月dd日) private Date hireDate; ExcelProperty(value 年薪万元, index 3) NumberFormat(#.##) private BigDecimal annualSalary; ExcelProperty(value 部门, index 4) private String department; }关键注解解析ExcelProperty: 最重要的注解value定义表头名称index定义列顺序从0开始。这里有个大坑如果不指定indexEasyExcel会按照类中字段的声明顺序来排序列但这依赖于Java反射在某些JVM或编译环境下顺序可能是不确定的导致生成的Excel列序混乱。因此强烈建议为每个需要导出的字段显式指定index。DateTimeFormat: 指定日期类型的格式化模式。如果不指定默认会使用yyyy-MM-dd HH:mm:ss可能不符合业务要求。NumberFormat: 指定数字类型的格式化模式。比如保留两位小数、千分位分隔等。设计考量这种注解方式将Excel的元信息格式、表头与数据填充逻辑解耦。开发人员只需要关注业务数据的组装Excel的样式和结构在实体类定义时就已确定大大减少了编写模板代码的复杂度也使得代码更易于维护。2.2 流式写入高性能背后的原理“为什么EasyExcel不怕大数据量” 答案就是流式写入。我们通过EasyExcel.write()方法的核心参数来理解这个过程。// 经典写入调用链 String fileName 用户列表.xlsx; EasyExcel.write(fileName, UserExportDTO.class) .sheet(用户数据) // 指定sheet名 .doWrite(dataList); // 执行写入传入数据列表看似简单的一行背后发生了这些事初始化写入上下文EasyExcel.write()会创建一个ExcelWriterBuilder它持有输出流、目标实体类等信息。创建薄Sheet调用.sheet()时并不会立即创建完整的Sheet对象而是创建一个轻量的“写作上下文”。触发流式写入doWrite(dataList)是核心。这里的dataList是一个Iterable通常是List。EasyExcel会遍历这个集合对于集合中的每一个UserExportDTO对象利用反射和之前定义的注解解析出该行数据每一列的值。将这一行数据以XML片段的形式直接追加flush到磁盘上的.xlsx文件中。内存中最多只保留少量行数据可配置写完即弃。与传统POI的对比POI (XSSF)在内存中构建完整的XSSFWorkbookDOM树所有单元格、样式都在内存里。数据量大了DOM树庞大必然OOM。POI (SXSSF)POI自己也提供了流式变体SXSSF但API依然相对复杂且样式处理、单元格类型映射需要大量手动代码。EasyExcel在SXSSF的基础上封装了更友好的API和自动映射逻辑让你几乎感知不到流式的存在却能享受流式的红利。实操心得doWrite方法传入的dataList不一定非要是内存中完整的List。对于超大数据集更优的做法是传入一个基于数据库游标或分页查询的迭代器实现真正的“边读边写”全程保持内存使用量在一个很低的水平。这是应对百万级数据导出的关键技巧。3. 基础写入实操从零到一生成你的第一份Excel理解了原理我们动手实现一个完整的导出流程。我会假设一个场景从数据库分页查询用户数据导出到Excel并包含一些基本样式。3.1 环境准备与依赖引入首先在项目的pom.xml中引入EasyExcel依赖。请注意版本尽量使用较新的稳定版。dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version !-- 请检查最新版本 -- /dependency为什么是这个版本3.x版本相较于早期的2.x版本在API设计上更加一致修复了大量已知Bug并且对WebFlux等新型框架支持更好。建议从3.x版本开始学习。3.2 构建数据模型与写入逻辑我们沿用前面定义的UserExportDTO。现在在Service层编写导出逻辑。Service Slf4j public class UserExportService { Autowired private UserMapper userMapper; // 假设使用MyBatis /** * 导出用户数据到Excel文件 * param response HttpServletResponse用于Web下载 */ public void exportUserList(HttpServletResponse response) { // 1. 设置响应头触发浏览器下载 String fileName user_export_ System.currentTimeMillis() .xlsx; response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); response.setHeader(Content-disposition, attachment;filename URLEncoder.encode(fileName, UTF-8)); // 2. 这里模拟分页查询数据。实际项目中应使用数据库分页。 // 关键点避免一次性查询全部数据到内存 int pageSize 1000; int pageNum 1; ListUserExportDTO dataList; try (ExcelWriter excelWriter EasyExcel.write(response.getOutputStream(), UserExportDTO.class).build()) { WriteSheet writeSheet EasyExcel.writerSheet(用户列表).build(); do { // 模拟分页查询每次取1000条 dataList userMapper.selectListForExport((pageNum - 1) * pageSize, pageSize); if (CollectionUtils.isEmpty(dataList)) { break; } // 3. 分批写入 excelWriter.write(dataList, writeSheet); log.info(已写入第 {} 页数据共 {} 条, pageNum, dataList.size()); pageNum; // 4. 每写一批清空列表帮助GC。如果是真实分页dataList每次都是新的无需手动clear。 // dataList.clear(); // 注意如果dataList是复用对象需要clear。 } while (dataList.size() pageSize); // 如果查不满一页说明是最后一页 } catch (IOException e) { log.error(导出Excel失败, e); throw new RuntimeException(导出失败); } log.info(导出完成); } }代码逐行解析与注意事项响应头设置Web场景这是让浏览器识别为Excel文件并弹出下载框的关键。Content-Type必须是Excel对应的MIME类型。文件名最好进行URL编码避免中文乱码。分页查询这是防止OOM的核心。即使EasyExcel写入是流式的但如果你的数据源如从数据库查询一次性把所有数据加载到内存的List里内存压力会转移到数据查询这一步同样会OOM。所以必须分页。使用ExcelWriter对象我们通过EasyExcel.write().build()创建了一个ExcelWriter对象并在try-with-resources语句中管理它确保输出流最后能被正确关闭。WriteSheet是对一个Sheet的抽象描述。分批写入在循环中每次查询一批数据如1000条立即调用excelWriter.write(dataList, writeSheet)写入。EasyExcel内部会处理好追加写入的逻辑。资源清理ExcelWriter实现了Closeable接口使用try-with-resources语法可以自动关闭底层的输出流和Workbook对象这是避免内存泄漏和文件锁定的重要习惯。踩坑记录曾经有一次线上导出任务卡住发现是服务器上生成了巨大的临时文件但流没有关闭导致磁盘空间被占满。根本原因就是在复杂的异常处理流程中没有保证excelWriter.close()一定被执行。所以务必使用try-with-resources或确保在finally块中关闭资源。4. 高级特性与样式自定义让Excel更专业基础导出跑通了但生成的Excel白底黑字不太美观。接下来我们看看如何自定义样式比如设置标题行背景色、字体加粗、自动列宽等。4.1 使用WriteCellStyle与WriteFont定义样式EasyExcel的样式配置稍微有点绕它通过WriteTableStyle或WriteHandler写入处理器来注入样式。最常用的方式是实现CellWriteHandler接口在单元格写入的各个生命周期进行拦截和样式设置。我们先看一个相对简单的方案使用HorizontalCellStyleStrategy水平单元格样式策略来统一设置表头和内容的样式。// 1. 定义表头样式 WriteCellStyle headWriteCellStyle new WriteCellStyle(); // 设置背景色为浅灰色 headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); // 设置字体 WriteFont headWriteFont new WriteFont(); headWriteFont.setFontHeightInPoints((short)12); // 字号 headWriteFont.setBold(true); // 加粗 headWriteCellStyle.setWriteFont(headWriteFont); // 设置水平居中 headWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER); // 2. 定义内容样式 WriteCellStyle contentWriteCellStyle new WriteCellStyle(); // 内容区字体 WriteFont contentWriteFont new WriteFont(); contentWriteFont.setFontHeightInPoints((short)11); contentWriteCellStyle.setWriteFont(contentWriteFont); // 设置自动换行对于长文本很有用 contentWriteCellStyle.setWrapped(true); // 3. 将样式策略注册到写入器 HorizontalCellStyleStrategy styleStrategy new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle); // 4. 在写入时使用这个策略 EasyExcel.write(fileName, UserExportDTO.class) .registerWriteHandler(styleStrategy) // 注册样式策略 .sheet(用户数据) .doWrite(dataList);参数详解与避坑IndexedColors提供了Excel内置的标准颜色索引。使用索引比直接使用RGB颜色值兼容性更好。setHorizontalAlignment对齐方式。除了CENTER还有LEFT,RIGHT,JUSTIFY等。setWrapped(true)这个配置非常实用。当单元格内容过长时会自动换行显示而不是被截断或撑开列宽。但要注意自动换行会影响行高如果数据行非常多可能会使最终文件体积略微增大。HorizontalCellStyleStrategy这个策略只能简单地区分表头和内容体并应用统一的样式。如果需要对特定列、特定行甚至特定单元格进行条件格式化它就力不从心了。4.2 通过CellWriteHandler实现精细控制对于更复杂的样式需求比如“年薪大于50万的标红”、“空单元格填充默认值”、“给某一列添加下拉框”对应你提供的热词“easyexcel实现下载模板自带下拉框”就需要用到CellWriteHandler。下面以实现“年薪大于50万标红”为例Component Slf4j public class SalaryHighlightCellWriteHandler implements CellWriteHandler { Override public void afterCellDispose(CellWriteHandlerContext context) { // 这个方法在单元格内容被写入、样式被应用之后调用是修改样式的合适时机 Cell cell context.getCell(); // 只处理内容行排除表头 if (context.getRowIndex() 0) { return; } // 判断是否是“年薪”列根据你的DTOindex3是年薪列 if (context.getColumnIndex() 3) { Object cellValue cell.getNumericCellValue(); // 注意数字类型在Excel中是NumericCell if (cellValue ! null) { double salary ((Number) cellValue).doubleValue(); if (salary 50.0) { // 大于50万 CellStyle cellStyle cell.getSheet().getWorkbook().createCellStyle(); // 克隆原有样式避免覆盖其他样式如边框、对齐方式 cellStyle.cloneStyleFrom(cell.getCellStyle()); // 创建红色字体 Font redFont cell.getSheet().getWorkbook().createFont(); redFont.setColor(Font.COLOR_RED); cellStyle.setFont(redFont); // 应用新样式到单元格 cell.setCellStyle(cellStyle); } } } } // 还需要实现其他方法如beforeCellCreate, afterCellCreate但这里我们用不到可以空实现。 Override public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {} Override public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {} }然后在写入时注册这个处理器EasyExcel.write(fileName, UserExportDTO.class) .registerWriteHandler(new SalaryHighlightCellWriteHandler()) // 注册自定义处理器 .registerWriteHandler(styleStrategy) // 仍然可以注册统一的样式策略 .sheet(用户数据) .doWrite(dataList);关键点解析时机选择afterCellDispose是操作单元格样式的黄金位置此时单元格已有内容和基础样式。列索引判断context.getColumnIndex()获取的是当前单元格的列索引从0开始。你需要根据实体类ExcelProperty中定义的index来匹配业务列。样式克隆cellStyle.cloneStyleFrom(...)非常重要如果不克隆直接创建新样式并设置会丢失之前已配置的样式比如通过HorizontalCellStyleStrategy设置的居中、换行等。这是一个很容易被忽略的细节。处理器顺序多个WriteHandler的执行顺序就是注册的顺序。通常先注册全局样式策略再注册特定的自定义处理器。实操心得CellWriteHandler功能强大但也不要滥用。每个单元格的写入都会经过这些处理器如果逻辑过于复杂会影响导出性能。对于大数据量导出应确保处理器中的逻辑尽可能轻量。对于简单的全局样式优先使用HorizontalCellStyleStrategy。5. 应对复杂场景动态表头、多Sheet与模板写入实际业务中导出需求千变万化。比如表头需要根据用户选择动态生成或者需要将不同类别的数据导出到同一个Excel的不同Sheet中。5.1 动态表头与复杂表头多级表头有时候表头不是简单的一行可能是两行甚至需要动态拼接。EasyExcel通过ListListString来支持复杂表头。// 构建动态的、复杂的表头 ListListString head new ArrayList(); // 第一行表头 ListString headRow0 new ArrayList(); headRow0.add(基本信息); // 这是一个跨列的表头 headRow0.add(基本信息); headRow0.add(基本信息); headRow0.add(薪酬信息); headRow0.add(部门信息); head.add(headRow0); // 第二行表头具体列名 ListString headRow1 new ArrayList(); headRow1.add(用户ID); headRow1.add(用户姓名); headRow1.add(入职日期); headRow1.add(年薪万元); headRow1.add(部门); head.add(headRow1); // 数据。注意此时不再需要ExcelProperty注解的实体类数据需要手动组织成ListListObject ListListObject dataList new ArrayList(); for (User user : users) { ListObject row new ArrayList(); row.add(user.getId()); row.add(user.getName()); row.add(user.getHireDate()); row.add(user.getAnnualSalary()); row.add(user.getDepartment()); dataList.add(row); } // 写入。注意这里传的是head和dataList没有.class参数 EasyExcel.write(fileName) .head(head) // 设置动态表头 .sheet(动态表头示例) .doWrite(dataList);注意事项表头与数据对齐head中每个ListString代表一行表头其元素数量必须与数据行ListObject的元素数量严格一致否则会出现错位。失去类型转换由于没有实体类映射日期、数字等类型需要你自己在组装ListObject时处理好格式。EasyExcel会尝试自动推断但不如注解方式可靠。适用场景适用于表头极度动态比如由用户配置生成、或者表头结构复杂多级合并的情况。如果表头固定强烈推荐使用注解方式更清晰、更安全。5.2 写入多个Sheet到同一个Excel这需要使用ExcelWriter并多次调用write方法指定不同的WriteSheet。String fileName 多Sheet报告.xlsx; try (ExcelWriter excelWriter EasyExcel.write(fileName).build()) { // Sheet 1: 用户列表 WriteSheet userSheet EasyExcel.writerSheet(0, 用户概览) // 索引名称 .head(UserExportDTO.class) .build(); excelWriter.write(userList, userSheet); // Sheet 2: 部门统计假设是另一个DTO WriteSheet deptSheet EasyExcel.writerSheet(1, 部门统计) .head(DepartmentStatDTO.class) .build(); excelWriter.write(deptStatList, deptSheet); // Sheet 3: 使用动态表头 WriteSheet dynamicSheet EasyExcel.writerSheet(2, 动态数据) .head(dynamicHead) // 前面定义的动态表头 .build(); excelWriter.write(dynamicDataList, dynamicSheet); } // try-with-resources 自动关闭 writer关键点同一个ExcelWriter可以写入多个Sheet每个WriteSheet对象定义了对应Sheet的索引、名称、表头等信息。这非常适合于生成包含汇总、明细、图表数据等多个部分的综合报告。5.3 基于模板的写入这是另一个强大功能。先准备一个设计好样式、固定表头、甚至带有公式的Excel模板文件.xlsx。然后EasyExcel可以定位到模板中的特定位置通过坐标如B3开始填充数据。// 假设有一个模板文件 template.xlsx在Sheet1的B2单元格开始是数据填充区 String templateFileName classpath:template/template.xlsx; String outputFileName filled_report.xlsx; // 使用模板写入 EasyExcel.write(outputFileName) .withTemplate(templateFileName) // 指定模板 .sheet() // 默认使用模板中的第一个Sheet也可指定名字 .doWrite(dataList);模板写入的核心占位符在模板中你可以使用{}作为占位符。例如在某个单元格写入{{name}}在调用doFill模板填充专用方法时可以通过传入一个Map或对象将name属性值填充到该位置。数据填充区域对于列表数据你需要在模板中定义好一个“区域”。最简单的方式是在模板中写好表头行数据从表头下一行开始填充。EasyExcel会自动向下追加行。保留样式模板中所有的样式字体、颜色、边框、公式、合并单元格等都会被保留。你填充的数据会继承对应单元格的样式。注意事项模板功能非常适用于生成格式固定、样式复杂的报表比如合同、发票、带有复杂计算公式的统计表。但它比简单的写入要复杂一些需要精心设计模板文件。如果只是简单的列表导出直接用常规写入更简单。6. 性能调优与常见问题排查即使使用了EasyExcel在大数据量或高并发场景下不注意细节依然可能遇到性能瓶颈或诡异错误。这里分享一些调优经验和常见问题的排查思路。6.1 性能优化关键点禁用自动列宽计算默认情况下EasyExcel会尝试计算每一列的最佳宽度这是一个非常耗时的操作尤其是数据量大时。EasyExcel.write(fileName, UserExportDTO.class) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 这是默认开启的 .sheet() .doWrite(dataList);如果你能接受固定列宽或者不介意列宽强烈建议关闭此策略可以大幅提升写入速度。// 不注册任何ColumnWidthStyleStrategy或者使用自定义的简单策略调整SXSSF的滑动窗口大小EasyExcel底层使用SXSSF它会在内存中保留一个“滑动窗口”的行数默认100行。写入时超出窗口的行会被刷到磁盘临时文件。如果你的单行数据非常大包含超长文本或很多列可以适当调小这个窗口以减少内存占用但可能会轻微增加IO。不过EasyExcel目前没有直接暴露这个参数的API通常默认值已足够。优化数据查询这是最大的性能瓶颈来源。确保你的数据库查询有合适的索引避免SELECT *只查询导出需要的字段。使用流式查询如MyBatis的CursorJDBC的ResultSet.TYPE_FORWARD_ONLY配合分页实现真正的流式导出。避免在WriteHandler中执行重逻辑如前所述CellWriteHandler中的代码会对每个单元格执行。避免在这里进行数据库查询、复杂的计算或IO操作。6.2 常见错误与解决方案这里结合你提供的一些错误热词进行分析java.lang.OutOfMemoryError: Java heap space原因最可能的原因不是EasyExcel本身而是你的数据源一次性加载了太多数据到内存如没有分页的List。排查检查导出Service中的数据查询逻辑。是否一次性SELECT * FROM huge_table必须改为分页查询。其次检查是否在WriteHandler中累积了大量数据如用一个List保存所有单元格值再处理。com.alibaba.excel.exception.ExcelGenerateException: Can not close IO.原因通常发生在Web导出场景输出流被提前关闭或响应被提交后尝试写入。排查确保response.getOutputStream()只被获取一次并且在EasyExcel写入完成前没有其他代码如过滤器、拦截器关闭或刷新它。确保在Controller方法中没有返回ResponseBody或其他会导致Spring MVC自动处理响应体的注解与手动操作输出流冲突。通常导出接口的方法返回类型应为void。绝对不要在try-catch外再调用response.getOutputStream()。NumberFormatException或 日期显示为数字原因实体类字段类型如BigDecimal,Date与Excel单元格格式不匹配或者注解配置错误。排查检查DateTimeFormat和NumberFormat注解的模式字符串是否正确。确认Java对象中的值本身是有效的。一个为null的Date字段如果不处理写入Excel可能会显示为数字。对于可能为空的数值或日期字段可以考虑在DTO的getter方法中返回默认值或者使用Converter转换器进行自定义处理。生成的Excel文件损坏无法打开原因流未正确关闭这是最常见原因。必须使用try-with-resources确保ExcelWriter关闭。写入过程中发生异常异常导致写入中断文件不完整。并发写入同一文件多线程同时写一个文件。解决严格使用try-with-resources管理ExcelWriter。做好异常处理在catch块中尽可能清理可能产生的残缺文件。为每个导出任务生成唯一的文件名如使用UUID或时间戳。关于热词ram check failed address 0x20000000. write: 0xe7febe00 e083e069 read: 0x00和failed to write to target ram注意这些错误信息看起来更像是嵌入式开发或硬件编程中的内存读写错误例如通过JTAG/SWD向微控制器RAM中烧写程序时出现的校验失败与EasyExcel或Java应用层面的Excel导出无关。可能是搜索词关联到了不相关的技术领域。在Java应用导出Excel的语境下如果出现内存问题通常就是标准的OutOfMemoryError。6.3 一个健壮的导出服务示例结合以上所有点这里给出一个相对健壮的Web导出示例RestController RequestMapping(/export) Slf4j public class ExportController { GetMapping(/users) public void exportUsers(HttpServletResponse response, RequestParam(required false) String deptName) { // 1. 设置响应头 String fileName 用户导出_ System.currentTimeMillis() .xlsx; response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(utf-8); response.setHeader(Content-disposition, attachment;filename*UTF-8 URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll(\\, %20)); // 2. 定义样式策略简单表头样式 WriteCellStyle headStyle new WriteCellStyle(); headStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); WriteFont headFont new WriteFont(); headFont.setBold(true); headStyle.setWriteFont(headFont); HorizontalCellStyleStrategy styleStrategy new HorizontalCellStyleStrategy(headStyle, new WriteCellStyle()); // 3. 执行导出 try (ExcelWriter excelWriter EasyExcel.write(response.getOutputStream(), UserExportDTO.class) .registerWriteHandler(styleStrategy) // .registerWriteHandler(new CustomCellWriteHandler()) // 可按需添加自定义处理器 .build()) { WriteSheet writeSheet EasyExcel.writerSheet(用户列表).build(); int pageSize 2000; int pageNum 1; ListUserExportDTO dataChunk; do { // 分页查询数据这里是关键 dataChunk userService.getUsersForExport(deptName, pageNum, pageSize); if (CollectionUtils.isEmpty(dataChunk)) { break; } excelWriter.write(dataChunk, writeSheet); pageNum; // 可选每写几页记录一下进度或者检查一下线程是否被中断用于支持取消操作 if (pageNum % 10 0) { log.debug(导出进度已处理 {} 页数据, pageNum); } } while (dataChunk.size() pageSize); } catch (IOException e) { log.error(导出用户列表至Excel时发生IO异常, e); // 注意此处通常不能再向response写错误信息因为流可能已部分写入。 // 更优的做法是在导出开始前做好参数校验避免中途出错。 throw new RuntimeException(系统繁忙导出失败); } catch (Exception e) { log.error(导出用户列表至Excel时发生未知异常, e); throw new RuntimeException(导出过程发生错误); } // 4. 方法返回voidSpring MVC会处理完response } }这个示例包含了分页查询、响应头设置、资源自动关闭、基本样式和简单的异常处理是一个可以直接参考的生产级代码骨架。记住分页查询和资源关闭是保证稳定性的生命线。