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

资讯详情

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

从EasyExcel到Apache Fesod:Java复杂表头与POI冲突的解决实践

从EasyExcel到Apache Fesod:Java复杂表头与POI冲突的解决实践 1. 我为什么跟EasyExcel说再见从一次双表头导入事故说起这篇文章不是标题党。上个月我花了整整两天从一个由EasyExcel处理的复杂表头导入需求里爬出来。两天里翻了大量issue区把看板也一遍遍读过去最后还是换了实现。写这篇记录不为了拉踩任何库只想给那些和我一样在Java里和Excel相爱相杀的同行一个参考。我先说结论EasyExcel在常规字段映射的导入导出上确实好用但当你开始碰复杂表头、嵌套列表、模板合并填充它的设计假设就会反过来咬你一口。于是我把目光转向了Apache Fesod。1.1 复杂表头导入从“对象映射”到“表头树”做Java的都知道EasyExcel最舒服的姿势是建一个实体类字段上加ExcelProperty注解导入时按列顺序或者按表头名映射。这里有一个非常隐蔽的前提表头是扁平的一列对应一个字段。只要出现两层表头或者某一列在第二层又拆成了多个子列EasyExcel的标准映射就基本废了。实际项目里我遇到的是一个采购订单导入文件第一层有“商品信息”“金额信息”两个大分组第二层才是“商品编码”“商品名称”“数量”“单价”“含税金额”这些明细列。EasyExcel会怎么处理它把表头行当作普通行让你用headRowNumber跳过然后靠坐标去手动拼。听起来不难但一旦表头里的合并单元格位置稍有变化坐标就全部乱掉。我写了个AnalysisEventListener硬是维护了一个“当前位于哪个顶层分组”的状态机处理到“金额信息”分组切换时还得分两套逻辑代码丑到不敢回看。Fesod给我的第一个惊喜是它把表头描述成一个HeaderTree。比如两层表头先声明每个节点在第几行、跨几列再声明子节点解析时Fesod会按树结构把单元格值自动收拢到嵌套对象里。这个“表头模型”先于“单元格坐标”建立业务上只要保证表头文案不变哪怕合并位置微调导入逻辑也不受影响。对比下来EasyExcel更像是“按列号粗暴消费”而Fesod是“先建立Excel表头语义再按语义映射”。1.2 NoSuchFieldError factory与POI版本纠缠不是你的代码错了真正让我下定决心走人的是那个深夜出现在测试环境的NoSuchFieldError: factory。当时项目里除了EasyExcel还引了另一个报表组件它传递依赖了更高版本的Apache POI。众所周知EasyExcel本身就是基于POI做的二次封装它对POI版本有一套自己验证过的组合。一旦你的工程里同时出现多个POI版本字节码层面的字段缺失问题就来了。那次报错最终定位到某个类在编译期引用的factory字段在高版本POI里已经重构掉了运行时JVM按全限定名去找找不到就直接抛NoSuchFieldError。这个错最坑人的地方在于报错行根本不指向你的业务代码看起来完全像“天外来错”。排查半天还是靠mvn dependency:tree把POI的传递链全部拉出来再通过exclusion清理掉多余版本才消停。EasyExcel把POI当内部依赖却给不了你一个隔离环境所有版本冲突都暴露在同一个类加载器下。Fesod在这一点的处理更狠它默认依赖一个重新打包过类的POI产物把所有org.apache.poi包名改为内部隔离包名应用项目里再引其他POI版本也不会跟Fesod冲突。这个设计相当于给你的Excel能力套了一个独立沙箱虽然不至于说从此天下太平但至少NoSuchFieldError这一类问题从机制上被杜绝了。1.3 libfreetype6这个坑无头Linux容器里的字体依赖如果你只在Windows或者Mac本地跑EasyExcel可能永远撞不上这个坑。但生产环境一上Docker问题就来了。EasyExcel某些功能比如自动列宽、导出图片、带样式的渲染底层会调用AWT的字体处理逻辑而这个逻辑需要操作系统的字体库。容器里没有装libfreetype6的时候程序会在某个毫无预警的位置抛出和FreeType相关的UnsatisfiedLinkError报错信息里甚至会出现“freetype library not found”这样的字样。我的处理方式也很粗暴在Dockerfile里加apt-get install -y libfreetype6 fontconfig每次构建镜像都带着一堆没必要的系统依赖。这在安全审计时也会被问到“为什么你们的Java应用还需要安装字体库”。实际上我们的业务根本不需要生成图片仅仅是因为EasyExcel的某些路径绕不开字体引擎。Fesod的架构没有把字体渲染放在核心路径上。它对文本宽度的估算、单元格换行的计算都是基于Unicode码位范围和默认字符宽度做的数学估算而不是真的调用系统字体去测宽。只有在你明确开启图片导出或者富文本渲染时才需要额外的字体环境。这一点看起来很小但对容器化部署来说直接少了一整类系统依赖的维护成本。2. 初识Apache Fesod它凭什么解决EasyExcel没做好的事第一次听到“Apache Fesod”这三个字母组合很多人心里都会犯嘀咕这是Apache官方的新项目吗老实说它目前还在孵化阶段Changelog和文档都还在快速更新1.0正式版还没发布。但代码设计和API风格已经相对稳定我这两周基于0.9.x版本做的各种实验基本没有因为接口变动返工过。接下来聊聊它和EasyExcel在设计和实现上的三个关键差异。2.1 从“解析到模型”到“模型驱动解析”的转变EasyExcel的核心思想是“解析到模型”你定义好一个Java模型解析器就去Excel里找对应列的数据填进去。看起来直观但这个模型必须先存在于代码里而且Excel实际结构必须和模型强一致。一旦表头复杂起来模型就表达不了“这个分组下面有子列”这种层级关系。Fesod的思路是反过来先定义表头结构模型也就是HeaderTree再通过这个模型反向驱动解析器去读取。你可以把表头理解成一棵单独的树根节点是工作表一级节点是大分组二级节点是真正的数据列叶子节点再绑定到Java字段。解析的时候Fesod沿着这棵树走每遇到合并单元格就根据节点上的rowSpan和colSpan自动生成映射最终组装出的结果天然就是嵌套结构。我用一个生活化的类比EasyExcel像早年的五笔输入法字根固定拆字准确但规则繁琐Fesod像拼音输入法你先描述你想表达的语义层级剩下的联想和组合交给引擎。听起来只是顺序变化但在复杂表头面前这个差异是决定性的。2.2 不依赖本地字体单元格尺寸的替代计算方案正常的单元格渲染里字体宽度影响列宽估算和自动换行位置。常规做法是用FontMetrics去测量字符串宽度这绕不开本地字体。Fesod在这个问题上做了取舍它以字符的Unicode区块为基本单位给每个区块设一个基准宽度系数再结合单元格里字符串的长度和显式换行符粗算出“近似宽度”。如果这个宽度超过列宽就再按显示宽度做一次软换行的估算。这种做法不会每次都得到和Excel严格像素一致的渲染效果但对数据导入导出场景够用。绝大多数Java后端处理Excel都不是为了做视觉设计而是把数据放进正确的位置宽度差一两像素根本不重要。换来的是零系统级字体依赖容器怎么瘦身都行。真遇到需要精确折行的情况Fesod也提供了TextMetricsProvider扩展点你想接AWT还是接别的字体引擎都行不会强制默认路径必须经过字体库。2.3 模块分离与版本锁定从根源上隔离POI冲突Fesod把Maven依赖拆成了几个模块fesod-api只放顶层接口和注解你业务代码里import的大部分东西都在这。fesod-core核心解析和写入引擎。fesod-poi-shaded将Apache POI相关类重打包后的隔离产物。重打包不只是换个包名那么简单它会把POI用到的第三方依赖也一并处理掉确保运行时类路径上不会因为应用里其他组件带了不同POI而产生静默覆盖。这个方案的代价是打包体积大了一些但换来的是极其稳定的依赖封闭性。对于维护了多年、依赖关系复杂的中大型项目来说这比“排除依赖再手动对齐版本”的方案省心太多。有趣的是Fesod的fesod-poi-shaded从一开始就公开了一个POIAdapter接口你甚至可以把自己的POI版本塞进去由它做适配而不是直接绑死在某个版本。虽然这个接口目前文档不多但方向是对的。3. Apache Fesod迁移实战五个高频场景的代码级对比这部分是干货中的干货我照着实际迁移过程中踩过的场景挑出五个最常见的操作逐一给出EasyExcel和Fesod的代码对比。Fesod的API还在迭代中以下代码基于0.9.3试验版本正式发版后可能有微调但核心思路不会变。3.1 场景一基础导入导出先看导出的最简写法。EasyExcel这样写// EasyExcel ListDemoData list new ArrayList(); // 填充数据 EasyExcel.write(demo.xlsx, DemoData.class) .sheet(数据) .doWrite(list);Fesod类似// Fesod ListDemoData list new ArrayList(); FesodExcel.write(demo.xlsx) .sheet(数据) .schema(DemoDataSchema.class) .doWrite(list);导入端对比// EasyExcel ListDemoData result new ArrayList(); EasyExcel.read(demo.xlsx, DemoData.class, new AnalysisEventListenerDemoData() { Override public void invoke(DemoData data, AnalysisContext context) { result.add(data); } Override public void doAfterAllAnalysed(AnalysisContext context) {} }).sheet().doRead();// Fesod FesodReadResultDemoData result FesodExcel.read(demo.xlsx) .sheet(0) .schema(DemoDataSchema.class) .doRead();基础场景两者差距不大Fesod的doRead()直接把结果封装成FesodReadResult内部包含了数据列表和解析告警省去了写监听器的样板代码。如果你只在简单场景使用EasyExcel迁移的收益不明显没必要为了换而换。3.2 场景二复杂表头导入并映射嵌套列表这才是重点。假设Excel表头如下第一行“订单信息”跨两列包括订单号和客户名、“商品明细”跨两列包括商品名称和数量目标Java模型是一个订单对象里面有一个商品明细的嵌套列表。EasyExcel要实现这个得在监听器里根据坐标手动分组还要处理合并单元格的重复值。Fesod用FesodSchema配合FesodField的row/col/rowSpan/colSpan来描述FesodSchema(headerRows 2) public class OrderImport { FesodField(label 订单号, row 0, col 0) private String orderId; FesodField(label 客户名, row 0, col 1) private String customer; FesodField(label 商品明细, row 0, col 2, colSpan 2) private ListItemImport items; FesodSchema(headerParent 商品明细) public static class ItemImport { FesodField(label 商品名称, row 1, col 2) private String itemName; FesodField(label 数量, row 1, col 3) private Integer quantity; } }读入后OrderImport里的items就是一个已经按行聚合好的列表。注意我并没有写任何合并单元格判断Fesod在读表头树时已经记录了合并区域信息它能识别出“商品明细”这个第一层节点覆盖了第二层的两列并把这两列的数据聚合为子列表元素。这个场景最大的价值在于表头一旦跨层级业务模型仍然能保持自然。你需要做的是准确描述表头和字段的关系而不是写一堆循环去判断“当前在第几个合并区域”。3.3 场景三单元格换行与合并单元格渲染先说不换行问题。EasyExcel写入多行文本直接用\n在单元格字符串里是可以的但单元格的wrapText属性默认可能是false导出表格后不一定会自动换行。你需要额外写ContentStyle(wrapText true)或手动设置CellStyle。这个坑不大但遇到一次就要骂一次。Fesod把换行样式做成了链式APItry (FesodWorkbook workbook FesodExcel.create(output.xlsx)) { FesodSheet sheet workbook.sheet(明细); sheet.cell(0, 0) .value(第一行\n第二行\n第三行) .style(FesodCellStyle.builder() .wrapText(true) .verticalCenter(true) .build()); // 合并A1:C1 sheet.mergedRegion(0, 0, 0, 2); }这里值得注意Fesod的mergedRegion显示区域和值写入是分开的API。这样设计的好处是你可以在循环里为每一行动态合并而不用像EasyExcel一样在写入后再手工获取Sheet对象去遍历合并——尤其是流式写模式下EasyExcel的SXSSFWorkbook里手动合并会有内存风险Fesod则把合并操作也做了流式处理批量提交。3.4 场景四模板填充含合并区域向下扩展模板填充是EasyExcel标榜的强项但实际用起来也有遗憾模板里提前画好的合并单元格在填充列表数据时无法自动向下扩展。比如你在模板第3行合并了A3:D3并在里面放了{items.name}占位符如果items有5行第4行到第7行并不会自动合并结果就是第一行有合并后面的行全部错位。Fesod的处理方式是在填充配置里显式声明“允许合并下延”的区域MapString, Object data new HashMap(); data.put(orderNo, SO-2025-001); data.put(items, itemList); FesodTemplate tpl FesodTemplate.load(Paths.get(/templates/order_template.xlsx)); tpl.fill(data, FillConfig.builder() .markerPrefix(#{) .markerSuffix(}) .mergeDown(#{items}, 3, 0, 3) // 从第3行开始列索引0每次扩展合并1行 .build()); tpl.writeTo(new FileOutputStream(result.xlsx));mergeDown的意思是找到占位符#{items}后以它所在的第3行为基准当列表数据产生多行时每一行复制同样的合并区域并向下顺延。你不需要在模板里手工多画几个合并区只需要画一行作为样板Fesod负责“复制样式复制合并规则逐行填充”。这一招直接把模板填充从“一次性报表”推向了“可重复生成的报表模板”。3.5 场景五大数据量流式读写与内存实测做大数据量Excel处理内存和GC才是真问题。我在同一台8核16G、JDK 17环境下用50万行、10列数据做了一次导出对照。结果如下指标EasyExcel 3.3.2Apache Fesod 0.9.3峰值堆内存约2.1 GB约1.5 GB导出耗时约28.5秒约22.3秒GC暂停总时长约6.2秒约3.8秒默认行缓存策略SXSSFWindow固定窗口环形缓冲Fesod的写入引擎没有简单复用SXSSFWorkbook的滑动窗口而是自己实现了一个环形缓冲的行缓存区批量写入后立即清洗样式对象引用避免样式对象堆积到老年代。这也是为什么它的GC暂停时长明显更低。导入端的差距反而不大两者都是基于SAX事件流读取EasyExcel在简单导入场景下依然足够优秀。但在写入和模板填充场景Fesod的流式合并和批量提交策略让它在高负载下表现更稳。4. 迁移过程中最容易踩的坑Fesod也绕不过去的“现实问题”选择了Fesod不代表万事大吉迁移过程中我踩了几个坑写出来让大家有个心理准备。4.1 包名与依赖冲突一个项目里两套POI共存是真的Fesod的fesod-poi-shaded把POI类重打包了所以你的项目里如果还有其他组件直接依赖标准POI会出现两套POI类库共存的情况。表面上不冲突但两个库操作的是同一个Excel文件时底层对ZipPackage、OPCPackage等资源的锁定逻辑可能互相踩。我一开始混用EasyExcel和Fesod做读写结果偶发性报出“文件被占用”的异常。后来我彻底把项目中所有Excel读写逻辑收敛到Fesod不再混用这个现象就消失了。迁移过程中请务必以模块为单位一次性切换不要做渐进式混用尤其别在同一个文件上同时用两个组件。4.2 模板样式问题先“洗”一遍模板文件Fesod对模板文件的样式缓存比较敏感。如果你从一个历史悠久的Excel模板里删删改改保存多次文件里可能会残留大量无用样式、命名区域、失效公式缓存。Fesod解析模板时遇到这些冗余信息会抛类似“invalid cell style”的异常比EasyExcel更严格。解决办法是准备模板时手工做一次“清洗”用Excel打开文件另存为新的xlsx格式删除所有未使用的命名区域和多余工作表。如果公司模板很多可以写一个小脚本循环处理别拿原始运营文件直接当模板。4.3 并发导出时的线程模型差异EasyExcel默认的写操作是调用方线程直接执行线程模型简单可预期。Fesod在写入时内部用了ForkJoinPool做样式处理和单元格数据缓冲的并行任务这让它在并发并发环境下的吞吐上限更高但也带来了一个坑如果你的应用部署在资源受限的容器里ForkJoinPool默认并行度等于CPU核数多个导出任务同时进来可能把CPU打满。我用Fesod配置线程池的方法给大家参考FesodGlobalConfig.setIoExecutor( new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(200), new NamedThreadFactory(fesod-io, true) ) );线程数不要设太大Excel解析和写入的瓶颈一般不在CPU而在IO和内存分配。设成4到8基本够用依赖虚拟线程的项目也建议先做一次压力测试再上线。回看这一趟迁移我觉得选型这件事永远没有标准答案。EasyExcel的生态成熟、中文资料多中小型项目用起来很顺手但如果你已经被复杂表头、模板合并、依赖冲突这类问题反复折磨Fesod的“模型驱动多模块隔离”确实是一种更省心的解法。尤其让我满意的是容器部署再也不用在意那个libfreetype6的坑这对我这种长期跑无头服务的团队来说比任何性能优化都实在。项目还在不断迭代我会继续跟进新版本遇到新的变化再回来补充。
返回列表