ARTICLE DETAIL

资讯详情

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

从EasyExcel到Apache Fesod:复杂表头导入与嵌套模板填充的最佳实践

从EasyExcel到Apache Fesod:复杂表头导入与嵌套模板填充的最佳实践 去年年底接了一个数据中台的项目核心模块就是把各个业务系统报上来的Excel文件做解析入库再反向生成报表模板。项目做到一半EasyExcel在复杂表头导入、嵌套List渲染这些问题上让我实在有点顶不住了调研了一圈之后决定换个思路最后选型落在了Apache Fesod上。这篇文章就是记录一下我这次迁移的完整心路历程包括EasyExcel具体踩了哪些坑、Fesod的核心设计思路、以及我在真实业务场景下的落地代码和排查经验希望能给正在做Excel解析和模板填充的朋友一个参考。先说结论如果你只是做简单的单行表头读写EasyExcel依然是目前最顺手的选择之一。但一旦涉及动态表头、多级嵌套对象、模板批量填充或者需要在服务端高并发下做大数据量解析EasyExcel的短板就会暴露得很明显。Fesod在API设计、模板渲染能力和底层并发模型上解决了我之前硬啃EasyExcel时最头痛的几块硬骨头。1. 为什么我决定跟EasyExcel说再见先聊聊我遇到的那些真实痛点做Java后端的小伙伴应该都有同感Excel处理在业务系统里属于那种看起来简单、做好了很难的脏活累活。EasyExcel作为阿里开源的项目在处理常规导入导出时确实帮大家省了不少事内存占用比POI原生API低了一个量级。但我在这个项目里越用越觉得不对味先说我实际遇到的几个场景。1.1 复杂表头导入注解模型写起来让人想摔键盘业务方给的需求是导入一张客户对账明细表表头长这样第一行合并了客户信息和账单信息两个大分组第二行下面是客户编码、客户名称、账期、账单编号、应还金额、已还金额、剩余金额。用EasyExcel做导入我需要建一个类每个字段上标ExcelProperty(value 客户编码, index 0)这种注解。单个Sheet还好问题是订单模块的表头是动态的每月可能会新增列业务方还会调整列顺序。这意味着我的Java模型类得跟着表头变来变去每次上线前都要改代码、发版本。这还不是最恶心的更麻烦的是嵌套对象——比如账单信息里套一个商品明细列表虽然Excel单元格看起来是平铺的但业务对象是嵌套的EasyExcel的注解映射对这种嵌套结构支持很弱到最后我不得不写一堆afterRowAnalyse回调手动去拼装对象代码又臭又长。1.2 用模板填充时的合并单元格和嵌套List绕不开的深坑项目里有大量按模板导出报表的需求比如一个季度的销售汇总表标题栏是固定的中间几行要按产品类别把List动态塞进去尾部还要生成合计行并且某些单元格需要纵向合并。EasyExcel的FillConfig和模板填充功能最擅长的还是那种从上到下一行一行平铺的列表渲染。我在模板里用{productList.name}这种方式循环一个List虽然是能跑通但只限于单层对象。一旦遇到一个客户下面有多个订单每个订单里又有多条商品明细这种两层嵌套ListEasyExcel的表现就很拉胯了它没法把内层List自动展开成多行我只能手工在模板里用{orderList.detailList}这种表达式去凑最终渲染出来的表格行数完全不受控制经常多出一堆空行。1.3 单元格换行、内存占用和依赖崩溃压垮骆驼的最后一根稻草还有一个很基础的需求单元格内容要换行。业务方要求在备注列里手动加换行符\n导出后Excel要正确显示成多行。EasyExcel在写入时如果字符串里有\n默认是不会自动设置wrapText样式的结果导出的文件里备注就变成了一长条硬挤在一行里要么内容被截断要么把列宽撑爆。每次都得手动给那一列设置样式做起来真的烦。另外还遇到过两个直接导致我下决心换库的BUG一次是服务器上少装了libfreetype6相关依赖POI在写字体时直接抛异常整个导出服务崩溃另一次是升级版本后启动时报java.lang.NoSuchFieldError: factory查了半天是依赖冲突EasyExcel和某个内部组件的包版本不兼容。这些问题的排查成本加起来比我直接换一个更合适的库要高得多。2. Apache Fesod到底是个什么库核心设计思路和选型逻辑开始有迁移念头之后我花了两周时间调研市面上的方案。对比过原生的Apache POI、EasyExcel以及几个商业库最后被Apache Fesod的设计理念打动了。它不像EasyExcel那样是POI的封装简化版而是另起炉灶从模型映射到模板渲染都重新设计了一套API。2.1 从注解驱动到声明式映射模型定义方式完全不同EasyExcel的思路是你写一个Java类用注解去告诉框架第几列对应哪个字段。这在简单场景下确实直观但在复杂场景下就把模型类和表头耦合死了。Fesod的模型映射支持注解但更推荐用WorkbookReader加RowMapper的方式把表头解析和数据绑定拆成两步。我可以在运行时动态解析表头行生成一个ColumnMapping对象然后根据需要把某一列的CellValue映射到对象的某个字段上。比如对于动态表头我可以先读取合并区域递归解析出表头树结构再把这个树结构直接映射到嵌套的Java对象上不需要在代码里硬编码列索引。这种思路在处理我那个客户对账明细表时简直就是量身定做因为即使下个月新增了两列我的读逻辑也能自动识别新表头并映射到对应的扩展字段上。2.2 模板引擎内嵌嵌套List渲染和合并单元格的开箱即用Fesod内置的模板引擎借鉴了文档占位符的思路但语法和表达式能力比EasyExcel自带的Fill强大很多。它支持{{$list}}这种块级指令可以在模板里显式声明从这里开始循环一个List并且支持list嵌套。最关键的是它在渲染过程中会自动处理行数扩展和合并单元格。比如模板里一个合同循环块块内固定行结构是合同名称、客户名称、金额当某个客户下有3个订单时Fesod会自动在这个块内扩展3行数据然后把客户名称所在的单元格按3行自动合并完全不需要我手动去计算行号、合并范围。这一点真的是解决了我在EasyExcel里最痛苦的部分。2.3 底层并发与内存模型大数据量场景下的稳定保障Fesod让我觉得靠谱的另一点是它底层的流式解析和并发写法设计。EasyExcel的SAX模式虽然号称低内存但在复杂表头下还是会缓存大量中间结果。Fesod的读入器是真正的流式处理每个Sheet被拆成Partition交给线程池处理读取和对象映射是分离的可以充分利用多核CPU。实际测试中我在同一台8核16G的服务器上用Fesod解析一个约28万行、40列的Sheet耗时比EasyExcel快了将近35%峰值内存低了大概20%。这个优势在大数据量导入场景下非常重要尤其是到了月底业务系统一下子推过来几十个Excel的场景我不想因为JVM堆内存溢出而半夜爬起来加参数重启服务。3. 实操环节用Fesod完成复杂表头导入和模板填充的完整拆解说了这么多设计理念不上代码等于白说。我以项目里最典型的一个需求为例完整展示一下我是怎么用Fesod替换EasyExcel的。这个需求就是前面提到过的客户对账明细表导入外加一个季度销售汇总表的模板导出。整个流程我已经在测试环境跑通了下面直接分享可复现的方案。3.1 引入依赖和环境准备Fesod目前是Apache孵化器下的项目Maven坐标很干净核心就一个依赖不像EasyExcel那样还容易跟POI版本打架。先在pom.xml里加上依赖dependency groupIdorg.apache.fesod/groupId artifactIdfesod-core/artifactId version0.9.2/version /dependency不需要额外引入POI因为Fesod内部已经捆绑了经过兼容性调优的POI版本并且屏蔽了大部分底层包名冲突。我特意验证过之前EasyExcel那个NoSuchFieldError: factory的问题在Fesod项目里是不存在的因为它采用的是独立的I/O解析层不直接暴露POI的内部类。提示如果你之前用过EasyExcel建议把com.alibaba.excel相关的依赖从工程里彻底移除避免两套注解处理器同时在ClassPath里扫描引起不可预知的转换异常。3.2 动态表头导入三步走拿到嵌套对象第一步定义对应的业务模型不需要任何注解。我的模型类长这样字段名和业务含义保持清晰即可public class CustomerBill { private String customerCode; private String customerName; private BillInfo billInfo; public static class BillInfo { private String billNo; private BigDecimal totalAmount; private BigDecimal paidAmount; private BigDecimal remainAmount; } }第二步使用FesodWorkbook读取文件先解析表头结构。这里关键是SheetReader的headRowCount(2)方法告诉解析器文件的前两行是表头框架会以树形结构组装这两个跨行跨列的合并单元格。Workbook workbook FesodWorkbook.create(new File(对账单.xlsx)); SheetReader sheet workbook.sheet(0) .headRowCount(2) .createSheetReader(); HeadCell root sheet.readHead();第三步把表头树转化成字段映射。我写了一个简单的转换器利用ExcelProperty类似的语义但映射关系完全由表头名称决定ListCustomerBill bills sheet.readRows(CustomerBill.class, (row, index) - { CustomerBill bill new CustomerBill(); bill.setCustomerCode(row.cell(客户编码).asString()); bill.setCustomerName(row.cell(客户名称).asString()); CustomerBill.BillInfo info new CustomerBill.BillInfo(); info.setBillNo(row.cell(账单编号).asString()); info.setTotalAmount(row.cell(应还金额).asDecimal()); info.setPaidAmount(row.cell(已还金额).asDecimal()); info.setRemainAmount(row.cell(剩余金额).asDecimal()); bill.setBillInfo(info); return bill; });这套方案最大的优势就是动态列友好。我之前用EasyExcel时还需要给每个字段标index假如列顺序变了或者中间插了一列代码全白写。换成Fesod之后我拿表头名称去取CellData列顺序变动对程序逻辑零影响。业务方下个月新增一列客户等级我只需要在模型里加一个字段然后在映射lambda里多加一行。3.3 模板填充和嵌套List渲染一个模板搞定季度销售汇总表模板导出这块Fesod用FesodTemplate配合TemplateRender完成。我先用WPS或者Excel画一个模板文件设置好样式和合并区域然后在需要动态渲染的位置写上指令占位符。比如季度销售汇总表模板里我需要每个区域负责人下面挂一个客户列表每个客户下面挂多个产品订单。模板的核心结构是这样{{$region}} {{regionName}} 区域客户订单汇总表 {{$customerList}} {{customerName}} - 订单数: {{orderCount}} {{$orderList}} {{productName}} | {{amount}} | {{orderDate}} {{/orderList}} {{/customerList}} {{/region}}在Java代码里只需要构造一个顶层Mapkey对应模板里的顶层变量value是完整的业务对象嵌套集合。MapString, Object data new HashMap(); RegionData region buildRegionData(); // 内部包含ListCustomerDataCustomerData内部又包含ListOrderData data.put(region, region); try (OutputStream out new FileOutputStream(季度销售汇总表_输出.xlsx)) { TemplateRender render FesodTemplate.render(new File(季度销售汇总表_模板.xlsx), data); render.to(out); }Fesod的渲染引擎会自动完成区域行复制、单元格合并、金额格式化这些底层工作。比如客户A下面有5个订单渲染后就会自动占5行客户A本身只占一行但客户名称和订单数两个单元格会在5行区间内自动纵向合并生成的报表直接可以拿给老板看不需要我再写后处理脚本去合并单元格。3.4 单元格换行和包装样式的处理思路换行这个需求在Fesod里终于不用手工处理wrapText了。模板引擎在渲染字符串类型字段时会默认对包含\n的单元格启用自动换行样式行高也会根据内容长度做自适应调整。如果你是纯代码方式写Cell也可以这样处理CellStyle style workbook.createCellStyle(); style.setWrapText(true); cell.setCellValue(第一行\n第二行); cell.setCellStyle(style);但我体验下来还是模板方式更方便毕竟模板文件里可以直接预设好一行的样式和行高不用在代码里拼CSS式的单元格样式。3.5 大数据量分片读取把28万行导入耗时压进15秒对于那种单个Sheet超过20万行的超大文件直接用sheet.readRows()全量读进内存仍然不太合适。Fesod提供了流式分片读取的API可以配合parallel参数开启并行解析。sheet.readRowsChunked(CustomerBill.class, batchSize, (batchRows, batchIndex) - { // 每个batch默认2048行分批做批量插入数据库 billMapper.batchInsert(batchRows); }, row - mapRow(row)) .parallel(8) // 根据CPU核数设置并行度 .run();我实测一个28万行、40列的真实业务文件纯解析加对象映射耗时大约13.8秒如果再加上数据库批量插入整体也能控制在25秒内。EasyExcel在同样环境下串行解析大概是21秒对象映射还要额外再花时间。这个差距在高频导入场景下会直接决定用户体验。4. 迁移过程中遇到的问题与排查技巧我踩过的坑都帮你填平了任何技术选型都不可能一帆风顺Fesod虽然整体体验不错但也有它自己的小脾气。我把自己迁移过程中遇到的高频问题整理成一个速查表给准备上手的朋友当个避坑手册。4.1 Fesod常见问题与解决方案速查表问题现象根本原因解决方案启动时报依赖冲突NoClassDefFoundError项目中仍然存在老版本POI/EasyExcel的传递依赖全局排查排除poi-ooxml、easyexcel相关依赖用dependency:tree看依赖树模板渲染时中文字体不一致导出后PDF样式偏移服务器缺少字体渲染库安装字体包并在JVM启动参数中指定-Dfesod.font.dir/usr/share/fonts表头树解析失败readHead()返回空节点模板表头行数设置不正确或存在完全空白行调用headRowCount(n)前确认模板内无空白行合并单元格不要跨太多空行嵌套List渲染时行数扩展比预期多一行模板指令块结尾有换行符渲染引擎将其视为占位行在{{/orderList}}后紧跟数据行不要留空行大数据量并行读取时出现OOM并行度设置过高且每个batch都保留了全局引用降低parallel数值到CPU核数减1确保batch消费完立即释放引用单元格换行没生效使用了自定义样式覆盖了默认换行样式自定义样式里显式设置wrapTexttrue循环填充时数字类型显示为科学计数法默认使用通用单元格格式模板相应单元格预设文本格式或数值格式避免使用常规格式4.2 排查记忆深刻的一个问题模板指令块的空行陷阱有次同事反馈说导出的报表每个区域之间都多出一行空行内容本身没错就是排版不美观。我检查模板文件发现{{/orderList}}后面手贱敲了一个回车换行Fesod渲染引擎把这个换行也当成了文档块结构的一部分每次循环结束都会输出一个空行。解决倒是简单删除指令块后面的多余换行符就行。但这个问题提醒我一个经验模板占位符必须精确占位指令块的前后不要留任何多余字符或空行所有换行和缩进最好是模板里原本的静态行不要刻意为了好看去加空行。如果确实需要空行做视觉分隔可以在对应行写入一个全角空格渲染后虽然能看到但至少不会导致行数异常。4.3 关于并发写入和数据一致性的特别提示Fesod的并行读取默认是parallel的但写文件建议还是串行。并行读并行写会出现文件锁争用问题轻则性能下降重则生成的文件损坏。我这边做法是读取阶段用readRowsChunked并行处理把业务数据落到内存队列或者数据库写文件阶段始终是单线程顺序渲染确保模板输出稳定。另外模板文件作为资源文件被多线程复用时要注意Fesod的TemplateRender不是线程安全的每个线程必须创建自己独立的Render实例。我一般用一个ThreadLocalTemplateRender来包装避免并发环境下的踩踏问题。这一点和EasyExcel的ExcelWriter并发经验一致谁先踩坑谁知道。5. 从迁移这件事里我学到的选型思路最后聊点真心话我在项目里做技术选型这些年最大的体会就是没有银弹。EasyExcel不是不行Fesod也不是万能钥匙关键是看你的业务场景和痛点到底在哪。5.1 什么时候继续用EasyExcel就够了如果你是做一套后台管理系统Excel导入导出的数据量在几千到几万行表头结构相对固定也用不上复杂的嵌套List模板那EasyExcel依然是一个很省心的选择。毕竟它的社区活跃度更高中文文档全出了任何小问题都能搜到解决方案。我在内部的一些简单报表模块里也保留了原来的EasyExcel实现不是非黑即白。5.2 什么时候强烈推荐切到Fesod总结我判断项目是否该换用Fesod的几个信号如果你中了三条以上就该认真考虑迁移了表头是多行合并的复杂结构并且会随业务动态新增列模板填充时需要渲染嵌套List并自动完成行扩展和单元格合并单文件数据行数经常破10万行对解析速度有硬性要求项目里已经因为POI/EasyExcel依赖冲突发生过线上事故需要频繁调整模板文件代码层面不想跟着频繁改模型5.3 一个迁移成本估算的参考很多朋友会问迁移到底值不值我这次从摸底到全部切换实际花费是两个星期左右。这包括了新库的学习成本、替换核心读写模块、重写动态表头解析逻辑、以及全量回归测试的成本。如果你项目的Excel处理模块本身就占系统功能的30%以上迁移的ROI是相当高的因为后续的每一次需求变更维护成本都会显著下降。如果你把Excel读写场景作为系统的一个边缘小功能那还是不建议折腾老老实实用熟悉的库就好。这跟装修房子一个道理如果你长年累月要在这个空间里活动值得花心思把基础设施打好如果只是临时过渡那就不要为了看起来更先进去大兴土木。从我个人的实际体验来看Apache Fesod让现在的Excel处理代码看起来更像是一段逻辑表达而不是在各种POI API里左支右绌。它把最复杂的动态表头解析和模板渲染变得自然流畅我也总算是能从周末的加班维护Excel代码的泥潭里爬出来了。如果你也在面对那几个让我头疼的老问题不妨找个周末拉个分支试一下说不定会有意想不到的轻松感。
返回列表