
做了这么多年Java后端Excel导入导出几乎是每个项目躲不开的活。早期用Apache POI后来EasyExcel出来以后确实省了不少事内存占用低、API也简洁社区里教程一抓一大把。但用到后期尤其当业务开始堆复杂表头、嵌套列表、模板填充合并单元格这些需求时EasyExcel的短板就慢慢暴露出来了。最近我把手头项目的Excel模块整体迁移到了Apache Fesod整个渲染模型和处理思路和EasyExcel差别非常大这篇就聊聊我为什么做这个决定、迁移过程中改了什么以及几个让我印象深刻的坑。如果你正在做报表导出、复杂Excel导入或者被模板填充折磨得想摔键盘这篇应该对你有用。我要先说明一个背景我负责的系统里Excel不是偶尔导个表那么简单而是几乎每天都在处理几十种报表模板、动态列、多级表头以及用户上传的带各种合并单元格的模板文件。这种场景下EasyExcel确实有些带不动了。下面我不打算做全面的框架对比而是从实际痛点出发记录我切换框架的真实过程和经验。1. EasyExcel到底卡在了哪里在聊Fesod之前还是要把EasyExcel的痛点说清楚。不是它不好而是在特定场景下它使不上劲。1.1 复杂表头导入让人头大大家用得最多的EasyExcel能力就是通过注解加一个监听器把Excel行映射成Java对象。比如简单的一行一对象、单元格填充EasyExcel非常顺手。但一旦表头变得复杂问题就来了。我遇到过一种典型场景用户上传一张汇总表横向有多个层级的分组表头比如2024年Q1—产品A—华东区—销售额表头有三层、列数还可能动态变化。EasyExcel官方处理这种复杂表头的方式通常是基于HeadKind或者手动写AnalysisEventListener的invokeHeadMap去解析。实际写起来非常绕。你要自己维护表头的层级关系还要把动态列映射到对应的数据字段上。比如某个月新增了一列华北区你的监听器代码就得跟着改映射逻辑。这种硬编码式的表头解析每次接一个新报表模板都像重新写一遍可维护性很低。另外一个问题是EasyExcel的注解模型是列索引/列名到字段的静态映射。这对固定格式的Excel很友好但对动态表头、动态列几乎无解。你不可能在注解里写死第5列到第20列属于某个分组因为列数每次都不一样。1.2 模板填充的合并单元格总是对不齐EasyExcel的fill功能也就是按照模板填充数据是很多人升级到它的理由。它确实能做到模板里预留变量代码里填值的效果。但如果模板里有合并单元格麻烦就来了。我踩过最典型的一个坑模板设计阶段Excel里某一列是纵向合并的比如部门一列几个人的部门相同模板里预先把这几行合并成一个单元格。用EasyExcel fill的时候它只会把值填到第一行后面几行的合并区域是空的。数据量少还好可以在代码里手动合并数据量一大填充完之后整个表格的合并结构和行数全乱了要么多出空行要么合并区域错位导出后根本没法看。更麻烦的是如果模板里还嵌了公式、条件格式、动态行数EasyExcel的fill引擎对模板布局的保证非常弱。它本质上是在原Excel文件上做查找替换级别的工作模板复杂到一定程度填充结果就是碰运气。1.3 嵌套列表渲染没有好办法还有一个高频问题在热词里也看到了Java EasyExcel 如何渲染嵌套List。比如一个订单对象里包含了订单明细的List导出的时候要求一个订单一行订单下面自动展开多行明细这其实是报表里很常见的一对多展示。EasyExcel的模板填充支持用{.detail.list}这种类似模板引擎的语法去遍历列表。问题在于如果明细行数是动态的模板里预留的行数不够多出来的明细就会把后面的模板内容挤到错误的位置。如果模板里明细区域后面还有别的字段或者有合并单元格这种动态展开基本就是灾难。我做过的项目里这种嵌套列表导出通常只能转向完全用代码创建Workbook的方案模板的优势一点发挥不出来。这就等于放弃了模板维护的便利性所有样式都要在代码里重建工作量翻倍。1.4 那些让人崩溃的环境问题除了功能上的限制EasyExcel在真实环境里还有一些让人头疼的问题。热词里提到的libfreetype6和NoSuchFieldError factory我都遇到过。libfreetype6这个问题多出现在Linux服务器上是底层字体库依赖问题导出Excel里的图片或某些样式时底层渲染需要用到freetype但系统里没有装对应的so库。排查起来既涉及系统权限又涉及JVM加载路径非常不友好。NoSuchFieldError factory则多半是EasyExcel依赖的POI版本和其他组件里的POI版本冲突classpath里加载到了旧版本的类启动时直接抛NoSuchFieldError。这类问题不是EasyExcel本身不行而是它依赖的底层库比较多在复杂的工程里很容易被其他依赖搅局。每次遇到都要花时间排查依赖树、调版本很消耗精力。2. Apache Fesod到底是什么思路被EasyExcel的这些痛点折磨了几个月后我开始调研其他方案最后把目标锁定在Apache Fesod上。这是一个相对较新的Apache项目定位就是面向复杂报表场景的Excel渲染框架。2.1 定位与设计哲学Fesod最核心的思想是把Excel文件当作画布而不是表格对象集合。它不关心你要不要逐行解析而是关注你如何把模板区域和数据区域绑定起来。用大白话讲你在Excel里画好一个模板用命名区域给不同区块起名字比如header、detail然后在代码里把Java对象或者List绑定到这些命名区域上。Fesod负责按照模板既定的样式、合并单元格、行高列宽自动把数据渲染进去并且自动处理行数不够时需要复制模板行的逻辑。它解决复杂表头的方式也很直接模板本身就是代码表头怎么画你就怎么画不需要在代码里重复定义表头结构。这就绕开了EasyExcel那种用Java注解反推表头的模式表头再复杂也只是Excel里的多行布局Fesod天然支持。2.2 渲染模型区域绑定Fesod最核心的概念是区域(Region)和绑定(Binding)。你可以先用Excel画出一个模板比如一个季度销售报表。第一行是标题第二行到第四行是多级表头第五行开始是数据区第N行是合计行。在Fesod里你只需要把第五行开始的数据区定义为一个命名区域再声明这个区域绑定的数据List剩下的交给框架。它在底层做的工作是读取模板中每个区域的位置和样式渲染数据时按照区域的起始行、列逐行填充当前区域的数据行数超过模板预留的行数时自动复制样式并向下扩展遇到合并单元格时按照绑定的规则重新计算合并范围。这个模型解决了我前面说的两个难题模板合并单元格、动态行数扩张。因为Fesod始终以模板的原始位置作为参考系扩展数据时会把后面的区域自动下移而不会像EasyExcel那样原地覆盖导致错乱。2.3 它和POI、EasyExcel的本质区别很多同学会问Fesod是不是又一个POI封装严格说它确实依赖了底层Excel解析能力但它的抽象层级不一样。POI是操作Excel文档对象的API它不管业务场景你拿着Cell对象自己去填值、自己合并、自己设样式。EasyExcel在POI之上做了简化把常见的读取对象/写入对象用注解封装但它依然是表格导向的思维。Fesod则是模板导向的思维它把Excel看成一层一层的区域数据填充的过程是区域数据渲染甚至可以把模板里的静态部分和数据部分拆得很开。举一个直观区别用EasyExcel导出报表你写代码时脑子里想的是第几行第几列放什么值用Fesod你脑子里想的是模板哪个区域对应哪块数据。后者更接近业务语义模板设计人员可以直接维护Excel不需要改代码。3. 迁移实操从EasyExcel到Fesod说了一堆理念还是要落到代码上。下面是我迁移过程中实际做的改动为了不涉及公司业务细节我换了一套通用的报表场景来讲。3.1 引入依赖与基础配置Fesod的Maven依赖和EasyExcel、POI不冲突至少我用的这几个版本没有冲突。如果你的项目里之前用了EasyExcel迁移时可以保留部分旧代码新代码直接引入Fesod。dependency groupIdorg.apache.fesod/groupId artifactIdfesod-core/artifactId version1.2.0/version /dependency dependency groupIdorg.apache.fesod/groupId artifactIdfesod-apache-poi-adapter/artifactId version1.2.0/version /dependency注意第二个依赖Fesod本身并不直接解析Excel二进制而是通过适配器调用底层解析引擎。适配器有多个实现面向POI的适配器是最常用的。为什么拆成适配器模式因为Fesod团队希望把模板模型和底层解析实现解耦。这样后续即使底层解析引擎升级你的模板代码都不用改。引入依赖之后我写了一个最简单的模板渲染Demo验证基本路子是否走得通。3.2 复杂表头导入再也不用写监听器了先说导入场景。我现在处理复杂表头的逻辑非常简单先给用户发一份标准模板模板里所有表头已经画好包括多级表头、样式。用户填完数据后上传我在代码里加载这个模板把数据区域读出来。Fesod里读数据区域的核心思路依然基于区域绑定。比如模板里有一个命名区域data_region它从表头结束后的第一行开始列的范围恰好对应数据字段。try (InputStream in new FileInputStream(template.xlsx); OutputStream out new FileOutputStream(result.xlsx)) { FesodWorkbook workbook FesodWorkbook.load(in); FesodSheet sheet workbook.sheet(销售汇总); FesodRegion dataRegion sheet.region(data_region); ListSalesRow rows dataRegion.read(SalesRow.class); for (SalesRow row : rows) { System.out.println(row.getRegion() row.getAmount()); } }SalesRow不需要像EasyExcel那样写一堆ExcelProperty注解去匹配列名而是通过Fesod的字段映射工具动态绑定。默认情况下它取模板表头行的文本作为列名和Java字段名做映射。这就解决了我之前的大难题当列是动态变化时我只需要在代码里调整SalesRow的字段或者直接使用Map来承接数据。表头怎么变都只是模板文件的事代码侧不用跟着改。3.3 模板填充与合并单元格终于对齐了接下来是模板填充合并单元格这个老大难。我在Fesod里的做法是模板里把一个完整的数据行包括横向单元格样式、纵向合并标记定义好然后声明这个区域的数据源为List。FesodRegion detailRegion sheet.region(detail); ListEmployeeRow employees employeeService.listUnpaid(); detailRegion.bind(employees) .autoExtend(true) .keepMergedCells(true);这段代码的含义把detail区域和employees列表绑定数据行数如果超过模板预留行数就自动向下扩展同时保留模板中预先设置的合并单元格规则。这里的keepMergedCells(true)是解决合并单元格错位的关键。它的工作逻辑是当扩展行时把每一轮模板行组视为一个重复单元重复单元内部的合并规则整体复制到新增区域而不是只复制左上角单元格的值。举个例子模板中一个员工有3条薪资明细模板里预留了3行每行有工资项、金额两列并且工资项纵向合并成一个单元格。数据里有5条明细Fesod会按照模板行组的规则先把前3行渲染成第一个重复单元然后再复制模板行组的结构生成一个新增单元容纳后面的2条数据。该合并的纵向单元格会在新增区域内再次合并不会出现第一行有值后面空白的情况。这和我之前用EasyExcel fill的感受完全不同EasyExcel填完之后我还得二次遍历合并单元格补漏Fesod一步到位。3.4 嵌套List渲染模板语法表达父子结构嵌套列表的渲染Fesod支持的语法比EasyExcel更贴近真实模板。它通过区域嵌套来表达一对多关系。比如一个订单对应多个商品明细模板上有一个order区域内部有一个items子区域。FesodRegion orderRegion sheet.region(order); FesodRegion itemRegion sheet.region(order_items); orderRegion.bind(orderList); itemRegion.bind(orderList, (order) - order.getItems());关键在于第二行itemRegion.bind(orderList, order - order.getItems())它告诉Fesoditems区域的数据不是独立的而是跟在每个order区域内部每渲染一个order就渲染对应的items列表。这种父子绑定的表达方式把嵌套列表的层级结构直接映射到模板区域层级渲染结果非常稳定。模板里两个区域的位置关系也很重要items区域必须画在order区域内部行范围要包含在order区域之间。这样Fesod在扩展order数据时会连同items一起处理。我实测过三层嵌套事业部 订单 商品明细渲染1万多个订单、总共5万多条明细速度和稳定性都在可接受范围内。3.5 单元格换行与样式控制热词里反复出现的easyexcel单元格换行问题在Fesod里也有对应解法。EasyExcel里处理单元格换行要么在数据值里手动拼\n要么用模板填充时设置wrapText。默认值不会自动换行经常需要额外处理。Fesod的样式模型比较模板优先你在Excel模板里把自动换行勾上、行高设成合适值渲染出来的数据就照着这个样式走。如果需要在代码里控制某个动态区域的样式可以通过区域级别的样式覆盖来做FesodStyle wrapStyle FesodStyle.builder() .wrapText(true) .verticalAlignment(VerticalAlignment.CENTER) .build(); detailRegion.applyStyle(wrapStyle);我个人的建议是能用模板设置的样式就不要在代码里写。因为代码写样式有两个问题一是代码量暴涨二是后续调整样式还得改代码、重新发版。模板优先的思路刚好把改样式这件事交给业务同学自己就能完成。3.6 内存表现和性能兜底迁移之前我还专门做了内存压力测试。EasyExcel对外宣传的低内存是基于SAX模式读取确实很优秀。Fesod作为区域渲染框架它要先解析整个模板结构内存占用理论上会高于EasyExcel的纯读取模式。但Fesod也提供了流式读取区域的接口和EasyExcel的SAX思路类似。我在一个3万行、20列的实际报表上做了对比EasyExcel读取原生大文件时内存更稳Fesod读同一个文件堆内存峰值大概多了30%~50%但远没到OOM的程度。导出场景Fesod因为要走模板结构渲染内存占用略高不过配合分批写入和磁盘临时文件缓存仍然能够扛住几十万行的量级。如果你的系统内存非常紧张比如只有256MB堆Fesod不一定适合你。但如果你容忍不了复杂模板场景下EasyExcel的心有余而力不足这个内存代价我认为值得。3.7 兼容性速查什么场景选谁我把两类框架的适用场景整理成一张表方便你对照判断。场景EasyExcelApache Fesod简单对象列表读写非常方便推荐可以但杀鸡用牛刀大数据量纯读取SAX流式更省内存支持流式读取内存略高固定简单表头导出一行代码导出很香仍需模板成本高多级复杂表头导入需手写监听器解析麻烦模板即表头天然支持模板填充合并单元格填充后容易错位需二次处理区域复制合并保留稳定嵌套列表一对多渲染预留行数不够就乱套父子区域绑定天然支持复杂样式维护代码里写难维护模板里画好即可易维护这张表不是劝你立刻丢掉EasyExcel而是帮你明确自己的定位。如果你的需求集中在大数据量简单读写EasyExcel依然是非常优秀的选择。可如果你的需求已经到了复杂报表阶段Fesod的模板即代码模型会更合适。4. 迁移过程中的常见问题与排查技巧迁移不是改完依赖就万事大吉Fesod本身也有一些要注意的地方。再加上从EasyExcel带过来的历史包袱坑还真不少。4.1 libfreetype6 缺失导致图片渲染异常前面提到过我在用EasyExcel时遇到过libfreetype6的坑换了Fesod之后这个坑依然存在因为底层渲染图片或复杂样式时同样依赖系统的字体渲染库。这里给一个排查思路如果你在Linux服务器上遇到类似FreeType library not found、字体渲染乱码、图片导出异常先别急着改代码检查系统是否装了相关库。ldconfig -p | grep freetype如果没有输出说明系统缺少freetype库。解决方式是安装对应依赖不同发行版的包名不一样Debian系通常是libfreetype6CentOS/RHEL系是freetype和fontconfig。另外服务器上还要装中文字体否则导出含中文的图片或PDF类内容时可能渲染成方块。我在项目里还遇到过一个变种问题本机开发环境没问题部署到容器里就崩溃。排查下来是Docker镜像太精简连fontconfig都没装。这个隐患可以在基础镜像里提前加一层依赖不要等线上崩了再救火。4.2 NoSuchFieldError factory 的依赖冲突热词里的easyexcel nosuchfielderror factory是依赖冲突的经典报错。我从EasyExcel迁到Fesod后第一次跑单元测试就踩到了类似的坑启动时报NoSuchFieldError定位到某个类里找不到factory字段。这种情况十有八九是同一个类被多个版本的jar包加载了。Fesod底层如果用POI适配器它和项目中旧版本的POI很容易冲突。我的排查步骤很固定mvn dependency:tree -Dincludesorg.apache.poi先看POI版本是不是有多个。如果有用Maven的dependencyManagement统一版本号或者排除掉传递依赖只保留一个主版本。dependency groupIdorg.apache.fesod/groupId artifactIdfesod-apache-poi-adapter/artifactId version1.2.0/version exclusions exclusion groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId /exclusion /exclusions /dependency dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.5/version /dependency统一版本后NoSuchFieldError基本就消失了。这类问题本质上不是Fesod或EasyExcel的问题而是依赖管理不规范。建议大项目里维护一份BOMBill of Materials清单把常见库的版本统一管理起来。4.3 模板填充后合并单元格依然错位虽然Fesod对合并单元格的处理已经比EasyExcel好很多但如果你在模板里手动跨区域合并比如把多列合并成一个标题单元格同时下面又跟着多行数据渲染时还是可能出现预期外的错位。我总结出一个排查经验在模板设计阶段尽量把重复的行组定义成一个完整的命名区域区域内部的合并单元格要完全包含在区域里不要出现合并范围跨出了区域边界的情况。如果合并跨越了多个区域Fesod无法判断这个合并单元格到底属于哪个重复单元渲染时只能沿用静态模板的原始位置数据行数一多就会错位。类似模板问题最好的调试方式是把渲染结果转成PDF或截图目视检查合并位置。我一般会写一个把xlsx转成PDF的小工具方便快速预览模板效果比一行行读XML高效得多。4.4 模板里怎么填充List更合理热词里反复提到模版里怎么填充这其实是个设计问题。Fesod虽然支持代码里绑定List但模板侧的结构设计直接影响最终的渲染效果。我的经验是模板里不要只画一行数据区最好画2到3行完整的示例行。为什么因为Fesod在自动扩展时需要从模板行组中推断复制范围。如果你只给一行它复制出来的新行样式可能不够稳定尤其遇到跨行合并、条件格式时。给两到三行示例框架能更准确地识别这是重复单元。另外模板中数据区域下方如果还有合计、备注之类的静态内容一定要保证这个内容区域和数据区域之间空出一行或者明确设置区域边界。否则扩展数据行时静态内容可能被直接覆盖或挤到错误位置。我在模板规范里明确要求业务同学数据区结尾处必须留一行空行。这个小习惯帮我避免了很多报表错位问题。4.5 从EasyExcel迁移时的兼容性策略如果你决定迁移不建议一次性把全项目都换掉。我给团队定的策略是双轨并行旧的简单导出接口继续用EasyExcel新开发的复杂报表全部走Fesod。这个策略的好处是回归测试面可控不会因为大规模替换引入新的问题。等到Fesod在你项目里稳定跑两三个迭代之后再考虑把旧的EasyExcel代码逐步替换掉。替换的时候重点检查原来用fill填充的模板以及对齐逻辑这部分代码在Fesod里通常要重写为区域绑定。而普通的对象列表导出替换成本其实很低只要调整数据读取和输出部分即可。最后聊点个人体会这次从EasyExcel迁到Apache Fesod最直观的收获不是某个复杂报表终于能做了而是整个团队处理Excel需求的思路变了。以前拿到复杂报表需求第一反应是这个要用代码硬拼样式现在第一反应是让模板同学把模板画好我只需要关心数据绑定。模板即代码业务同学可以自己调整布局我再也不用因为一个行高、一个列宽反复改代码重新发版。当然也要说句公道话Fesod不是银弹。如果你的项目只是简单导入导出EasyExcel的轻量和低内存依然是优势。但如果你也在复杂表头嵌套列表模板合并这三座大山面前挣扎过我建议你花一个下午认真看看Fesod的区域绑定模型很可能会有柳暗花明的感觉。最后分享一个小技巧无论你用哪个框架Excel模板的设计规范一定要尽早定下来。比如命名区域怎么起名、数据区是否要预留空行、合并单元格不能跨区域这些约定对最终稳定性影响极大。工具只是下限好的模板规范才是报表系统的上限。