资讯详情

SpringBoot整合EasyExcel:从入门到百万级数据导出实践

📅 2026/10/12 23:54:18 | 华诺云谱 👁 阅读
SpringBoot整合EasyExcel:从入门到百万级数据导出实践
SpringBoot 后端接口里做 Excel 导出基本是每个 Java 开发者都绕不开的活。以前用 Apache POI 硬写代码又长又啰嗦数据量稍大还容易内存溢出动不动就要调大 JVM 参数。后来换成 EasyExcel同样的功能代码量直接少了一大半内存也稳得不行谁用谁知道确实是香。这篇博客就来聊聊 SpringBoot EasyExcel 做文件导出的完整玩法从最简单的入门 Demo 到大数据量分批写、动态表头、模板填充和样式定制再到我踩过的一些坑一次讲清楚。如果你是刚接触 EasyExcel 的新手可以照着第一部分到第二部分抄代码入门如果你的项目已经上了 EasyExcel进阶场景和问题排查这两块应该能帮你少走不少弯路。1. 为什么选 EasyExcel 而不是直接用 POI1.1 Excel 处理到底难在哪做导出功能本质上就是把内存里的业务数据按照约定的格式写到 xlsx 文件里。听起来很简单但 Excel 文件的格式复杂度是被很多人低估的。一个 xlsx 文件本质是一个 ZIP 压缩包里面有一堆 XML 文件描述工作表、单元格、样式、图片、公式、共享字符串……如果用底层 API 直接操作你要自己保证这些 XML 的逻辑一致随便一个单元格样式写错了文件就可能打不开。Apache POI 是 Java 领域最老牌的 Excel 操作库功能很全几乎什么都能做。但它有一个非常典型的问题它默认把整个工作簿、每个工作表、每一行、每一个单元格都构建成完整的内存对象模型也就是常说的 DOM 模式。数据量小的时候没问题一旦要导出的数据达到几十万行、上百万行内存里同时存在几十万个 Java 对象再加上样式对象、字体对象内存很容易就爆了。我当年接过一个报表导出需求单表 60 万条记录直接用 POI 一次性构建 Workbook结果 JVM 报 OutOfMemoryError最后没办法只能分批切片导出一个 Excel 拆成好几个文件再压缩给用户体验很差。这个问题的根源就是整个文件被整体加载到内存里了。1.2 EasyExcel 的“香”到底香在哪EasyExcel 的出现很大程度上就是冲着 POI 的这些痛点去的。它最核心的差异是采用了流式读写机制不构建整棵内存对象树。我习惯用一个类比来解释POI 那种方式相当于你写一篇一万字的文章得先在心里把整篇文章一个字一个字背下来再一次性写到纸上EasyExcel 则是边想边写每一行数据处理完就可以丢掉了所以无论数据有多少内存占用都相对平稳。对于导出来说EasyExcel 底层会通过一个 SAX 风格的行写入器把实体对象逐行写出到输出流对象写完一行这一行的临时数据就可以被垃圾回收内存占用基本和写入行数没有线性关系。这在处理百万级数据导出时是决定性的优势。同时EasyExcel 把 API 设计得非常贴近业务你想导出一个 List只用 EasyExcel.write(...).sheet(...).doWrite(list)一行代码就能完成。实体类上用注解声明列名、列宽、日期格式、数字格式语义清楚维护起来也容易。相比 POI 里需要手写遍历单元格、设置样式的样板代码开发效率提升是非常明显的。1.3 什么情况下仍然考虑 POI也不是什么场景都无脑选 EasyExcel。有些特殊需求比如生成非常复杂的动态公式链、操作 Excel 里的图表对象、读写宏文件、做复杂的单元格合并和公式依赖分析POI 的底层模型会更直接可行性也更高。EasyExcel 现在的版本虽然也在逐步增强底层扩展能力但主要目标是高效地完成导入导出不是为了成为一个无所不能的 Excel 编辑器。另外如果你的项目原来就有大量基于 POI 的代码也没必要立刻推翻重写可以在新开发的导出接口里用 EasyExcel老接口按风险级别逐步替换。我经手的某个模拟项目 X就是把高频的大报表导出接口从 POI 换成了 EasyExcel内存和老代码维护成本都降下来了而那些只导出几百行的小接口就保持原样没必要动。2. 从零搭一个入门导出接口2.1 依赖引哪些版本怎么选如果你的项目是标准 SpringBoot 工程直接在 pom.xml 里加 EasyExcel 的依赖就行。注意现在的版本号用起来要稍微留个心我用的比较稳定的是 3.x 系列dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.4/version /dependency3.x 的主要 API 和 2.x 基本一致但一些细节有明显变化比如样式接口、WriteCellData 相关的 API2.x 里很多写法到 3.x 不兼容。如果你是从 2.x 升级上来的建议先跑一遍项目里的导出用例确认没有编译错误和样式丢失再上线。2.x 已比较老除非老项目维护否则新项目直接上 3.x。如果遇到依赖冲突可以优先排查是不是项目里同时引入了不同版本的 POI。EasyExcel 本身是基于 POI 的但它的核心特性并不依赖你项目里手动引入 POI默认传递依赖通常够用。硬要手动引入 POI 版本反而容易触发 NoSuchMethodError 这类问题。2.2 用注解定义导出结构EasyExcel 导出的核心玩法就是注解。定义一个和 Excel 表格结构对应的实体类字段上标注列名和格式框架就能自动完成映射。以某个运营后台要导出的用户列表为例import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.format.DateTimeFormat; import com.alibaba.excel.annotation.format.NumberFormat; import lombok.Data; import java.math.BigDecimal; import java.util.Date; Data public class UserExportVO { ExcelProperty(用户ID) private Long userId; ExcelProperty(用户昵称) private String nickName; ExcelProperty(注册时间) DateTimeFormat(yyyy-MM-dd HH:mm:ss) private Date registerTime; ExcelProperty(账户余额) NumberFormat(#,##0.00) private BigDecimal balance; }用 ExcelProperty 定义表头和字段的对应关系是导出最基础也最常用的方式。注意字段顺序决定了表格列顺序所以实体类里字段的排列要提前设计好别写到一半发现列的顺序打乱了。DateTimeFormat 和 NumberFormat 做的是数据类型格式化可以省掉你在业务代码里事先把 Date 转 String、把 BigDecimal 转 String 这些麻烦步骤。但有一点要注意如果你把一个字段定义为 String 类型然后在注解上放 DateTimeFormat框架是按字符串生成的日期格式不会生效正确做法是字段类型保持 Date、BigDecimal 这类原始类型让注解去处理格式。不过我还遇到过一种情况业务方希望在导出的“账户余额”列前面加人民币符号。如果你直接用 String 字段拼上符号会导致这一列变成文本格式后续用户拿到 Excel 再做求和、统计就不方便了。更合理的做法是保持 BigDecimal 字段和 NumberFormat 注解把货币符号放到表头里比如“账户余额(元)”既满足展示需求又不破坏单元格数值类型。2.3 核心导出代码一个可以直接复制的 Controller接下来是控制器层和响应头。导出接口和普通 JSON 接口最大的不同是它要直接把 Octet 流或者 Excel 的 MIME 类型写回给前端所以响应头的设置很关键。下面是带登录鉴权假设的 Controller 示例引入了一个简单的用户服务import com.alibaba.excel.EasyExcel; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.List; RestController RequestMapping(/user) public class UserExportController { private final UserQueryService userQueryService; public UserExportController(UserQueryService userQueryService) { this.userQueryService userQueryService; } GetMapping(/export) public void exportUserList(HttpServletResponse response) throws IOException { // 文件名要 URL 编码避免中文乱码 String fileName URLEncoder.encode(用户列表, StandardCharsets.UTF_8.name()) .replaceAll(\\, %20); response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setCharacterEncoding(UTF-8); response.setHeader(Content-Disposition, attachment;filename*UTF-8 fileName); // 简易版一次性导出一批数据 ListUserExportVO dataList userQueryService.listAllUser(); EasyExcel.write(response.getOutputStream(), UserExportVO.class) .sheet(用户信息) .doWrite(dataList); } }这里有几个细节值得展开。响应头 Content-Disposition 里filename 使用filename*UTF-8这种 RFC 5987 格式是解决中文文件名乱码最可靠的方式。以前老代码喜欢写attachment;filename用户列表.xlsx在部分浏览器和操作系统组合下会乱码甚至直接下载失败。改用 URLEncoder 先把中文编码再放到filename*UTF-8后面几乎没有再碰到过乱码问题。MIME 类型 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 是标准 xlsx 文件的类型。如果你偷懒用 application/octet-stream 也能下载但部分浏览器识别不对打开文件时可能提示格式不匹配。EasyExcel.write 第一个参数接收的是 OutputStream你可以直接传 response.getOutputStream()框架会负责把数据写入这个流并在 doWrite 内部完成收尾。注意不要在 doWrite 之前关闭 response 的流否则什么都不会写进去。我还见过一个容易踩的坑有些同学习惯在 Service 里把 List 拼成很长一段字符串再返回或者把整个 List 先转成 JSON 存到缓存导出时再从缓存读。这在数据量小时没感觉数据量一大额外的转换开销会直接拖慢导出接口。正确姿势是拿到 List 就直接写不要在导出链路里做多余的中间态转换。2.4 前端怎么接收下载虽然这篇主要讲后端但导出联调时前端经常问怎么接。我先说最简单的方案直接用浏览器地址访问下载接口或者用 window.location.href 跳转。这种方式的缺点是你没法在响应里拿到业务错误信息比如导出失败时后端返回 JSON 错误浏览器会直接把 JSON 当成文件下载。更稳的做法是用 fetch 或者 axios 请求接口时配置 responseType: blob拿到 Blob 再创建一个临时下载链接。fetch(/user/export, { method: GET }) .then(response { if (!response.ok) throw new Error(导出请求失败); return response.blob(); }) .then(blob { const url URL.createObjectURL(blob); const link document.createElement(a); link.href url; link.download 用户列表.xlsx; link.click(); URL.revokeObjectURL(url); }) .catch(err console.error(err));用 Blob 方式接收后端如果抛出 500前端就能捕获到错误并弹提示体验会好很多。还有一种做法是在请求里带一个 Authorization 头很多前后端分离项目下载接口也要校验登录态直接用 window.location.href 没法带自定义请求头这种情况就必须用 Blob fetch 方案。我见过不少团队因为导出接口要带 Token前段跳转半天下载不了最后都改成了 Blob 接收。3. 进阶场景大数据量、动态表头、模板填充与样式3.1 大数据量导出分批查询 循环写入很多导出需求其实根本没有大到需要特殊优化一次 doWrite 就完了。但业务报表系统里经常会出现“全量导出”这种需求一导就是几十万行甚至上百万行。这时候如果你还是一次性把数据全部查出来塞进一个 List最大的瓶颈其实已经不在 EasyExcel 了而在于这条查询 SQL 本身。几十万条记录全部装载到内存里光业务对象的占用就非常可观。正确思路是数据库分页查询EasyExcel 分批写入。下面这段代码是当前端要做数据全量导出时的推荐写法import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.write.metadata.WriteSheet; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; public void exportLargeData(HttpServletResponse response) throws IOException { // 响应头设置省略和上一节一致 ExcelWriter writer null; try { writer EasyExcel.write(response.getOutputStream(), UserExportVO.class).build(); WriteSheet writeSheet EasyExcel.writerSheet(用户信息).build(); int pageSize 5000; int pageNum 1; while (true) { ListUserExportVO pageData userQueryService.queryPage(pageNum, pageSize); writer.write(pageData, writeSheet); pageNum; // 假设查询返回数量小于 pageSize说明已经到最后一批 if (pageData.size() pageSize) { break; } // 每批写完及时清除局部引用便于 GC pageData.clear(); } } finally { if (writer ! null) { writer.finish(); } } }这段代码用 ExcelWriter 对象手动控制写入生命周期。你可以循环调用 writer.write(pageData, writeSheet)把每一批数据追加到同一个 Sheet 里最后统一 finish输出流关闭文件生成。pageSize 选择经验值一般是 3000 到 10000 都行太大反而会让单次 SQL 查询和对象创建的开销变大太小网络和上下文切换开销明显5000 是一个比较均衡的值。千万要注意 writer.finish() 必须在 finally 里调用。如果你只调了 writer.write() 就返回输出流没有正确完成收尾生成出来的文件大概率是损坏的或者只有几 KB 且打不开。doWrite() 内部会自动 finish但如果你用的是手动 ExcelWriter 模式就必须自己负责 finish这是最容易遗漏的地方。另外分页查询本身也有坑。用普通 Limit 做深分页时页数越靠后性能越差因为数据库要跳过前边大量行。如果要导的数据接近百万更推荐用游标式查询或者基于主键 ID 做范围分页比如where id lastId order by id limit 5000每一批都以上一批最后一条 ID 作为起点。这种写法在导出大表时能明显减少数据库扫描压力。3.2 动态表头怎么做有些导出需求连表头都是动态的比如客户要求根据查询条件动态生成统计周期列上一周可能只有三列这一周变成五列。这种场景没法提前写死一个实体类需要完全跳过实体映射手动构造表头和行数据。EasyExcel 里最直接的方式是给 write 方法传一个 ListList 作为表头然后传一个 ListListListListString head new ArrayList(); head.add(Arrays.asList(业务员)); head.add(Arrays.asList(渠道)); head.add(Arrays.asList(销售额)); // 每一行的 Object 列表要和表头一一对应 ListListObject rows new ArrayList(); rows.add(Arrays.asList(张三, 线上, 12800.50)); rows.add(Arrays.asList(李四, 线下, 9200.00)); EasyExcel.write(outputStream) .head(head) .sheet(销售统计) .doWrite(rows);这种动态列的写法非常灵活但代价是你失去了实体类那种类型映射和格式注解。field 是 BigDecimal 还是 String完全要靠你在构造 List 时自己控制。数字在单元格里希望写成数值类型就放 BigDecimal希望保留前导零就放字符串这个细节容易踩。曾经有个同事把手机号都放成了数字类型结果导出后开头是 0 的手机号全部丢失后来才改成字符串字段。还有一种变体实体类负责大部分固定列动态列单独剥离。你可以先通过实体映射出前几列再把动态列追加到表头和数据行的剩余位置。实际操作时可以用 map 结构维护动态列名和对应值代码会比较绕但能兼顾类型映射的便利性和动态扩展的需求。3.3 模板填充让数据套进现成报表模板填充是 EasyExcel 一个特别实用的功能。很多时候业务方给了一个做好的 Excel 模板里面有固定的标题、表头、品牌名、签字栏甚至预设了公式你只需要往某个区域把数据填进去。用代码重新生成这个模板非常痛苦用模板填充可以直接基于模板文件输出最终文件。使用方式也不复杂把一个模板文件放到 classpath 下读取成流再用 withTemplate 指定import com.alibaba.excel.EasyExcel; import com.alibaba.excel.ExcelWriter; import com.alibaba.excel.write.metadata.WriteSheet; InputStream templateStream this.getClass().getResourceAsStream(/template/user_report.xlsx); ExcelWriter writer EasyExcel.write(outputStream) .withTemplate(templateStream) .build(); WriteSheet sheet EasyExcel.writerSheet().build(); // 数据对象里如果包含 List 字段用 FillWrapper 包裹以区分数据块 writer.fill(new FillWrapper(users, userList), sheet); writer.finish();模板里的写法用花括号加变量名占位比如表头下方要输出一批用户列表可以在模板的某个区域先写好占位字段然后 fill 方法会自动识别并展开成多行。注意模板里占位符的命名要和 FillWrapper 的 key 对应。单对象填充也可以直接用 writer.fill(map, sheet)Map 里的 key 对应模板占位名。这里容易忽略的问题是使用模板填充时模板本身的单元格样式、边框、行高会被保留这是模板填充最大的好处。但如果你在模板里写了公式占位比如合计行本来预埋了 SUM 公式填充数据后公式可能不会自动重新计算具体取决于模板预埋时的公式形式和 EasyExcel 版本的表现。稳妥做法是模板里预埋公式并且用 Excel 已经能正常打开验证过最终输出文件后再抽查打开确认公式正常。3.4 样式定制列宽、行高、表头背景色很多开发者第一次用 EasyExcel 时说生成的表头太“秃”想加背景色、字体加粗、调整列宽行高。最简单的办法是在实体类上加类级和字段级注解。import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.ColumnWidth; import com.alibaba.excel.annotation.HeadRowHeight; import com.alibaba.excel.annotation.ContentRowHeight; import lombok.Data; Data ColumnWidth(20) HeadRowHeight(30) ContentRowHeight(24) public class ProductExportVO { ExcelProperty(商品编码) private String sku; ColumnWidth(32) ExcelProperty(商品名称) private String productName; ExcelProperty(库存) private Integer stock; }ColumnWidth 加在类上是默认列宽加在字段上会覆盖默认值HeadRowHeight 和 ContentRowHeight 分别是表头行高和内容行高。这三组注解基本能覆盖日常大多数需求。如果要给表头加背景色、字体加粗类级注解也能做但颜色值写法在 3.x 里稍显晦涩。我更常用的方式是写一个 CellWriteHandler 实现类在里面针对表头单元格设置样式。代码看起来多一点但样式完全可控且不会因为版本升级导致注解里的颜色常量失效。有一个经验分享样式性能问题在大数据量导出时会被放大。比如你给每一行内容都自定义字体和边框百万行导出时样式对象偶尔会成为开销的隐形大头。尽量复用同一个样式对象别在每行回调里 new 出大量样式实例。普通报表导出的样式做在表头和关键列上就够了内容行保持默认样式完全能接受。3.5 多 Sheet 导出一个文件装下多类数据业务上经常遇到“一个报表文件里有好几个 Sheet”的需求比如客户明细一个 Sheet销售明细一个 Sheet。EasyExcel 对多 Sheet 的支持非常方便同样使用手动 ExcelWriterExcelWriter writer EasyExcel.write(outputStream).build(); WriteSheet customerSheet EasyExcel.writerSheet(0, 客户列表).head(CustomerVO.class).build(); WriteSheet orderSheet EasyExcel.writerSheet(1, 订单列表).head(OrderVO.class).build(); writer.write(customerList, customerSheet); writer.write(orderList, orderSheet); writer.finish();每个 Sheet 可以单独指定表头实体和数据源。writerSheet 的第一个参数是 Sheet 序号第二个是 Sheet 名称。如果只传一个名字EasyExcel 会按顺序自动排序比如第一个 sheet 名字叫“客户”下一个叫“订单”它也能正确区分但我习惯显式传序号避免未来调整代码时 Sheet 顺序乱了。很多人不知道的是ExcelWriter 支持一个 writer 连续写任意多个 Sheet而不只是两个。所以如果你有循环生成不同 Sheet 的需求比如月初要按部门各导出一个 Sheet只要在循环里依次 writer.write 就行性能也完全可控。4. 问题排查与避坑实录4.1 中文文件名乱码或下载不了这应该是导出需求里出现频率最高的问题之一。表现是文件能下载但文件名是一串百分号编码或者问号。前面已经提过解决办法就是用 URLEncoder 编码中文并且响应头写成 filename*UTF-8形式。另一种情况是部分浏览器对 RFC 5987 支持不完整尤其老版本的老式浏览器。如果公司内有用户反馈下载文件名不对可以在 Content-Disposition 里同时写普通 filename 和 filename*给老浏览器一个降级方案。示例Content-Disposition: attachment;filenameusers.xlsx;filename*UTF-8%E7%94%A8%E6%88%B7%E5%88%97%E8%A1%A8.xlsx这样新浏览器按 UTF-8 filename* 解析老浏览器也能拿到一个 ASCII 的 filename不至于完全无法下载。4.2 导出大文件时内存溢出OOM 几乎都是“调试数据一次性全查了 用 POI 构建 Workbook 没注意响应流”的组合拳。换用 EasyExcel 的流式导出后正常场景已经很难 OOM。如果你改成了分批写还是 OOM先看是不是做分页的 service 方法内部把全量数据 pull 出来了比如先查了总数再在同一个事务里加载全部 ID 列表之类。另外注意聚合查询和 List 装载时的对象大小。一百万条记录每条对象十几个字段对象头、引用、字符串内容加起来可能有几百兆。批量查询每批 5000 条写完后让这批对象不可达等下批查询占用另一批内存才是正常状态。有人会在循环外声明一个大 List 反复 add 每批数据然后从那个大 List 来构造 Excel那样内存是不可能降下来的。4.3 日期格式不对、时区偏移问题EasyExcel 里日期格式化常用 DateTimeFormat。但有些项目全局设置了 Jackson 的时区而数据库连接 url 也带 serverTimezone 参数导致从数据库取出的时间本身就是偏移后的值。这种问题不是 EasyExcel 造成的导出的数据源头已经错了再怎么格式化都是错的。排查思路是先确认查询到的 Java Date 值是不是期望值再确认实体字段是不是 Date 类型最后看 DateTimeFormat 的 pattern 是否正确。之前遇到过一位同学把 pattern 写成 yyyy-MM-dd HH:mm实际数据需要精确到秒导出的列全部缺秒用户反馈后又重新导这种低级错误其实花一分钟自测就能发现。还有 LocalDateTime 类型的字段3.x 对 LocalDateTime 支持也不错但旧版本可能需要额外注册 LocalDateTimeStringConverter如果你遇到 LocalDateTime 导出来是一串数字先查一下版本必要时手动转换 String。4.4 并发导出与线程安全EasyExcel 的底层读写器实例不是设计成跨线程共享复用的。我在实际运维中看到有的同事把 ExcelWriter 保存在类的静态字段里想着一次初始化多次复用结果两个用户同时点击导出文件直接串数据或者报 Already finished 异常。正确姿势非常简单每次请求都 new 一个 writer。Servlet 环境下 Controller 本身是单例但导出代码里通过局部变量创建 writer用完在 finally 里 finish天然就是线程安全的。实体类也要注意不要有静态可变集合、静态 SimpleDateFormat 之类的东西这些在并发导出时都是隐患。4.5 导出文件打不开或内容空白假如你下载下来的 .xlsx 双击提示文件损坏先不要怀疑 EasyExcel bug优先检查你的代码是否在 doWrite 之前就关了输出流或者把它包进了 try-with-resources 后提前 flush 了。我自己踩过一次这样的坑在 finally 块里顺手调用 response.getOutputStream().close()导致 EasyExcel 后续写入时流已是关闭状态生成结果就是一个 0 字节或者残损文件。文件中有一个 Sheet 但全是空白多半是导入到导出的 VO 映射出了问题比如源数据 List 是空的或者实体类字段和查询结果不一致。终极排查办法是把导出的 List 打印几行到日志或者先用内存列表测一下确认是数据问题还是框架问题。4.6 导出接口在网关/登录拦截器下失效还有一类问题表面上是 EasyExcel 相关实际是拦截器把响应劫持了。有些权限框架在请求处理后强制对 response 做 JSON 包装或者往输出流里追加了内容导致 Excel 文件最后多出额外字节Excel 打开时提示文件损坏。这类问题定位起来比较隐蔽你可以先用 Postman 直接请求导出接口把返回的文件保存下来看是否正常Postman 正常但浏览器或前端异常大概率是前端或网关的问题。导出接口如果被统一日志拦截器记录响应体也可能因为试图读取输出流内容而破坏二进制数据。解决办法是把导出接口加入路径白名单跳过响应包装和响应体日志。下面把高频问题整理成速查表方便你以后直接对表排查现象常见原因解决方向文件名乱码Content-Disposition 没做 URL 编码使用 URLEncoder filename*UTF-8文件打不开输出流提前关闭或缺 finish()不要手动关流finally 里 writer.finish()导出全是空查询数据为空或 VO 字段映射不对打印 List 日志检查字段名中文内容乱码响应 CharacterEncoding 没设置设置 response CharacterEncoding UTF-8内存 OOM全量 List POI 构建分页查询 EasyExcel 分批写日期缺少时分秒DateTimeFormat pattern 不对检查 pattern 与字段类型列宽太窄/太宽未使用 ColumnWidth 注解类级默认字段级覆盖并发导出串文件静态复用了 writer每次请求新建 writer数字变成文本字段类型用了 String保留 BigDecimal/数字类型Sheet 顺序错乱writerSheet 未显式传 index传 0、1、2…索引4.7 一个我很少在文档里看到的习惯最后分享一个实践心得。导出代码里我习惯把“查询数据”和“生成 Excel”这两段强制分离。Controller 只负责拿到数据源然后交给一个独立的 ExportService 去写 Excel。如果哪天要换底层导出工具或者给同样的数据换一种报表模板都不需要改动查询逻辑。这个习惯在项目迭代比较快的时候非常有用。另外一个我常用的优化是把每次导出操作记一个简单的日志谁导的、导出多少行、耗了多少秒。很多业务问题比如“系统慢”“文件数据不对”到最后都要靠这份日志反查。导出接口慢的时候通过日志能直接看出来到底是 SQL 慢还是生成文件慢省掉大量排查时间。从初期的 POI 到现在的 EasyExcel导出功能在 Java 后端已经变得非常“日常化”。入门成本低能跑通一个导出接口只需要十几分钟想做得稳、扛得住数据量再往深走一步就是分批写、模板填充和多 Sheet 组合。希望这篇里分享的代码和避坑经验能派上用场少踩几个我已经踩过的坑。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑