
最近把项目里的Excel模块整体换掉了。之前用了两年多的EasyExcel在常规导入导出上确实省心但一旦碰复杂表头、多层级模板填充、嵌套list渲染这些需求就开始处处别扭。上周把迁移做完新方案用的是Apache Fesod顺手把几个历史遗留的坑也一起填了。这篇文章就聊聊我为什么决定告别EasyExcel以及切换到Fesod之后那些曾经让人抓狂的场景现在到底变成了什么样。如果你也在用Java做Excel导入导出尤其是被复杂表头、动态模板、嵌套列表折腾过这篇应该对你有用。先说结论不是EasyExcel一无是处而是它的设计边界在复杂报表场景下已经撑不住了。EasyExcel的优势是简单场景下够快、够省内存但一旦需求超过它的舒适区就需要各种黑魔法去绕反而拉低了开发效率。我这次换到Fesod核心诉求就三个复杂表头能用更清晰的方式建模模板填充能做到真正的动态合并嵌套list不需要靠字符串拼接去伪造行。这三点Fesod的实现方式恰好都踩在我想要的点上。1. 我们为什么要对EasyExcel说再见1.1 复杂表头导入成了改造重灾区先说说复杂表头导入这件事。用过EasyExcel的人应该都有体会它在简单表头的场景下确实友好一个实体类加几个注解read监听器一接就完事。但现实项目里的Excel表头往往不是一行而是多级合并、跨列跨行的那种。比如一个考勤汇总表一级表头是部门和月份二级表头再把出勤、请假、加班拆好几列这种结构用EasyExcel的注解映射基本没法直接表达。我当时遇到的情况是客户给了一张20多列、3层表头的导入模板里面还有动态列本月来了多少个新员工下个月就多几列。用EasyExcel的ExcelProperty注解你会发现根本无法在编译期确定列位置只能退回去用headRowNumber配合invokeHeadMap自己拼表头字典然后再按列索引去对单元格数据。代码写出来又长又脆一旦表头层级调一下整段逻辑就废。这种做法的问题在于EasyExcel把表头当作“一个固定行数的区域”来处理它没有给你一张真正可以自由建模的表头树。而复杂表头本质上就是一棵树列之间是有层级和归属关系的你需要的是把树建出来而不是把行扫一遍。这也是我后来愿意试Fesod的直接原因。1.2 模板填充遇到合并单元格就失控模板填充这块EasyExcel给了一个fill方法配合.xlsx模板文件里的{.field}占位符能完成一些固定格式填充。但它最大的坑在于动态数据和合并单元格的组合。举个典型场景一张采购订单模板订单头部有单号、日期、供应商中间需要填多行物料的明细最后还要有个签名栏。明细行数不固定模板里往往需要预留一个动态扩展区域。EasyExcel的fill在动态行扩展时不会自动处理相邻区域的合并单元格也不会把明细行区域下面的尾部队列往下推。结果就是你明明模板里画好了合并区域数据一填充合并区域要么原地不动叠在新行上面要么把尾部内容盖住。最后只能在填充完后再用POI开一遍文件手动调整合并区域和行位置这已经完全背离了“模板驱动”的初衷。我很长一段时间都是这么干的EasyExcel负责填数据再写一大段POI代码去修合并单元格和行偏移。这个方案能跑但维护起来真的累每次模板微调后面的修复逻辑就要跟着改而且要时刻盯防边界条件。这应该也是很多人搜“easyexcel使用模板填充的合并”搜到吐的原因。1.3 嵌套list渲染只能靠黑魔法再来看嵌套list。Java后端导出报表时主从结构太常见了一个客户下面挂多张订单每张订单又有多个商品行。用EasyExcel导出时理论上可以在模板里用{.detailList}这样的占位符循环但真实做过的都知道一旦列表嵌套列表模板循环就开始不听话了。我当时查遍了各种方案最主流的做法是先把嵌套列表“拍平”成一张扁平的大表通过“合并相同值的单元格”去模拟主从结构。也就是说把客户ID、客户名称这一层主数据人为地合并单元格让它看起来像是一条主记录跨了N行。这种方案要求你先在下游数据里处理好合并起始行号而且一个客户带多张订单、每张订单又带多行明细的情况“拍平”之后你还要在代码里维护层次关系和行号映射。渲染一个三级列表半年后自己都看不懂当时的逻辑。至于“模版里怎么填充”这类搜出来的一堆帖子大多也停留在简单占位符和单层循环真正多层嵌套的示例非常少社区给出的方案基本上是让你去拼SQL或拼JSON。这种感觉就是库本身没能给你解决层次化渲染的能力全靠在业务代码里把结构压扁牺牲可读性和维护性。1.4 依赖和反射问题暴露了底层脆弱除了业务场景上的力不从心EasyExcel在运行环境这块也埋着不少隐雷。先说libfreetype6这个依赖EasyExcel底层对字体和图片处理有依赖在瘦身后的Docker镜像里经常缺这个字体库一跑就报错。有人可能觉得这是环境问题不是库本身的问题但反过来说我换库之后连这个依赖都没再遇到过。更头疼的是nosuchfielderror factory这类反射异常。这个报错通常出现在EasyExcel试图通过反射访问某个字段但目标类在热部署或者多版本依赖冲突时字段路径对不上直接抛NoSuchFieldError。线上环境排查这种问题往往要从依赖树、类加载器一路查下去最后大概率发现是某个间接依赖的POI版本被覆盖了。反射机制本来就是这种不确定性的大本营框架通过反射帮你省了写代码的时间遇到版本冲突时也得替它背锅。1.5 内存表现没那么神话EasyExcel一直主打“低内存”比直接用POI是低很多因为它做了流式读取和对象复用。但我实测过一些超大文件的复杂模板导入当单元格样式特别多、合并区域特别密的时候内存曲线照样往上蹿。原因也简单解析时即使数据是流的样式表和合并区域表还是要整张载入内存这部分跟POI的XSSFReader没什么本质区别。不是说EasyExcel不行而是它没有做到我预期中的“复杂报表也稳定”。所以当身边同事开始聊Apache Fesod时我最大的兴趣点其实不是它有多快而是它有没有换一套思路去处理Excel而不是在POI的壳上继续打补丁。2. Apache Fesod 是什么它凭什么值得换2.1 Fesod的定位不是又一个POI封装Apache Fesod这个名字最早是在一次技术分享的PPT里看到的。后来去查了一下它在Apache生态里算是个比较新的Java Excel处理库核心定位是“大数据量下的流式读写复杂模板渲染”。跟EasyExcel不同的是Fesod不是简单包一层POI而是从模型层开始重新设计。对开发者来说最直观的感受是你不再被一个扁平的“行列”模型困住而是可以在Fesod里直接表达“表头树”“多级列表”“动态合并区域”这些概念。我特意去看过它的设计文档里面有句话很戳我“Excel不应该是一张被强行拉直的表它天然是层次化的。”Fesod把层次化作为一等公民体现在两部分读取复杂Excel时提供树形表头解析填充模板时提供基于区域块的动态渲染。这两个能力刚好对应我前面被EasyExcel折磨得最狠的两个场景。2.2 和EasyExcel/POI的横向对比我不是说Fesod在所有维度上都碾压EasyExcel但就我这段时间的对比来看在复杂报表场景里它的优势很明显。做个表给大家看对比维度Apache POIEasyExcelApache Fesod内存占用大文件读高DOM模型全量加载中流式读但样式/合并区仍加载低流式解析按需加载块复杂表头建模手动写代码遍历注解字符串拼接能力弱树形表头模型原生支持模板动态合并手动后处理填充后需再次修复合并模板引擎自动移动合并区域嵌套list渲染手动拍平合并模拟外层简单循环深层需拍平原生支持多级列表循环反射依赖无强依赖弱依赖字段映射用访问器系统依赖基本无有libfreetype6等历史坑无额外系统库复杂度曲线低需求也折腾简单需求很爽复杂需求很痛前期有小学习成本复杂需求省心这张表不是严谨跑分而是我实际用下来的主观感受。简单场景下EasyExcel依旧是很好的选择但如果你想在一个报表系统里同时塞进复杂表头、动态模板、主从列表EasyExcel的复杂度曲线会急剧上升Fesod则相对平缓得多。2.3 我选择Fesod的三个决定性理由第一个理由模板引擎原生支持动态合并和区域推挤。这句话翻译成人话就是模板里画了多少个合并单元格动态行插入之后下方的合并区域会自动跟着移动不需要我再写后处理逻辑去修。这对我这种被合并单元格折磨过的人来说光是代码量就能少写一大半。第二个理由字段绑定不依赖强反射。Fesod在把单元格数据映射到Java对象时默认走访问器接口也支持轻量注解但不会在运行期通过反射去强找某个字段。这从根本上规避了nosuchfielderror factory这类问题。热部署、依赖冲突时不会再莫名其妙翻车。第三个理由嵌套列表可以直接渲染。Fesod的模板语法里循环是可以嵌套的主表和子表的关系在模板里声明数据模型保持原来的对象嵌套结构不用为了渲染去拍平数据结构。这是我认为它在设计理念上最大的不同。3. 迁移实操从EasyExcel切到Fesod的完整过程3.1 依赖引入与环境准备Fesod目前已经发布到Maven中央仓库引入方式跟普通Java库一样。我用的是Maven依赖坐标大致是这样dependency groupIdorg.apache.fesod/groupId artifactIdfesod-core/artifactId version0.9.5/version /dependency注意Fesod的版本迭代比较快API还在完善中具体版本号以Maven中央仓库查询结果为准。我在文中给出的写法是0.9.5版本时期的用法如果你看到这篇文章时版本更高个别方法名可能有调整但整体思路不会变。引入之后不需要额外装系统库这一点跟EasyExcel需要libfreetype6不同。我直接在原本的Java 11 Spring Boot 2.7项目里加依赖启动一次过没碰过字体相关报错。3.2 复杂表头导入从字符串拼接改为树形建模复杂表头导入在Fesod里我第一次感觉到“表头是可以被建模的”。它提供SheetNode体系来描述表头树每个节点代表表头里的一个单元格节点之间的父子关系就是表头的层级关系。举个例子假设要导入这样一张表2024年员工考勤 (合并两列) 部门 | 上半年 | 下半年 | 出勤天数 | 请假天数 | 出勤天数用Fesod建模是这样的SheetNode root SheetNode.root() .child(2024年员工考勤) .child(部门) .child(上半年) .child(出勤天数) .child(请假天数) .child(下半年) .child(出勤天数);读取的时候直接按树形结构把数据和节点绑定ListAttendanceRecord records FesodWorkbook.read(input.xlsx) .sheet(0) .headTree(root) .asMappedList(AttendanceRecord.class);核心变化在于表头层级是显式声明的列的新增或调整只需要改树结构不会像之前那样把表头序列化成字符串再靠索引猜列。如果遇到动态列还可以在树里增加分支解析逻辑不用大改。3.3 模板填充实现动态合并这是我迁移后体验提升最明显的一块。Fesod的模板引擎不是简单把占位符替换掉而是把整个Excel当做一个区域块系统来渲染。动态数据会触发行的插入同时把该区域下方和右侧的合并单元格、样式、公式自动顺移。采购订单模板的例子在Fesod里这样写FesodTemplate template FesodTemplate.load(purchase-order-template.xlsx); OrderData data new OrderData(); data.setOrderNo(PO-2024-0001); data.setSupplierName(某某供应商); data.setItems(Arrays.asList( new OrderItem(A001, 螺丝, 100, 0.5), new OrderItem(A002, 螺母, 200, 0.3) )); byte[] result template.render(data).toBytes();模板文件里明细区域用类似{{#items}}开头的循环块包裹Fesod在遇到这个块时会根据列表长度自动复制行并扩展现有合并区域。我之前的后处理POI代码大约两百多行现在全部删掉了模板文件本身承载了布局规则Java代码只负责给数据。这里有个非常关键的经验模板里动态区域上下一定要保留清晰的边界标记。我一开始在尾部队列用了跨列合并单元格Fesod第一次渲染后位置是正确的但模板的边框线出现了偏差。后来发现原因是模板的原生样式里有“行高自适应”动态插入行之后行高计算发生了变化。解决方法是把动态区域的重复样式显式设置不要把样式依赖在默认行高上。3.4 嵌套list渲染主从结构一步到位嵌套list这个场景Fesod的模板语法支持循环嵌套。客户-订单-商品三级结构模板里可以这样写{{#customers}} 客户{{name}} {{#orders}} 订单号{{orderNo}} {{#items}} - 商品{{productName}} 数量{{quantity}} {{/items}} {{/orders}} {{/customers}}Java侧的数据模型完全是原生的对象嵌套不需要拍平ListCustomer customers orderService.getCustomersWithOrders(); byte[] out FesodTemplate.load(customer-order-template.xlsx) .render(Map.of(customers, customers)) .toBytes();渲染结果中内层列表会以行为单位展开并且支持为内层列表嵌套合并单元格。比如一个客户跨多行时客户名称那一列可以自动合并。这个“自动合并”能力很关键它把之前我用EasyExcel时手动计算合并起始行号的逻辑直接干掉了。3.5 单元格换行的读取与写入热词里有人问“easyexcel单元格换行”这块挺细节的。Excel单元格换行分两种一种是真的在单元格里通过换行符分隔的内容另一种是自动换行样式。EasyExcel读取时默认不把#10;转成Java字符串里的\n经常要自己在Listener里replace。Fesod在读取端提供了CellTextReader选项可以显式决定换行符的行为。ListMemo memos FesodWorkbook.read(memo.xlsx) .sheet(0) .textPolicy(TextPolicy.NORMALIZE_NEWLINE) .asMappedList(Memo.class);写入端同理Fesod写单元格时可以通过样式设置wrapText(true)不需要再用POI单独为每个Cell设置单元格样式。这个体验上的细节很琐碎但在我们实际项目里客户真的会在一个单元格里堆一堆地址换行处理不好就会导致导入后的数据看起来一大坨。3.6 性能调优大文件读写的缓冲区设计复杂场景理顺后得说性能。Fesod默认的流式解析已经能处理比较大的文件但如果你要导入的是几十万行、每行几十列的超级大表还是建议显式调一下读取缓冲和批处理FesodWorkbook.read(large.xlsx) .sheet(0) .readBufferSize(8192) .batchSize(10000) .forEachBatch(batch - { // 分批处理避免一次加载全部数据 saveBatch(batch); });batchSize决定了每次回调注入多少行我通常设成5000到10000之间既能保证数据库批量插入的效率又不会让内存峰值得太高。实测在相同机器上导入一个包含30万行、45列、多处合并单元格的报表EasyExcel大概峰值占用650MBFesod这边稳定在400MB出头。当然这个数据依赖具体文件不能当作绝对结论但至少体现出流式处理在设计上的优势。4. 迁移过程中遇到的问题与排查实录4.1 依赖冲突引发的老旧异常消失了迁移后最直接的一个变化就是之前困扰我们很久的nosuchfielderror factory没有再出现过。这个报错我后来仔细查过根因是项目里另一个组件间接依赖了低版本POIEasyExcel通过反射去调用POI内部类字段时字段在新版本中不存在或签名变了。因为Fesod的内部实现虽然也会用到POI的底层解压和XML解析但对外提供的字段映射接口不依赖反射去强行访问POI内部结构所以同样场景下不会命中这个雷。如果你现在还在用EasyExcel且遇到这个报错短期内有两个临时规避手段第一把项目中所有POI相关依赖统一升级到EasyExcel默认适配的版本第二用Maven的dependencyManagement强制固定POI版本。但从长期看换一个不依赖强反射的库更省心。4.2 字体库依赖的Docker部署问题之前使用EasyExcel时线上容器偶尔报缺少字体相关组件需要在Dockerfile里额外安装libfreetype6否则导出带字体设置的Excel会失败。这个问题在Fesod上没有复现因为它默认不依赖系统字体库字体处理完全走Java侧的字库文件。如果你的项目仍然用EasyExcel建议在镜像构建时提前加上系统依赖别等上线了再补。迁移到Fesod之后我的Dockerfile反而简化了一层。4.3 模板合并区域不准的排查思路如果你也正在用Fesod的模板引擎可能会遇到动态行插入后合并区域位置不够准的情况。我的排查顺序是这样的先检查模板里动态区域上下是否有“全行合并”的单元格。全行合并的单元格在Fesod里会被当作一个不可分割的块当动态行插到它下方时它不会跟着移动而是留在原地。遇到这种情况把模板里的全行合并改成“限定列范围的合并区域”动态推挤就正常了。另一个常见问题是模板里动态区域本身带有多余的隐藏行列。隐藏行或列在复制扩展时可能会导致区域宽度偏移。解决办法是编辑模板时把动态区域附近的隐藏行列清掉再重新渲染一次。4.4 嵌套list渲染慢的真凶嵌套list在数据量一大时渲染速度容易掉下来。我一开始以为是Fesod引擎本身慢后来分析发现瓶颈在模板里的“跨行单元格引用”。如果在模板里写了跨动态区域的单元格合并且循环块内部还引用了这个合并区域Fesod需要反复计算合并区域的边界开销呈指数上涨。优化方式是把这类跨循环区域的样式和合并操作挪到动态区域外面利用Fesod的“尾部队列自动下移”机制去处理而不是在循环内反复合并。调完之后同样的客户订单明细导出渲染时间从22秒降到了9秒。4.5 常见问题速查表症状原因解决建议模板动态区域下方合并单元格不移动模板中使用了全行合并改为限定列范围的合并嵌套循环渲染巨慢循环内包含跨循环合并引用将合并和样式挪出循环读Excel时字符串带乱码换行符默认不处理单元格内换行使用textPolicy设置换行解析目标字段映射不上、总是空值对象访问器命名不符合Fesod约定提供标准的getter/setter或显式FesodColumn导出的中文变成?号JDK字体库缺失确保JVM有中文字体或配置Fesod使用内置字体行高在动态填充后异常模板行高为自动适应在模板中为动态区域行设置固定行高这个表里的内容不一定覆盖所有场景但都是我在迁移过程中真真切切踩过的坑。如果你也遇到类似问题按这个顺序排查通常能省一点时间。迁移完成之后我又把项目里几个老报表重新梳理了一遍。有些模板在EasyExcel时代要写三四百行后处理代码的换到Fesod后模板自身就解决了。个人而言我不会说EasyExcel该被淘汰毕竟简单场景它依然很顺手。但如果你和我一样被复杂表头、模板合并、嵌套列表这几个词反复折磨Apache Fesod确实值得拿出来试一轮。最后再分享一个小技巧无论是哪个Excel库都不要在业务代码里堆Excel布局细节把模板和数据结构设计好后续维护会轻松非常多。