ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

告别EasyExcel:Apache Fesod复杂表头与嵌套List渲染实战

告别EasyExcel:Apache Fesod复杂表头与嵌套List渲染实战 1. 为什么我决定告别 EasyExcel先说结论不是 EasyExcel 不好而是我在实际项目里被它“卡脖子”的次数实在太多了最后不得不换掉。我负责的系统里有个很典型的场景每个月要从外部供应商那边导入一大批 Excel表头是动态生成的字段多的时候能到七八十列而且前几行还是多层合并表头——父列下面挂子列子列下面还有嵌套的子子列。以前用 EasyExcel 导入这种文件我需要先写一堆注解、再搞自定义 Listener、还要处理单元格合并逻辑光是表的模型类就得维护两三百行代码。每次供应商悄悄改一列我这边就要跟着改代码、改测试、重新发版真的是人在工位坐锅从天上来。后来我调研了一圈发现 Apache Fesod也就是 Apache FOP 生态里的新一代 Excel 处理项目有些地方也叫 Fesod Excel在这类复杂表头的场景下要舒服得多。它的核心思路跟 EasyExcel 不一样EasyExcel 是“按模型映射数据”Fesod 更偏“按模板渲染 流式填充”前者适合字段相对固定的报表后者更适合表头复杂、模板经常微调、数据又有嵌套结构的业务。这个差异在普通导出场景里感知不强但一旦碰上上面说的那种“导入一张天级表头 导出几百个 Sheet”的需求差距就非常明显。这篇文章我不打算把 EasyExcel 批得一文不值毕竟它简单场景下确实顺手。我主要是想把这几个月迁移的真实过程拆开讲踩过的坑、怎么替换、模板怎么设计、嵌套 List 怎么渲染、合并单元格怎么处理、还有几个很容易被网上的 demo 带偏的地方。如果你是 Java 后端日常工作里被复杂 Excel 折磨过这篇文章应该能帮你在选型上少走不少弯路。2. 换框架前的技术选型分析2.1 EasyExcel 到底卡在了哪里先说清楚EasyExcel 在简单导出上体验是很好的。一个实体类加几个注解几行代码就能导出一张漂亮的表。问题出在下面几个点复杂表头导入多层合并表头用 EasyExcel 处理本质上要把“表头结构”硬编码成 Java 类比如ExcelProperty(value {父列, 子列}, index 3)。供应商表头一变Java 类就得跟着变重编译重发版非常被动。嵌套 List 的渲染比如一个订单里包含多个商品明细每个明细又有多个批次这种三层嵌套导出EasyExcel 原生支持并不好一般得靠手动拼 CellRangeAddress 搞合并单元格或者先把数据压平成一张平表再导但那样格式又很难看。模板填充的合并单元格用 EasyExcel 的fill方法填充模板时如果模板里有合并单元格填出来的行经常错位尤其是往下追加行的时候合并区域不会自动延展容易把整个表格结构撑坏。大数据量时的内存控制虽然 EasyExcel 主打流式读但在复杂模板填充场景下依然会冒出各种 OOM特别是同时渲染多个 Sheet、每个 Sheet 又有几十列的时候内存压力比想象中大得多。这不是我一个人的感受。很多人的报错记录里都出现过nosuchfielderror factory、easyexcel libfreetype6之类的问题前者多半是依赖冲突或者版本不一致导致工厂类加载失败后者是在 Linux 环境中缺了字体库导出带文字的图片时就崩。这些坑单独看都不难解但攒在一起维护成本就很高了。2.2 为什么 Apache Fesod 更适合复杂场景Apache Fesod 属于 Apache 开源生态里的办公文档处理项目底层沿用了 Apache FOP 在 XML 渲染上的思路把 Excel 当成一种可以通过模板与数据源绑定渲染的文档。它的优势在我看来有三点第一表头结构跟代码解耦。Fesod 里表头是模板的一部分模板是 xml 或 excel 文件不在 Java 代码里。供应商调整表头我只需要改模板文件逻辑代码基本不动。这个对业务变化快的系统来说价值巨大。第二对嵌套 List 的支持很自然。模板里可以通过类似循环区块的语法控制某一行或某一个区域按集合重复渲染订单、明细、批次这种三层结构可以顺着模板往下叠不需要自己在 Java 里处理坐标计算。第三模板填充和合并单元格的处理更加成熟。因为渲染引擎本身支持区域模型合并单元格在填充时会跟随循环区块自动延展不会出现 EasyExcel 那种“填充后格式全乱”的情况。当然Fesod 不是没有学习成本它的模板语法跟 EasyExcel 完全不一样团队上手需要一段时间。而且社区资料相对少不少问题得去源码或者官方示例里翻。但站在长期维护的角度这点成本是值得的。3. 迁移前置准备环境与依赖替换3.1 依赖引入的注意事项迁移第一步就是改 pom 依赖。我先把 EasyExcel 相关的依赖从项目里拆出去然后引入 Fesod 的依赖。这里提醒一下别图省事直接在旧项目里加 Fesod 依赖而不移除 EasyExcel两个框架底层有部分 XML 解析和 POI 相关的依赖容易冲突。我项目里的依赖配置大概是这样的!-- 移除后的 EasyExcel 依赖 -- !-- dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.x.x/version /dependency -- !-- Apache Fesod 核心依赖 -- dependency groupIdorg.apache.fesod/groupId artifactIdfesod-core/artifactId version0.1.0/version /dependency !-- 如果涉及复杂表头导入导出还需要引入 xml 映射模块 -- dependency groupIdorg.apache.fesod/groupId artifactIdfesod-xml/artifactId version0.1.0/version /dependency实际版本号你以自己的仓库为准我这里写的是我使用时的版本。引入之后先跑一个最简单的导出 demo确认环境没问题再往下走。3.2 Linux 环境下的字体与依赖坑很多团队生产环境是 Linux之前用 EasyExcel 导出图片时遇到过libfreetype6缺失的问题这次换成 Fesod 也差点踩到类似坑。Fesod 在渲染一些带文本的图片或复杂样式时底层可能用到字体解析系统里没有中文字体的话导出的文件里中文会变成方块或者直接报错。解决方案很简单在 Dockerfile 或服务器上装好字体apt-get install -y fonts-dejavu-core fonts-wqy-zenhei装完之后记得清一下字体缓存否则可能不生效。这个步骤看起来小但没做的话排查起来很费时间。4. 模板设计与嵌套 List 渲染4.1 模板文件设计思路Fesod 的核心是模板。我用的模板格式是 xml里面定义表格结构、合并单元格、样式以及数据填充的占位符。模板设计的好坏直接决定后续导入导出的灵活度。拿我那个供应商导入场景举例模板大致长这样workbook sheet name供应商账单 loop varorder items${orders} row cell${order.orderNo}/cell cell${order.supplierName}/cell cell${order.totalAmount}/cell /row loop vardetail items${order.details} row cell${detail.productName}/cell cell${detail.quantity}/cell cell${detail.price}/cell /row /loop /loop /sheet /workbook这里的loop就是循环区块items绑定的是数据源里的 Listvar是循环变量名。内部还可以嵌套另一个loop天然支持多层嵌套结构。这种模板的好处是表头怎么合并、循环从哪里开始到哪里结束全部一眼就能看清不像 EasyExcel 那样用代码去描述坐标。4.2 合并单元格在模板里怎么处理之前在 EasyExcel 里被模板填充后的合并单元格没少坑过Fesod 这边直接在模板里声明合并区域就行row cell merge3供应商名称/cell cell${supplierName}/cell /row这里的merge3表示这个单元格向右合并 3 列同类还有mergeDown可以向下合并。如果合并区域在循环区块内渲染引擎会跟随循环自动延展不用手算行号。我自己第一次迁移时犯过一个错把合并单元格放在循环外面结果数据一多合并区域跟数据行对不上。后来才明白合并单元格必须跟循环区块的层级保持一致——如果合并的是表头就放在循环外如果合并的是数据行的某些列就要放在循环内跟着数据一起重复。4.3 处理嵌套 List 的数据模型模板定了数据模型也要跟着调整。以前用 EasyExcel 时为了让嵌套数据能导出我得写一个扁平的 DTO把订单、明细、批次全部拼成一行。用 Fesod 之后我恢复了面向对象的模型结构public class OrderVO { private String orderNo; private String supplierName; private BigDecimal totalAmount; private ListOrderDetailVO details; // getter / setter 省略 } public class OrderDetailVO { private String productName; private Integer quantity; private BigDecimal price; private ListBatchVO batches; // getter / setter 省略 }然后直接把这个带嵌套 List 的对象丢给 Fesod 渲染引擎会自动根据模板里的loop层级展开。这个体验对比 EasyExcel 简直是质的提升——不需要自己写CellRangeAddress不需要计算合并区域代码量减少得非常明显。5. 实操过程复杂表头导入与导出落地5.1 核心代码结构我这里把导入和导出拆开说。先看导出核心就三步加载模板、绑定数据、渲染输出。public void exportOrders(ListOrderVO orders, OutputStream outputStream) throws Exception { // 1. 加载模板 InputStream templateStream getClass().getResourceAsStream(/templates/order_template.xml); FesodWorkbook workbook FesodWorkbookFactory.read(templateStream); // 2. 绑定数据 FesodContext context new FesodContext(); context.set(orders, orders); // 3. 渲染输出 workbook.render(context); workbook.write(outputStream); }模板加载用的是工厂方法数据绑定就是 set 一个参数最后 render 并输出。整个流程没有跟 POI 的 Workbook 直接打交道代码非常干净。导入复杂表头比导出要复杂一些但 Fesod 也不是靠注解硬映射而是通过模板反向解析public ListOrderVO importOrders(InputStream inputStream) throws Exception { // 1. 加载导入模板 InputStream templateStream getClass().getResourceAsStream(/templates/order_import_template.xml); FesodWorkbook template FesodWorkbookFactory.read(templateStream); // 2. 读取待导入文件 FesodWorkbook source FesodWorkbookFactory.read(inputStream); // 3. 按模板结构解析数据 FesodImportEngine engine new FesodImportEngine(template); ListOrderVO orders engine.parse(source, OrderVO.class); return orders; }导入模板里同样会声明表头结构、循环区块Fesod 会按模板的层级把数据一行行解析到对象里。这样即使表头列顺序变了只要模板里调整一下顺序就行Java 代码不用动。5.2 动态表头的应对方案回到我最头疼的动态表头。供应商有时候会在表头中间插入一列“备注二”以前用 EasyExcel我必须改实体类加字段重新发版。现在用 Fesod我可以让表头模板也变成动态的——在模板里用条件判断控制某些列是否渲染loop varextraColumn items${extraColumns} row cell${extraColumn.name}/cell /row loop varrowData items${rowDataList} row cell${rowData[extraColumn.key]}/cell /row /loop /loop这里extraColumns是从接口或数据库查出来的额外列信息rowDataList是对应的数据行。列是动态的模板语法还是同一套Java 代码完全不用为了新增列而改结构。这个方案上线之后我再也没被供应商临时加列搞得加班过。5.3 大数据量渲染时的内存优化Fesod 虽然比 EasyExcel 在这方面好一些但也不是万能的。我早期用 Fesod 导出几万行数据时也 OOM 过一次。后来看源码发现它跟 EasyExcel 一样支持行级别的流式渲染。要让流式渲染生效需要注意几点不要一次性把所有数据都放到内存里的 List能用Iterator或分批查询就用分批查询。渲染时尽量使用FesodStreamingWorkbook而不是普通的FesodWorkbook。如果同时导出多个 Sheet尽可能串行处理避免多个 Sheet 同时驻留内存如果确实需要并行要严格限制线程数和队列大小。我后来把一次导出 10 万行数据的查询改成流式查询内存占用从原来的接近 2GB 降到了 500MB 以内效果非常明显。6. 常见问题与排查技巧实录6.1nosuchfielderror factory或nosuchfielderror factory类似报错怎么处理这个报错我迁移时也遇到过当时一脸懵报错信息大致是java.lang.NoSuchFieldError: factory排查下来是依赖冲突。Fesod 底层依赖了某些 XML 解析库而项目其他地方用了老版本的 POI 或 XMLBeans导致类加载时找不到对应字段。解决办法是用mvn dependency:tree查看依赖树把重复依赖找出来。排除冲突依赖比如dependency groupIdorg.apache.fesod/groupId artifactIdfesod-core/artifactId version0.1.0/version exclusions exclusion groupIdorg.apache.xmlbeans/groupId artifactIdxmlbeans/artifactId /exclusion /exclusions /dependency然后单独引入一个兼容版本。这个问题的核心就是版本不要想着绕过去直接理清依赖树最靠谱。6.2 模板填充后合并单元格断裂有一次我导出的文件打开后发现数据区域的合并单元格没有连贯起来隔几行断一次。排查半天最后发现是模板里合并单元格的声明放在了循环区块的上方而不是在循环内部。举例来说如果我要让每一行里“商品名称”列合并两列那么合并声明必须在loop内部和row同级这样每循环一行就渲染一个合并区域。如果放在loop外面它只会渲染一次后面循环出来的行自然就没有合并效果。这个规则记住一句话循环区块内部声明的合并会随循环重复循环外部的合并只在最终文件的对应位置出现一次。6.3 单元格换行内容解析错位还有一次导入的 Excel 里有个单元格内容是带换行的用 Fesod 解析后换行符变成了多个空格导致数据错位。解决办法是在模板里给对应列设置特殊处理cell typetext wraptrue${content}/cell同时在解析时如果发现换行被吃掉可以考虑在数据绑定前对来源字符串做一次预处理把\r\n统一替换为\nFesod 对\n的处理通常比\r\n更稳定。6.4 模板里的中文字体变成方框这个问题我在 3.2 里提过主要是 Linux 服务器缺少中文字体。除了安装字体还有一种方式是直接在模板里指定字体名称比如cell stylefont-family: Microsoft YaHei${content}/cell但注意如果服务器上没有安装微软雅黑指定了也没用。所以最稳妥的还是安装系统字体。7. 迁移过程中的真实体验说完技术细节再聊聊感受。从 EasyExcel 迁到 Fesod我最大的变化是不再害怕供应商改表头了。以前每次收到新模板心里都是一紧现在只需要打开模板文件改几行 xml 配置重新跑一下测试就完事。但我也要说实话Fesod 的上手曲线比 EasyExcel 陡峭。EasyExcel 是那种看一眼 demo 就能上手的框架Fesod 需要你先理解模板引擎的思路特别是loop、cell、sheet这些标签之间的关系。一开始写模板的时候我经常漏标签渲染出来空荡荡一张表后来慢慢调才习惯。给后来者一个建议先别急着把线上功能全部迁过来找一个小模块练手把导入、导出、嵌套 List、合并单元格这几个核心场景跑通确认团队成员都能熟练使用模板语法了再逐步迁移核心报表功能。我自己就是从一张最简单的导出表开始用了大概两周时间才把系统里十几个 Excel 功能全部迁完。迁移完之后代码量差不多减了三分之一维护起来轻松太多了。8. 几个容易被忽略的易错点最后集中说几个我踩过的坑都是网上教程不会专门讲的。第一个是模板 xml 的编码问题。Fesod 的模板文件默认按 UTF-8 解析如果你的模板里直接写了中文务必确保文件编码是 UTF-8别用系统默认编码否则导出后全是乱码。我用 IDEA 设置文件编码为 UTF-8 之后这个问题就没再出现过。第二个是循环区块里的空数据问题。如果orders是 null 或者空 ListFesod 会直接跳过循环但如果你在循环外面还有别的行可能出现表头和数据对不上的情况。所以我一般在渲染前做一次判空如果是空集合就渲染一行“暂无数据”并合并整行。第三个是文件流关闭问题。Fesod 的write方法执行完外层 OutputStream 还需要你自己关闭不然文件可能没写完或者资源泄漏。这个跟 EasyExcel 的习惯不太一样EasyExcel 的finish方法内部会关闭流Fesod 不会写代码时要格外注意。第四个是依赖冲突排查的思路。如果一上来就出现莫名其妙的NoClassDefFoundError、NoSuchFieldError不要怀疑 Fesod 有问题先用mvn dependency:tree找重复依赖十有八九是 POI、XMLBeans、commons-compress 这几个库的版本冲突。9. 总结性质的个人建议写到这里我也不是劝所有人都立刻抛弃 EasyExcel。如果你的系统里 Excel 功能很简单字段固定、表头固定、数据量不大EasyExcel 依然是很好的选择。但如果你跟我一样频繁面对复杂表头、动态列、嵌套数据、模板合并单元格那我强烈建议你认真看看 Apache Fesod。从我这几个月的使用体验来看Fesod 真正解决的不是“能不能导出 Excel”这个问题而是“Excel 结构变化时你的代码需不需要跟着变”这个问题。后者才是日常开发里真正消耗精力的地方。如果你决定尝试 Fesod我的建议是先做一个小型 POC用你系统里最复杂的那张表当测试用例跑通之后再决定是否替换。这样你能在最短时间内感受到模板引擎带来的差异同时也能提前发现团队适应成本。就我个人而言换过来之后再也不想换回去了。
返回列表