十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

从EasyExcel迁移到Apache Fesod:Java后端Excel处理新实践

从EasyExcel迁移到Apache Fesod:Java后端Excel处理新实践 再见了EasyExcel。写下这句话的时候我刚把一个跑了两年多的导入导出模块整体重写完毕。没有依依不舍反而有种卸下一块石头的轻松感。这个决定其实不激进近半年我被EasyExcel的复杂表头导入、模板填充合并、嵌套List渲染等问题反复折磨光“libfreetype6”和“NoSuchFieldError: factory”就让我在测试环境里蹲了两天。真正让我下决心迁移的是在排查一个线上导出乱码问题时发现项目里居然同时存在三套Excel工具代码互相之间还各自为战。后来我无意间了解到Apache Fesod——一个构建在POI之上、设计思路更现代的Excel读写框架试了三天之后直接把主项目的导入导出全部切了过去。这篇博文就当是留一份迁移记录适合那些和曾经的我一样被Excel解析折腾到头秃的Java后端开发者。1. 这次告别背后EasyExcel让我反复踩坑的四个场景很多人问我EasyExcel不是挺好用的吗Apache POI才是更底层的选择为什么非要折腾新东西我的答案是EasyExcel解决了一部分“简单场景的复杂化”问题却在“复杂场景的简单化”上做得不够。我拿最近半年实际踩过的坑来说。1.1 复杂表头导入全靠胶带粘EasyExcel对简单表头的导入处理得很漂亮一两行注解就能把Excel行映射成对象列表。但一旦表头变成多行、合并单元格、分组列并列的结构事情就不对了。比如我手里那张“区域销售汇总表”表头长这样第一层区域 | 销售额 | 客户明细第二层华东 | 华南 | 本月 | 上月 | 客户名 | 联系方式 | 下单次数第三层线上 | 线下 | 线上 | 线下这种表头在EasyExcel里没有一个直观的映射方式。你可以用headRowNumber指定表头行数然后用invokeHeadMap自己去拼行号再手动从AnalysisContext里一格一格取数据。一次两次能忍业务里一多维护成本直接爆炸。每个导入逻辑都变成读Excel手工拼表头反射赋值的缝合怪新同事接手时根本看不懂哪些列对应哪些字段。1.2 模板填充和合并单元格动不动就乱串EasyExcel的fillAPI 适合做简单的列表填充但遇上“模板里既有固定字段又要动态生成合并单元格”的场景就很痛苦。我要生成一份按月统计的对账单模板第一行是公司名第二行是时间段第三行开始是数据列表。列表中间还带小计行小计行又要横向合并某些单元格。用EasyExcel做这个效果要么写一堆CellWriteHandler要么用LoopMergeStrategy自己去算合并范围。我试过几次合并行列数稍微一变导出的表格就“串行”数据和表头对不上或者在错误的位置多出几条合并线。最让人心累的是这类问题只会在真实数据下复现单元测试里用两条数据永远是好的。1.3 环境依赖和版本冲突一个比一个玄学如果说功能上的坑还能靠加班填那环境依赖的坑就是纯靠玄学。easyexcel libfreetype6这个问题在Linux服务器上尤其常见明明本地Windows跑得好好的部署到CentOS容器里就报缺库。EasyExcel底层依赖POIPOI的图形渲染部分会用到PoI的font相关原生库一旦基础镜像里没有libfreetype6导出带图形的Excel直接崩。我当时的解决方式是改Dockerfile装库但每次换镜像都得重新踩一遍。还有java.lang.NoSuchFieldError: factory这个更离谱。项目里某个中间件依赖了旧版POIEasyExcel引用了另一版POI类加载器一打架运行时就报这个错。我硬是靠排除依赖、强制指定版本才压下去而类似版本冲突问题在EasyExcel社区里一搜一大把几乎成了月经帖。1.4 嵌套List在模板里怎么都渲染不出来用EasyExcel做模板导出时如果数据模型里带一个ListObject按文档说明要在模板里写{.detail}这样的语法。但如果你要渲染的是“一个订单里包含多个商品明细”这种嵌套结构而且商品明细还想展开成多行EasyExcel的模板语法就开始力不从心。我试过{.skuList}、{.order.goodsList}甚至在模板里写循环标记结果要么只显示第一行要么直接报模板解析异常。最后只能用最原始的方式先把Excel模板按Sheet读出来用POI底层API逐行复制单元格一个小功能写了一百多行代码。这个经历让我彻底意识到EasyExcel的定位是“快速处理简单表格”而不是“成为Excel领域的通用内存模型”。2. Apache Fesod 到底做了什么不一样的设计接触Apache Fesod之前我以为它又是一个“换汤不换药”的POI封装。真正用下来才发现它在设计上走了一条和EasyExcel完全不同的路不强求兼容所有历史API而是重新定义了Excel导入导出的编程模型。我觉得有三个设计点特别值得聊。2.1 不搞自己的单元格模型直接站在POI肩膀上EasyExcel为了避免POI的复杂性自己维护了一套并行模型这虽然让简单读写变得很快但也带来两个问题一是无法完全覆盖POI的全部能力很多高级特性只能通过回调接口“打洞”二是有独立的版本依赖和项目里其他POI相关组件容易冲突。Apache Fesod的做法相反——它没有封装自己的内存表格模型而是直接以POI的Workbook、Sheet、Cell为底层载体在这个基础上增加声明式映射、校验、链式API等上层设施。这样一来它天然兼容POI所有能力。你可以在Fesod的读写流程里随时拿到原生POI对象做精细化操作不需要在“框架函数”和“原生能力”之间做取舍。从使用角度说这意味着你不需要担心“框架不支持某个POI特性”的问题。我之前在EasyExcel里需要绕路实现的单元格格式、批注、条件格式在Fesod里直接握着原生对象改就行心智负担小很多。2.2 从“读写操作”升级到“映射校验”数据模型是第一公民EasyExcel本身只是一个读写组件它不关心你导入的数据是否合法你自己要在业务层再做一遍校验。而Fesod把数据映射和校验前移到了解析阶段。它允许你在字段模型上声明非空、长度、正则、枚举值等规则读取Excel时边读边校验坏数据直接收集进错误列表不会中断整个导入流程。举个例子一个用户导入表手机号列、金额列、部门列都可能有脏数据。用EasyExcel的做法是先全部读进ListUserDTO再写一长串if (dto.getPhone() ! null dto.getPhone().length() ! 11)之类的逻辑。用Fesod的做法则是在模型字段上直接标注校验规则导入完成后自动得到一个成功的对象列表和一个失败原因列表。这个设计对后台管理系统尤其友好——前端只需要把错误文件一键回传提升了整个导入链路的产品体验。2.3 流式API设计让Excel逻辑变得更像普通Java代码Fesod的另一个直观感受是API风格很现代。它把读取、映射、校验、写出、模板填充、合并单元格这些操作都设计成了可链式调用的组件配合Java 8的Stream很多逻辑读起来像在读业务代码而不是在记框架API。关键示例是它的入口不叫EasyExcel.read()或EasyExcel.write()而是通过一个统一的Fesod门面创建读写器。以我的使用习惯来说这种门面式设计最大好处是“所有的可能性都从同一个入口展开”IDE自动补全时不会在十几个静态方法里迷失。我接触Fesod三十分钟就能上手写第一个导出比第一次用EasyExcel顺畅得多。3. 迁移实战我把项目从EasyExcel切到Apache Fesod理论说得再多不如实际操作来得实在。下面我把这次迁移的核心步骤按顺序列出来里面涉及的具体代码都来自我真实改造过的项目你可以直接参考。3.1 第一步替换依赖首先是Maven依赖的变化。原本项目里是dependency groupIdcom.alibaba/groupId artifactIdeasyexcel/artifactId version3.3.2/version /dependency替换成dependency groupIdorg.apache.fesod/groupId artifactIdfesod-core/artifactId version0.9.4/version /dependency这里必须提醒一点Fesod内置依赖的是新版POI如果你项目里其他组件引入了旧版POI一定要统一升级版本否则照样会遇到和EasyExcel时代一样的NoSuchMethodError。我的做法是在pom.xml的dependencyManagement里显式指定POI版本保证整个项目只有一个版本。3.2 第二步复杂表头导入重写我先拿最头疼的复杂表头开刀。改造前我的代码大概是这样的指定headRowNumber3然后在监听器里根据行号拼接表头字段再反射赋值。改造后Fesod允许我在模型上直接声明表头层级关系。假设要导入的Excel表头是三层目标结构是“区域-渠道-销售额”我先定义导入模型import org.apache.fesod.annotations.FesodColumn; import org.apache.fesod.annotations.FesodHead; import org.apache.fesod.annotations.FesodTable; FesodTable(sheet 销售数据, headRowNumber 3) public class SalesImportRow { FesodHead(group 区域, name 华东, columnIndex 0) private String eastRegion; FesodHead(group 区域, name 华南, columnIndex 1) private String southRegion; FesodColumn(name 线上销售额, columnIndex 2) private BigDecimal onlineAmount; FesodColumn(name 线下销售额, columnIndex 3) private BigDecimal offlineAmount; }然后导入代码简化为ListSalesImportRow rows Fesod.reader(inputStream) .sheet(销售数据) .header(3) .model(SalesImportRow.class) .validate() .read();这里有个细节要注意headRowNumber和FesodHead的group用来描述表头的层级逻辑真正物理上占几行以header(3)为准。如果Excel里的合并单元格跨行但不规则你可能会遇到表头解析后部分字段取不到值的情况。我的解决方式是先打印一行Fesod.headErrors()的返回结果确认是哪一列解析不匹配再调整columnIndex。整体来说这个改造把过去两百多行的表头拼装逻辑压缩成了不到二十行且错误信息明确得多。3.3 第三步模板填充与合并单元格重写模板填充是这次迁移里收益最大的一步。改造前用EasyExcel的fill做动态合并单元格我写了一堆CellWriteHandler每次数据行数变化合并区域就乱套。Fesod提供了一种“模板占位符自动合并声明”的方式。我在Excel模板里给数据区域起了一个动态表名salesRows然后在代码里声明这个区域的行范围和合并规则Fesod.writer() .template(classpath:template/sales_report_template.xlsx) .sheet(报告) .list(salesRows, salesDataList) .merged(0, 0, salesDataList.size() 3, 0) // 第一列按行合并 .merged(0, 1, salesDataList.size() 3, 1) // 第二列按行合并 .build() .write(outputStream);这段代码的意思很直接salesRows是模板里的数据占位符merged参数前两个是起始行列后两个是结束行列表示这块区域在渲染完数据后要执行合并。它之所以比EasyExcel靠谱是因为合并操作是基于“最终渲染出的数据范围”来计算的而不是基于预估行数。数据行数越多salesDataList.size()3越大合并范围自动跟着扩展。我得坦白说刚开始我也担心这个API会不会有边界问题后来测了500行、5000行的数据合并结果都稳定。唯一要留神的是模板里的行号占位。如果模板里还有其他要和动态数据区域保持左右对齐的静态列一定要把静态列也写在同一个list()声明的后面否则无法保证同步扩展行高。我的建议是先做一个最小例子只渲染一行数据确认占位符位置正确再上真实数据。3.4 第四步嵌套List模板渲染重写嵌套List曾是压垮我耐心的最后一根稻草。Fesod解决这个问题的思路不太一样它支持在模板里用类似#foreach的标记定义循环区域然后把嵌套对象直接作为循环项传递。以一个“订单商品明细”的模板为例我在模板里这样写订单号区域${order.orderNo}商品明细表头商品名、数量、单价、小计明细行区域首行#foreach goods in order.goodsList明细行内部${goods.name}、${goods.quantity}、${goods.price}、${goods.subtotal}明细行区域尾行#end代码里不需要手动渲染子列表直接传整个订单对象OrderDTO order new OrderDTO(); order.setOrderNo(SO2025001); order.setGoodsList(Arrays.asList( new GoodsDTO(手机, 2, new BigDecimal(2999.00)), new GoodsDTO(耳机, 1, new BigDecimal(499.00)) )); Fesod.writer() .template(classpath:template/order_template.xlsx) .sheet(订单) .bind(order, order) .build() .write(outputStream);这段代码最妙的地方是你完全不用关心Excel里要写多少行Fesod会根据goodsList.size()自动扩展模板中的循环区域。对比我之前的POI手工复制行代码可维护性提升了不止一个档次。如果你之前用EasyExcel没法渲染嵌套List可以试试这个方案。它本质上是把模板中的循环语法交给了Fesod自己的模板引擎解析而不是依赖POI的底层API。4. 迁移路上的常见问题排查与避坑清单任何工具迁移都不会一帆风顺Apache Fesod也不例外。我整理了一份自己在迁移中遇到的典型问题清单按“问题现象-原因分析-处理方式”的格式写出来方便你对照排查。问题现象原因分析处理方式导入时某些列的值一直是null表头物理行数多于声明行数或者列索引对应错误检查header(n)是否和真实表头行数一致用headErrors()查看解析结果模板填充后行高/列宽异常模板中循环区域行高设置不一致在模板里先设置好循环行的默认行高Fesod复制行时会保留原行样式合并单元格在数据少时出现空行合并merged()参数计算有误结束行写到了空白区域打印最终渲染行数确保结束行等于最后一条数据所在行启动时出现NoSuchMethodErrorPOI版本与其他组件冲突在dependencyManagement中统一POI版本重启服务验证嵌套List只渲染第一条模板中缺少#foreach结束标记或者字段名拼写错误检查模板循环区域是否有#end再核对字段名和getter读取大文件时内存占用高默认流式开关未开启在Fesod.reader()上显式调用.stream()开启流式读取或开启并发解析导出内容出现乱码输出流Content-Type或字符集设置错误设置响应头Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet文件名做URL编码除了表格里的问题还有一个特别值得注意的经验迁移过程中一定要保留原有的Excel文件作为回归测试样本。我把EasyExcel时代积累的十几份“有毒”Excel文件全部拿来跑Fesod包括多级表头、合并单元格、空行、超长文本。这样比写一百个单元测试都有效因为真实用户的Excel永远不会按文档规范来。我还遇到过一个小误区Fesod的.validate()默认会校验字段的非空约束但如果你某个字段允许为空一定要在模型上加FesodNullable否则导入会意外产生大量错误行。这个设计一开始让我误以为框架有bug实际是我没读文档。实现校验逻辑时也建议先只启用非空校验等稳定后再逐步增加格式校验否则会被一堆历史数据错误刷屏。另外Excel单元格换行这块也值得一提。不管是EasyExcel还是Fesod单元格里的换行都靠\n加上单元格格式的WrapText属性。Fesod对带换行的文本处理比EasyExcel更直接导出时你只要在字段值里正常放\n并在模板或FesodColumn上设置wrap true就能得到正确的多行单元格效果。之前用EasyExcel时我遇到过换行符被自动吞掉的情况得手动替换成\r\n才正常Fesod这边倒没再出现。5. 迁移之后重新审视技术选型到现在Fesod在我的项目里运行了接近两个月稳定处理了线上数千次导入导出任务。我自己的体会是技术选型这件事看得不是谁名气大、谁用的人多而是谁更匹配你当前场景的复杂程度。EasyExcel在简单表格处理上确实轻快但一旦业务开始触及复杂表头、嵌套模板、字段校验它的抽象层级反而成了限制。Apache Fesod给我的核心价值是它在POI之上提供了一个“更高但不隔断”的编程模型我既可以用声明式注解快速完成常规读写又能在需要时下沉到原生POI对象做精细控制。最后再分享一个小技巧无论你最终选择EasyExcel、Apache Fesod还是直接写POI都建议给导入导出模块单独抽一个基础能力层把表头声明、模板位置、字段映射、错误收集这些配置统一管理起来而不是散落在各个Service里。我这次重构最大的收获不在于换了框架而是借这次机会把原来零散的Excel处理代码收拢成了一个有结构、可测试、可替换的模块。将来即使Fesod也出现维护停滞我换下一个框架的成本也会低得多。Excel处理这条路没有银弹但保持模块边界清晰你永远有从容选择的底气。
返回列表