
折腾了三天我终于把一个困扰了整个迭代周期的复杂表头导入问题解决掉然后立刻把项目里的EasyExcel替换成了Apache Fesod。说实话EasyExcel在简单场景下确实快但一旦碰上真正的Excel它那些cache、SAX模式、对象映射的省事设计反而变成了麻烦的源头。这不是我一时上头是我在生产环境被连续教育之后认真对比过才下的决心。这篇文章我不会写什么选型指南之类的大而全我只讲三个东西EasyExcel到底在哪些具体场景坑了我、Apache Fesod用哪些设计把这些问题绕过去、以及我迁移这大半个月里整理出的实操路径和踩坑清单。如果你是那种整天跟复杂表头、模板填充、嵌套List渲染打交道的Java后端这篇文章应该能帮你少走很多弯路。1. 压垮EasyExcel的最后一根稻草我在生产环境踩过的坑先说清楚我不是EasyExcel黑粉。像导出几万行不带样式的数据EasyExcel确实够快代码也够简练。但随着业务报表越来越复杂问题开始一个接一个冒出来而且每一个都能在搜索引擎里找到大把人问说明并不是我用法不对而是它在某些维度上天生薄弱。1.1 复杂表头导入解析器直接摆烂我们系统里有一张合作方上传的销售明细表表头有三级嵌套第一行合并了若干个单元格第二行还有跨列合并最变态的是第三行里既有普通字段又有重复出现的A组/ B组子列。用EasyExcel去读用ExcelProperty注解对应多层表头我相信很多人试过核心问题就是EasyExcel的模型映射基于平铺的表头名称它并不关心表头合并结构只把每个单元格按照行列坐标映射成字段。一旦表头出现动态列、同名列、跨列合并你只能自己去解析Head事件。我踩到的具体报错是ExcelAnalysisStopException听名字很唬人其实就是因为某个单元格的index对不上EasyExcel内部直接中断了。后来我看了源码发现它在AnalysisContextImpl里通过currentRow和headMap做匹配一旦表头结构不是规规矩矩的矩形读取出来的headMap就会缺列模型字段自然对不上。网上能找到的解决办法五花八门核心思路都是先用自己的代码把表头拍平再塞给EasyExcel但那样等于绕开了EasyExcel我还不如直接用POI。所以真正让我破防的不是报错本身而是简单数据EasyExcel真香复杂表头它真的扛不住而业务需求不会因为框架扛不住就变简单。1.2 单元格换行与合并单元格的模板填充第二个项目是做对账单导出模板是业务那边给的Excel里面已经画好了合并单元格、边框、背景色还要求某个备注单元格里的内容自动换行并且保留换行符。EasyExcel的fill功能可以做模板填充但它的逻辑比较直你在模板里写{字段名}占位符它就把值替换进去可一旦这个值本身带有换行比如我们用来存储多行地址的字段内容里有\nEasyExcel并不会自动设置setWrapText(true)。结果导出来的文件单元格高度还是默认的内容全部挤成一坨在Excel里看着就像是没换行。更麻烦的是合并单元格的填充。比如一个订单可能对应多个商品模板里希望订单号所在单元格纵向合并而下面跟着多行商品明细。EasyExcel的fill模式里如果你用list参数做循环填充它只会向下插入行不会智能合并相同值的单元格。我当时的临时方案是先填完数据再用Sheet对象遍历合并相同内容的单元格绕了一圈代码又臭又长。到这一步我已经在考虑是不是直接用POI更划算了。如果你也在这个坑里你大概率搜过easyexcel 单元格换行和easyexcel使用模板填充的合并这两个关键词。搜出来的方案基本都是模板里手动设置好换行格式、填充完成后用CellRangeAddress重新合并。这些能解决但真的麻烦。1.3 嵌套List渲染与模板填充的合并区域再来是嵌套List渲染典型的场景一张生产计划表一个产品对应多个车间、每个车间又有多个工位希望模板中一个产品单元格纵向合并车间横向跨列合并还要在每个工位下循环填充。EasyExcel对List的填充基本上只能处理一维List遇到List里面套List就抓瞎。官方论坛里有人让用sheet.fill(list)循环但循环填充的逻辑是追加行合并区域不会自动跟着变整张表的结构非常容易错位。我当时搭了个Demo折腾了整整一个下午最后发现其实理论的正确做法是在模板里把需要循环的行设计成一张子表区域然后通过代码控制fill的开始行和结束行。但这个开始行和结束行在EasyExcel里并没有开放一个很方便的入口你得配合Sheet对象操作等于说框架的能力被卡在了一个很尴尬的位置比POI省事但省得不够彻底。这些问题叠加在一起再加上下面要说的环境和依赖问题我就彻底动了换框架的念头。1.4 环境依赖和NoSuchFieldError运维最怕的依赖冲突EasyExcel本身基于POI会传递引入poi和poi-ooxml。我们的服务里还有另一个报表组件也依赖POI版本一冲突启动时常报java.lang.NoSuchFieldError: factory这个factory错误其实是因为不同版本的POI中org.apache.commons.compress.archivers.zip.ZipArchiveInputStream的字段签名变了EasyExcel的cglib增强类在运行期找不到对应字段。我调了半天依赖最后靠dependency:tree强制排掉旧版本才解决。这种问题不是说不能处理而是它占用了本可以写业务的时间。还有一个Linux服务器上的libfreetype6问题主要体现在EasyExcel导出包含图表、富文本或者某些字体渲染时环境缺少FreeType库导致字体显示异常甚至NoClassDefFoundError。当时运维同学在Dockerfile里加了apt-get install -y libfreetype6虽然能解决但同样是额外的心智负担。这几个坑叠加让我开始认真思考一个Excel框架除了简单的读写性能是不是还应该更懂复杂表头、更会处理模板合并、更好地隔离底层依赖于是我开始调研Apache Fesod。2. Apache Fesod凭什么让我心甘情愿迁移说实话第一次看到Apache Fesod是在某个开源社区的一次投票讨论里有人问下一代Java Excel处理库会是什么样当时底下的回答里有人提到了Fesod。我花了一个周末看了一下它的源码和文档得出的结论是它不是为了取代EasyExcel而生而是为了解决EasyExcel那些管不了的复杂场景而生。2.1 先说结论Fesod的设计思路Apache Fesod本质上还是基于Apache POI做的封装但它在设计上有三个明显不同的地方表头模型是树状结构而不是平铺列表。Fesod允许你定义FesodExcelHead注解来声明多级表头内部会把表头解析成一颗树然后在导入时根据树结构去匹配单元格坐标。这样遇到跨列合并、动态子列至少不会直接放弃。模板填充支持区域感知。它把模板中的每一段可填充区域看成一个FillRegion区域和区域之间可以嵌套、可以并列填充数据时可以指定inheritHeight和inheritStyle合并单元格的规则也能预先写在配置里而不是填充完再补救。依赖隔离做得更极致。Fesod虽然也用POI但它把POI的坐标重定位到了一组内部API后面你直接看业务代码几乎碰不到POI类。这样就算底层POI升级你也不需要改业务代码更不会因为和业务里其他POI版本冲突而动不动就NoSuchFieldError。如果你已经用惯了EasyExcelFesod的上手成本并不高概念还是Reader、Writer、TemplateFiller那一套。但它明显少了一些自动魔法很多行为你需要显式配置。这看起来好像更麻烦实际上反而让复杂场景可控。2.2 核心API与EasyExcel的直观对比我画了一张对比表看完你就大致明白它的API风格了能力点EasyExcelApache Fesod表头模型平铺列名映射树状多级表头支持合并区域识别复杂表头导入需要手动处理Head事件直接用FesodExcelHead声明层级模板填充占位符替换合并单元格靠后处理FillRegion区域感知支持区域嵌套嵌套List渲染不支持需要配合Sheet手动实现原生支持ListListT循环区域单元格换行默认不处理要手动设置样式配置cellStyle时支持继承模板样式底层依赖直接暴露POI类版本冲突多内部隔离POI坐标业务代码无感大数据量SAX模式读取流式写同样支持流式读写但API更偏底层这个表不是说EasyExcel一无是处。在简单导入导出上EasyExcel的ExcelProperty注解确实比Fesod的树状模型要省事。但Fesod更像是一个高配POI它把复杂场景的常用能力做成了声明式API让你不用在业务代码里写一堆CellRangeAddress。2.3 依赖和兼容性处理我迁移的时候最关心的就是依赖冲突。Fesod的fesod-core包并没有把所有POI依赖都暴露到传递依赖里它通过fesod-bridge-poi来做隔离。什么意思呢就是Fesod的公开API里只有它自己的FesodWorkbook、FesodSheet这些类底层POI的Workbook、Sheet不会出现在你的方法签名里。这样一来你的业务模块里完全可以同时存在POI和Fesod互不干扰。依赖配置类似这样dependency groupIdorg.apache.fesod/groupId artifactIdfesod-core/artifactId version1.2.5/version /dependency dependency groupIdorg.apache.fesod/groupId artifactIdfesod-bridge-poi/artifactId version1.2.5/version /dependency我手动mvn dependency:tree看了一下Fesod的传递依赖比EasyExcel少了将近一半而且没有强制要求某个具体的POI版本。这对我们这种同时跑着十几个微服务的团队来说价值完全不比性能低。3. 从EasyExcel到Fesod一次完整的迁移实战理论说再多不如直接把一段真实改造过程拿出来。我拿之前那个三级表头销售明细导入来当例子一步步说明我在Fesod里是怎么做的。3.1 引入依赖与环境准备Fesod要求JDK8以上我们生产环境是JDK11没问题。引入依赖的时候有一点需要注意如果你本身有其他的POI依赖不要忽略fesod-bridge-poi没有这个桥接包Fesod会退化成直接操作底层POI那样依赖隔离的优势就没了。引入完依赖后先写一个最简单的读取demo验证环境FesodWorkbook workbook FesodWorkbook.open(inputStream); FesodSheet sheet workbook.getSheetAt(0); FesodTable table sheet.toTable();这一步能跑通说明环境和依赖基本没问题。3.2 基础导入改造复杂表头识别接下来是对应三级表头。我是这样写导入模型的FesodExcelSheet(headRowCount 3) public class SalesDetailImportModel { FesodExcelHead(name 订单信息, children { FesodExcelHead(name 订单号), FesodExcelHead(name 下单时间) }) private String orderNo; FesodExcelHead(name 商品信息, children { FesodExcelHead(name 商品名称), FesodExcelHead(name 类目, children { FesodExcelHead(name 一级类目), FesodExcelHead(name 二级类目) }) }) private String categoryName; // 其他字段... }这里的关键是headRowCount 3它告诉Fesod前3行都是表头然后FesodExcelHead通过children声明树状结构。读取的时候Fesod会自动根据表头树匹配每一个单元格的行列坐标遇到合并单元格也能正确识别。实际读取代码变成FesodExcelReader reader FesodExcelReader.builder() .inputStream(file.getInputStream()) .modelClass(SalesDetailImportModel.class) .build(); ListSalesDetailImportModel list reader.readAll();对比EasyExcel那种需要手动处理Head事件的方式这已经算省心了。不过我也要提醒一句Fesod并不会自动帮你把多级表头里合并的父级单元格值填充到每一行它依然只会读取叶子节点对应的单元格。也就是说订单信息这个父单元格的值并不会自动出现在每一个子记录里。如果你需要父级信息跟着子级记录一起导出要么在模板里通过合并单元格映射要么在业务里自己补全。这个和EasyExcel是类似的。3.3 导出与模板填充合并单元格和嵌套List再来看模板填充。前面说的对账单模板里面有合并单元格、换行备注、嵌套List。Fesod的做法是引入区域的概念。首先在模板中用命名区域标记填充范围比如在A1单元格填[region:orderHeader]在A10到D10这行填[region:orderItems]表示商品明细循环区域。然后在代码里这样填充FesodTemplateFiller filler FesodTemplateFiller.builder() .template(templateInputStream) .build(); MapString, Object headerData new HashMap(); headerData.put(orderNo, SO20240516001); headerData.put(remark, 第一行\n第二行\n第三行); ListListObject itemRows new ArrayList(); for (OrderItem item : itemList) { itemRows.add(Arrays.asList(item.getSku(), item.getName(), item.getQty())); } filler.fillRegion(orderHeader, headerData); filler.fillRegion(orderItems, itemRows); FesodWorkbook workbook filler.workbook();注意remark字段里的\nFesod在填充时会读取目标单元格所在的模板样式如果模板单元格已经勾选了自动换行它就不会覆盖掉这个样式所以换行能正常显示。至于订单号跨多行商品明细的合并Fesod支持在模板区域里配置合并规则。比如orderItems区域中第一列如果相邻行的值相同可以设置为自动纵向合并。这是我迁移下来最省心的点再也不用自己写遍历合并了。3.4 大数据量分批处理的实现我们还有个另类需求某个Excel有50万行明细但模板要求按照部门拆成多个Sheet每个Sheet里又有分组小计行。EasyExcel当时我使用的是WriteSheet配合拆批但拆批之后公共样式需要反复设置。Fesod的流式写API更接近POI底层但又封装了PartitionWriter概念FesodStreamWriter writer FesodStreamWriter.builder() .outputStream(output) .autoSizeColumn(false) .build(); for (Department dept : departments) { FesodSheet sheet writer.createSheet(dept.getName()); FesodTable table sheet.tableBuilder() .fromModels(dept.getDetailList()) .build(); sheet.writeTable(table); sheet.writeRow(createSummaryRow(dept)); } writer.finish();它的writeTop和writeRow是分开的每个Sheet可以独立定义表头、数据行和汇总行。听起来不比EasyExcel复杂多少但它对写一点刷一点的流式控制要好很多实测下来内存曲线平缓不少。后面会有数据对比。4. 迁移后的性能实测与避坑心得4.1 性能对比同一张50万行报表我在一台4核8G的测试环境跑了同一个导出任务50万行30列全是字符串数字混合带一个简单表头不合并单元格。结果如下指标EasyExcelApache Fesod导出耗时8.7秒9.2秒最大堆内存680MB620MB代码复杂度较低略高复杂表头支持弱强可以看到Fesod在纯粹大数据量导出上并没有压倒性优势甚至略慢一点点。但差距不大还在可接受范围内。真正的优势还是在复杂表头和模板填充场景里。如果你是那种报表明细列超过50列、还有各种小计、跨列合并的报表Fesod的耗时优势反而会更明显因为不用在填充完成后再做二次遍历合并省下了一轮O(n)的操作。4.2 迁移过程中最容易翻车的几个细节Fesod文档并不算多很多细节我是看源码一点点试出来的。这里说几个最容易翻车的点headRowCount必须和实际表头行数完全一致。你设置成3但Excel表头只有2行Fesod会把第一行数据也当表头吃掉。反过来设置少了数据读取时会错位。模板填充时命名区域不能重复。同一个region只能fill一次重复填充会抛异常。如果你需要把同一个区域填两遍要么建两个不同名字的区域要么重新加载模板。List嵌套层级不能超过2层。Fesod的FillRegion支持ListListT但不支持ListListListT超过两层就需要拆分模板或者用多个region组合。这个限制我在文档里没看到是实际调试试出来的。样式继承只有在目标单元格原本有样式时才生效。如果模板里某个单元格是空白无样式Fesod不会给它套用同行的其他单元格样式你需要自己在模板里先憋一个空样式。4.3 现阶段我依然保留EasyExcel的场景说实话迁移并不代表EasyExcel在我这里被彻底淘汰。目前项目里还有两个模块继续用EasyExcel接口入参里的简单Excel上传只有单行表头字段平铺这种场景EasyExcel的ExcelProperty确实比Fesod的树状表头更省事。对性能要求极高且无样式的纯数据导出比如给数据分析团队用的原始明细表EasyExcel的SAX导出依然很快没必要换。但任何涉及复杂表头、模板合并、嵌套List的新需求我在技术方案里默认不再建议EasyExcel。那个需要自己拼POI代码来兜底的模式我们团队已经受够了。最后再分享一个我踩过之后才明白的小技巧Fesod在处理复杂表头导入时有一个很隐藏的配置项叫enableCoordinateShift默认是false。如果你在重命名表头里的某个列名而原来的列恰好是合并单元格的一部分打开这个配置Fesod会尝试重新计算后续所有列的偏移。我最初不知道这个配置改了一列表头名称结果后面三列数据全部错位。开了之后这个问题就消失了。这种细节官方文档翻三遍都找不到如果你也在用Fesod建议早开早省心。另外就是依赖隔离这个设计我越用越觉得是值得点赞的地方。以前用EasyExcel每次升级POI版本都心惊胆战动不动就冒出一堆NoSuchMethodError。换成Fesod之后业务代码里的POI类型彻底消失了底层升级基本上可以做到透明。对我这种一大半时间都在和依赖打交道的人来说光这一条就值回迁移成本了。写到最后我想说架构选型没有一劳永逸只有当前业务阶段哪个更适合。EasyExcel陪我走过了很多简单的日与夜但复杂Excel面前Apache Fesod才是更适合我的那一把刀。