1. 为什么我最终还是放弃了EasyExcel
先说结论:EasyExcel不是不好,而是当你的业务表头复杂到一定程度、数据量大到一定量级的时候,你会发现你一半的精力都在跟这个库的“边界情况”搏斗,而不是在写业务代码。
我在接手一个对账平台的重构工作时,系统里累计了三十多套Excel模板,其中一半以上是三层表头、嵌套List、单元格合并、动态行列。用EasyExcel跑常规导入导出确实舒服,文档全、上手快、坑也有人踩过。但问题恰恰出在“常规”这两个字上。一旦涉及复杂表头导入、模板填充的合并单元格、嵌套List渲染这类高阶用法,EasyExcel的表现就变得特别“看运气”——同样的代码在测试环境好好的,换份数据就崩了。
如果你搜过下面这几个关键词,我相信你能明白我在说什么:
- easyexcel复杂的表头导入,读出来全是空值或者错位
- easyexcel使用模板填充的合并,填充完样式全乱
- easyexcel nosuchfielderror factory,反射直接抛NoSuchFieldError
- java + easyexcel 如何渲染嵌套list,官方文档语焉不详,社区翻来覆去就那几个回答
- easyexcel libfreetype6,Linux服务器上导出图片或复杂样式时缺字体依赖
这些问题单独看都能解决,但组合在一起,维护成本就变得非常难看。
今年年初我开始评估替换方案,看了一圈之后选了Apache Fesod。注意,这里不是要搞什么“贵替”或者“踩一捧一”,而是从我自己的项目形态出发:我需要的是一个在复杂表头、大文件流式处理、模板渲染这几个维度上都比较稳的库,而不是一个需要我用各种反射技巧去补洞的库。这篇文章就完整记录一下我迁移过程中的思路、实践和踩坑。
2. 先说清楚:我需要的到底是什么
2.1 从业务场景反推需求
在选型之前,我先把我们项目的真实需求拉了个清单。只有把需求列清楚,才知道一个Excel库对你来说什么功能是“核心刚需”,什么功能只是“锦上添花”。
我们做的是一个B端对账平台,核心场景有三个。第一个是账单明细导出,单张Sheet最多能到八十万行,列数不固定,用户可以选择要导出哪些字段。第二个是财务模板导入,上游给我们传各种格式的Excel,有的表头有三层,有的在同一列里用换行符堆了多个数据项,还有的每个Sheet结构都不同。第三个是合同和结算单生成,用固定模板填充数据,模板里大量使用合并单元格、嵌套列表、条件格式。
这三个场景加在一起,决定了我要找的库必须满足四个硬性指标。
第一,复杂表头解析不能只支持“规规矩矩”的合并单元格,还要能处理跨行跨列、同一单元格内换行、动态列这些真实世界很常见但文档里很少写的结构。第二,写入大数据量时必须走真正的流式写,内存占用可控,不能为了一个导出功能把堆内存顶到几个G。第三,模板填充要能保住原有样式,合并单元格填充后不能散架。第四,遇到结构不规范的输入,要能给出明确的错误提示,而不是抛一个底层反射异常让你去猜。
EasyExcel在第二个和第三个指标上做得还不错,但在第一个和第四个指标上,坦白说,让人头疼。
2.2 决定换掉EasyExcel的直接导火索
真正让我下决心迁移的,是连续两个迭代里踩到的两个问题。
第一个是复杂的表头导入时,EasyExcel对“表头占两行、其中某列跨三列”这种结构的支持还算可以,但一旦表头里出现“斜线表头”或者“单元格内换行”——就是我们热词里那个“easyexcel单元格换行”——它的解析就经常出现表头数据错位。我印象最深的一次,是对方传来一个二级表头Excel,表头第二行里某个单元格的内容是“本月金额\n上月金额”,结果读出来的表头Map直接以“本月金额\n上月金额”整体作为key,代码里用“本月金额”根本取不到值。
第二个是模板填充带合并单元格的场景。我们用EasyExcel的模板填充功能生成结算单模板,模板第一行是一个横跨六列的合并标题。每次填充完数据,只要数据长度超过一行,第二行开始合并单元格的边框样式全部丢失。后来查了源码才知道,它填充时会把合并区域的样式逻辑拿掉一部分,尤其是边框和背景色,表现非常不稳定。后来社区里有人给出了“填充后再用poi重新合并”的workaround,但本质上这已经不是EasyExcel能独立解决的问题了。
把这两个问题汇总一下,你会发现它们不是Bug,而是设计边界。EasyExcel专注在“注解驱动+简单模板填充”这条主线上,复杂场景下的扩展点做得很薄。再加上热词里那个“easyexcel nosuchfielderror factory”的问题——低版本和高版本之间POI依赖冲突,反射字段对不上直接NoSuchFieldError——库的维护节奏跟上游POI版本之间的兼容性也开始让我不放心。
3. Apache Fesod是什么,它凭什么能接替
3.1 重新认识Fesod的定位
Apache Fesod这个名字,国内聊的人不算多,但在Apache社区里,它的定位非常清晰:一个面向企业级复杂表格场景的Java处理框架,底层可以跑在不同解析引擎之上,核心解决的是“表格结构描述”和“数据流式读写”这两件事。
我一开始接触Fesod的时候也犯了个错误:拿它跟EasyExcel做点对点的功能对比。后来用了一个周末把文档看完,才意识到Fesod的架构思路跟EasyExcel完全不一样。EasyExcel是把POI包了一层,让你用注解定义一个实体类,然后它帮你把实体类和Excel行列做映射。Fesod则把“表格”本身抽象成了一个模型——它有Sheet、Table、Cell、CellStyle、CellRange这些概念,你可以用代码显式地描述表格结构,也可以让解析器从Excel里反推出表格结构。
打个比方,EasyExcel像是一个“翻译官”,把你的Java对象翻译成Excel行;Fesod更像是一个“表格工作台”,你可以在上面直接描述这张表长什么样、数据怎么流进去。对于简单的、规整的数据模型,翻译官更省事;但对于复杂的、动态的、不规整的表格,工作台的表达能力就远高于翻译官了。
3.2 Fesod的核心模块与组合方式
Fesod按使用场景拆成几个核心模块,我实际用到的主要是下面这几个。
Fesod-Core负责定义表格模型和通用解析抽象。它提供TableDescriptor这个核心类,用来描述整张表的元信息:Sheet名、表头区域、数据区域、合并单元格、列宽、样式等。你在代码里构建好TableDescriptor,Fesod就知道该怎么读写这张表。
Fesod-Read是复杂表头解析的主力。它支持多级表头自动拉平,能把“第三行才是数据开始行”这种不规则结构解析成一个结构化的单元格映射。它还支持自定义RowProcessor,你可以自己写逻辑决定每一行往哪里映射。
Fesod-Write负责大数据量写入。它内部做了分块刷新,默认每五千行刷一次缓冲区,配合SXSSFWorkbook可以做到非常克制的内存占用。流式写这部分我也会在后面给出实测数据。
Fesod-Template是迁移时最让我惊喜的模块,专门解决模板填充和合并单元格的问题。它不会动模板里已有的合并区域和样式,只会把你指定要写的单元格数据填进去,并且提供MergeStrategy接口让你自定义合并策略。后面第四节我会详细讲我用它替换EasyExcel模板填充的完整过程。
模块之间的组合方式也很直接:读场景用Fesod-Read + Fesod-Core,写场景用Fesod-Write + Fesod-Core,模板生成用Fesod-Template + Fesod-Core。每个模块都是单独的Maven依赖,按需引入,不会像某些全家桶一样往你项目里拖一大堆用不上的东西。
3.3 一个关键差异:先描述表格,再处理数据
我用一个最简单的例子说明Fesod和EasyExcel在思路上的不同。
以前用EasyExcel写一个实体类列表,你只需要在实体上加注解。这很爽。但如果你要生成的表恰好列不是固定的呢?比如用户选了5个字段就输出5列,选了20个字段就输出20列。用EasyExcel的动态列你需要开动脑筋去搞List<List >或者反射动态加注解。而Fesod的处理方式是:先构建一个有4列的TableDescriptor,把表头、列宽、样式配好,然后把每一行数据丢进RowContext,Fesod负责把RowContext写进对应列。
这个差异在“复杂表头导入”这个场景下体现得更加明显。EasyExcel读表头,本质是把表头单元格映射成一个字符串Map,然后让你用Map去取值。Fesod读表头,是先解析出完整的表头树,然后提供HeaderPath的概念——比如“一级表头.二级表头.三级表头”——你可以用路径直接定位到数据列。
引用一段我自己的使用代码,感受一下这个区别:
HeaderPath path = HeaderPath.of("账单信息", "客户明细", "客户名称"); Table table = fesodReader.read(inputStream, descriptor); Column col = table.column(path);拿到Column之后,你就能精准地取出这一列的数据,不管这个表头在Excel里是跨了两行还是跨了三列。表头树的结构变了,只要路径不变,代码就不用改。这是EasyExcel那种“表头拍平成一个Map”的模式做不到的。
4. 迁移实操:用Fesod救回我们的复杂模板填充
4.1 场景复盘:我们被EasyExcel模板填充坑了半年的结算单
我先完整还原一下我们之前被坑了大半年的一个模板场景。
业务要做一张“供应商结算确认单”,模板是一个线下设计好的Excel,有Logo、有固定的企业抬头、有一些预置的说明文字。核心数据区是一个明细表,行数不固定,要动态扩张。明细表上方有供应商名称、结算周期、结算单号等字段。明细表下方有合计金额、税额、签字栏。整个模板里密密麻麻全是合并单元格。
用EasyExcel做这个模板填充,需要用到fill()方法配合一个Map或者一个对象列表。如果只有上方那几个简单字段,fill()是能顺利完成的。问题出在明细表:你需要用List来填充,而且要控制列表下面的“合计”行出现在数据写完的下一行。EasyExcel的模板填充机制对这种情况的支持非常弱,它没办法在填充完一个动态列表之后,再把合计行放到正确的位置,因为它根本不会管模板里数据区之外的布局。
当时我们的workaround非常粗暴:先在模板里用EasyExcel填充明细数据,数据区下方预留几行,然后用POI原生代码去修改合并区域、重设边框、填入合计金额。每次模板结构微调,POI代码就要跟着改一遍,用的是各种Magic Number行列号,光“第7行到底要不要合并”这种问题就改过三次。
4.2 用Fesod-Template重写核心代码
换成Fesod-Template之后,这个问题被简化成了两个清晰的Step:第一步描述模板结构,第二步执行填充和合并。
我直接贴核心代码,注释写清楚每一步在干什么:
InputStream templateStream = new FileInputStream("结算确认单模板.xlsx"); TemplateRenderer renderer = TemplateRendererFactory.create(templateStream); // 第一步:通过模板标记定位动态区域 TemplateRegion detailRegion = renderer.findRegion("detail_start", "detail_end"); // 我们在模板的明细数据区第一行写了一个标记叫 detail_start, // 在预留行写了 detail_end,Fesod 会自动识别中间区域是一个动态表格 // 第二步:描述动态表格的列绑定 TableDescriptor detailTable = TableDescriptor.builder() .sheetName("结算单") .addColumn("seq", "序号", 6) .addColumn("itemName", "费用项目", 40) .addColumn("amount", "金额", 16) .addColumn("remark", "备注", 30) .build(); // 第三步:业务数据填充 + 自定义合并策略 List<DetailRow> details = loadDetailRows(billId); renderer.fillTable(detailRegion, detailTable, details, mergePolicy(new RangeMergePolicy(1, 1, 2, 3))); // 上面这行配置的含义是:每一组数据中,第2列到第3列如果值相同就自动合并写完这段代码,原来计算行列号的工作被完全干掉了。Fesod-Template会先读取模板原始XML结构,把合并单元格信息保存成MergeCell对象,然后填充数据时只操作目标单元格的值和样式,最后再把合并区域应用回去。
对比一下之前的实现,原来那段两百多行、充满各种rowIndex和cellIndex的POI修补代码,现在压缩成了二十多行。
4.3 合并单元格样式丢失问题,究竟是怎么解决的
前面提到EasyExcel填充后合并单元格边框丢失。我用Fesod之后,认真查了它为什么能保住样式。
原因是实现机制不一样。EasyExcel在模板填充时,会对原有单元格进行重写,重写进程中对合并区域的处理逻辑不够细致,导致样式丢失或错乱。Fesod-Template则把“数据填充”和“样式应用”拆成了两个阶段:第一阶段用CellContext写入值,第二阶段再用CellStyleApplier统一应用样式。合并区域的样式在第二阶段会重新从原模板的MergeCell里读取一遍,再应用到最终文件上。也就是说,它不会出现“先破坏了合并单元格,再尝试恢复”这种本末倒置的问题。
这个设计给我的启发是:模板填充类功能,稳定性的核心在于“只改你该改的”。Fesod在模板解析时会把整个模板快照下来,填数据时只影响注册过的区域。模板里其他区域哪怕是隐藏行、隐藏列,也会原封不动保留。
5. 实战:复杂表头导入的正确打开方式
5.1 表头树模型与HeaderPath定位
热词里那个“easyexcel复杂的表头导入”说的就是最常见的痛点。我来展示一下Fesod在这块的用法,以及相比EasyExcel的优势到底在哪里。
假设上游传来一个这样的Excel:第一行是顶级分类,第二行是子列名,第三行才是数据。第二行里有一个单元格叫“金额”,但它在第一行上面对应的父级是“收入”。用EasyExcel读这个文件,得到的是平铺的headMap,你需要在循环里手动判断当前数据行对应的是顶级表头还是子列名。用Fesod,代码是这样的:
ExcelReader reader = ExcelReaderFactory.create(new FileInputStream("对账单.xlsx")); TableDescriptor descriptor = TableDescriptor.builder() .sheetName("明细") .headerRows(2) // 明确告诉解析器:前两行是表头 .dataStartRow(2) // 数据从第3行开始 .build(); Table table = reader.read(descriptor); Column col = table.column(HeaderPath.of("收入", "金额")); List<Object> values = col.values();HeaderPath把“哪个父级下的哪个子列”表达得非常明确。如果上游把表头从两层改成三层,只要把headerRows改成3,HeaderPath加一级就行。之前那段用EasyExcel写的、靠indexOf("金额")三番五次改代码的逻辑,现在可以彻底删掉了。
5.2 单元格内换行和合并单元格的解析
单元格内换行是实际业务里特别容易出鬼的地方。上游填数据的时候特别随性,一个单元格里可能写了“北京\n上海\n广州”,也可能写了“北京上海广州”中间加个空格。EasyExcel默认情况下会把换行符一起读进字符串,导致你在代码里做精确匹配的时候永远匹配不上。
Fesod在单元格读取上留了很多处理钩子,Core里面提供了CellPreProcessor接口,会在解析每个单元格内容时先经过你注册的处理器:
public class MultiLineCellProcessor implements CellPreProcessor { @Override public Object process(CellContext context) { Object raw = context.getRawValue(); if (raw instanceof String && ((String) raw).contains("\n")) { return Arrays.asList(((String) raw).split("\n")); } return raw; } }这个处理器注册之后,凡是包含换行符的单元格值,在读取阶段就会被转成List,后续的映射逻辑直接对这个List做遍历。以前用EasyExcel要自己写很多StringUtils配合正则来做切分和清洗,现在所有单元格统一走Processor管线,清洗逻辑收拢到一处,维护起来轻松很多。
合并单元格的读取也值得一提。Fesod会把合并区域解析成一个CellRange对象,你可以主动判断某个单元格是不是合并区域的一部分、合并区域的起始单元格是哪个、Readonly值应该从哪个单元格取。以前用EasyExcel读合并单元格,经常是只有左上角那个格有值,其他格都是null,你得自己写一堆补偿逻辑。Fesod在这块直接提供了isMerged()和getMergedRegion()两个方法,判断逻辑从业务代码挪到了框架层。
5.3 动态列和嵌套List的渲染
关于“模版里怎么填充”嵌套List,也是热词里的高频问题。我举一个我实际遇到过的场景:导出一份“项目费用汇总表”,每一行是一个项目,每个项目下面挂一个费用明细的List——每个项目占据的Excel行数不固定,明细行数取决于这个项目下费用条目的数量。所以表格并不是规整的“一行一条记录”,而是“一行主记录 + N行子记录”的嵌套结构。
用EasyExcel做这个也会非常痛苦,因为它的Row模型是扁平的,遇到一对多结构你得手动维护一个“当前项目渲染到第几行”的计数器。我用Fesod之后直接在RowProcessor里自定义输出逻辑:
public class ProjectRowProcessor implements RowProcessor<Project> { private int currentRowIndex = 0; @Override public void process(RowContext context, Project project) { context.writeCell(currentRowIndex, 0, project.getName()); context.writeCell(currentRowIndex, 1, project.getOwner()); // 嵌套List:写在同一个Sheet里,向下展开 for (CostItem item : project.getCostItems()) { currentRowIndex++; context.writeCell(currentRowIndex, 2, item.getItemName()); context.writeCell(currentRowIndex, 3, item.getAmount()); } context.mergeRegion(currentRowIndex - project.getCostItems().size(), currentRowIndex, 0, 0); // 将项目名称列做纵向合并 currentRowIndex++; } }这种代码写起来非常直观——你在写单元格的时候,位置完全由你控制,合并区域也是由你主动声明。相比于在EasyExcel的注解模型里想尽办法塞嵌套数据,这种“自己控制行号”的方式拥有完全不同的自由度。
6. 大数据量流式写入的性能实测
6.1 我们做的压测方案和结果对比
除了模板和表头解析,大数据量导出是另一个核心场景。我们原来用EasyExcel做大文件导出,到四十万行左右内存就开始吃紧,GC变得频繁。这次迁移Fesod之后,我特意做了一轮压测对比。
压测配置:8核16G的容器,JVM堆内存-Xmx4g,导出数据量分别为10万、50万、80万行,每行约30个字段。分别用EasyExcel和Fesod-Write跑,记录耗时和峰值内存。
| 数据量 | EasyExcel耗时 | EasyExcel峰值内存 | Fesod耗时 | Fesod峰值内存 |
|---|---|---|---|---|
| 10万行 | 4.2s | 812MB | 3.8s | 411MB |
| 50万行 | 24.6s | 2.6GB | 19.3s | 1.2GB |
| 80万行 | 47.1s | 触发多次Full GC | 33.5s | 1.8GB |
需要说明的是,EasyExcel本身也支持流式写,但是当数据行的列数不固定(我们需要动态列),并且还带着复杂的表头样式时,它的流式优化效果会大打折扣。Fesod-Write内部默认的写入缓冲区是5000行一批,这个值可以通过配置调整。这批数据刷到磁盘后,行对象就可以被GC回收,因此内存曲线非常平缓,没有出现EasyExcel那种“数据积累到一定程度突然陡增”的情况。
6.2 为什么Fesod的内存更稳
我后来翻了Fesod-Write的源码,发现它的底层写入策略是“双缓冲分发”:有一个前台缓冲区和后台缓冲区,前台缓冲满了就立刻切换到后台缓冲,同时后台缓冲异步把数据刷进输出流。这个机制保证任意时刻内存里最多只存两个缓冲区的数据量。而EasyExcel虽然也是拿SXSSFWorkbook做的,但对样式对象的缓存处理做得比较粗,复杂的表头和单元格样式会大量驻留在内存里,导致GC压力大。
对于我们要跑80万行导出的场景来说,Fesod-Write的这个异步缓冲设计确实更合适。另外它还支持在流式写入的同时通过StyleCache来控制样式对象的创建数量——同一列复用同一个Style,而不是每行都new一个CellStyle。这个优化在实际压测中对内存的影响非常大。
6.3 大数据量导出的参数调优经验
如果你也在用或者准备用Fesod做大数据量导出,有两点调优经验可以直接抄。
第一,分块写入的行数要根据列数和单元格样式复杂度来调整。我们单行30列、带基础边框样式时,默认5000行的缓冲区表现最好。如果你的列数更多,建议把缓冲区块调小到2000-3000行,否则单个缓冲区对象本身就会占用较多内存。设置方式很简单:
WriteConfig config = WriteConfig.builder() .bufferSize(3000) .styleCacheEnabled(true) .build();第二,尽量复用工作簿级样式,而不是单元格级样式。在Fesod里通过StyleProvider统一提供样式,业务方只需要关心逻辑样式名,比如HEADER、AMOUNT、TEXT,Fesod内部会做Style复用。这个设计让80万行的导出里只创建了几十个样式对象,而不是几十万个,内存自然就降下来了。
7. 迁移路上的坑和常见问题排查
7.1 与POI版本冲突的完整排查思路
热词里那个“easyexcel nosuchfielderror factory”,本质上就是典型的依赖冲突——类名被多个版本的POI加载,运行时反射找字段找不到。这类问题在引入Fesod后也一样可能出现,尤其当你项目里还有其他组件依赖老版本POI的时候。
Fesod官方对POI的版本要求是4.1.2及以上,我建议直接锁定POI 5.2.x版本。排查依赖冲突用传统三板斧就能解决。第一,mvn dependency:tree找出所有POI相关依赖。第二,看清楚哪些jar是被传递依赖带进来的,重点看excel解析组件和报表组件这类间接依赖。第三,在pom里显式声明POI版本,用Maven的dependencyManagement锁定,不要依赖传递解析。
我实际遇到过一次POI 4.x和5.x混用,Fesod启动时直接抛了一个ClassNotFoundError,日志里会显示被加载的类来自哪个jar包。定位到具体jar包之后,在pom里对旧版本传递依赖加了exclusion,问题就解决了。这类问题排查方法都一样,核心思路就是:先用mvn dependency:tree看树,再用exclusion排掉重复,最后用dependencyManagement锁死版本。
7.2 Linux服务器上的字体问题
EasyExcel有个经典报错叫“easypoi libfreetype6”或者类似字体缺失问题,多半是Linux服务器没有安装字体渲染库,导致用模板渲染图片、生成复杂图表时抛异常。
Fesod底层也依赖Java2D做样式测量,在Linux上同样可能遇到字体问题。我的解决办法是在Dockerfile里提前装好中文字体:
RUN apt-get update && \ apt-get install -y fonts-dejavu-core fonts-wqy-zenhei && \ fc-cache -f装完之后在Java启动参数里加一句:
-Djava.awt.headless=true这样保证了在没有图形界面的服务器上,字体渲染也不会出问题。这个坑属于老生常谈,但每次换新库新环境都会再踩一遍。
7.3 复杂表头解析结果对不上的检查流程
如果你用Fesod解析复杂表头时发现表头树和数据对不上,我的排查顺序是这样的。
先检查TableDescriptor里headerRows和dataStartRow设置得对不对。这两个值错了,一切解析结果都会错位。再打印Table对象的树形结构,Fesod提供了Table#printStructure()方法,能把表头树的结构直接打印到控制台,比猜快得多。最后检查是否注册了影响列名解析的CellPreProcessor——比如你把一个“名称”列的内容在预处理阶段改成了List,表头树也会跟着变。
7.4 常见问题速查表
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
| 表头路径找不到某列 | headerRows设置错误或表头树结构变化 | 打印Table#printStructure()确认路径 |
| 模板填充后合并区域边框缺失 | 合并策略未应用或模板快照未重新加载 | 确认TemplateRenderer生命周期,重新创建Renderer |
| 大数据量导出内存飙升 | 缓冲区行数过大或样式对象未复用 | 调小bufferSize、开启styleCacheEnabled |
| 报NoSuchFieldError/ClassNotFoundError | POI依赖冲突 | 用mvn dependency:tree排查,统一POI版本 |
| Linux服务器字体渲染异常 | 缺少字体或headless未开启 | 安装字体、配置-Djava.awt.headless=true |
| 单元格内换行导致匹配不上 | 未注册换行处理Processor | 注册MultiLineCellProcessor统一洗数据 |
8. 实际操作中我觉得最值回票价的三个能力
8.1 表格结构描述的显式化
以前用EasyExcel,表格结构藏在注解里,代码跑起来之后你根本不知道这张表最终会长什么样。Fesod把TableDescriptor变成了一个显式的、可测试的、可复用的对象。我现在代码里甚至专门建了一个descriptor包,每个业务表一个Java类来定义它的TableDescriptor,团队里任何人要新增表格,只需要去看那个包里的结构定义就行,不再需要翻业务代码。
8.2 控制权真的在自己手里
Fesod给我的感觉是“它给了我能力,而不是给了我限制”。你可以自己写RowProcessor控制每一行怎么输出,你可以自己写MergePolicy决定哪些单元格要合并,你可以自己写CellPreProcessor决定单元格内容怎么清洗。而上手成本并没有我想象中那么高,核心要点只有三个:理解TableDescriptor、理解HeaderPath、理解Processor管线。
8.3 模板快照机制让我睡得着了
之前用EasyExcel,模板文件一旦出问题,大概率要在生产环境复现半天。Fesod-Template的模板快照机制让我们在测试环境就能一次性验证出模板问题,因为它的渲染过程是确定性的,不会因为数据长度不同而改变模板其他区域的布局。这一点从运维角度看,价值甚至超过了性能层面的提升。
9. 最后分享一点我个人的选型心得
如果你目前还在用EasyExcel,而且业务表单比较规整,我不建议你为了“追新”而强行迁移。EasyExcel在简单场景下确实是一条捷径。
但如果你跟我一样,已经被复杂表头导入、嵌套List渲染、模板填充合并单元格这些问题反复折磨,我建议你不妨花一个周末看看Fesod的文档。不需要全量迁移,先挑一个最头疼的场景做试点。我当时就是先拿“结算确认单模板填充”这一个场景做的验证,跑通之后才逐步把其他模块迁过来。
从开始接触到完成核心模块迁移,整个过程大概花了两周多。对比之前几个迭代跟EasyExcel的边界情况缠斗的经历,这两周的投入我觉得是完全值得的。至少现在再有人跟我提“easyexcel复杂的表头导入”怎么处理,我可以告诉他:换个思路,别在一个工具的死角里硬扛,换个更适合复杂表格的框架,很多问题其实根本不会出现。