先把话说清楚Apache 基金会下面并没有叫“Fesod”的组件网上搜不到正经资料。标题里的“Apache Fesod”是我项目组内部给替换方案起的代号实际落到工程里是一套基于 Apache POI 的 FastExcel 方案。如果你是因为 EasyExcel 踩坑搜到这篇别急着骂标题党——这篇要聊的就是我为什么放弃 EasyExcel、最后怎么把复杂表头导入、模板填充合并单元格、嵌套 List 渲染这些老难题一次性理顺的。适合所有用 Java 做 Excel 导入导出的后端同学尤其是被 EasyExcel 的“高级功能”折磨过的人。EasyExcel 刚流行那几年我是真喜欢。API 简洁、自动映射、导大数据量内存控制得也好早期项目里我几乎是无脑用它。但项目一旦复杂起来问题就一个接一个往外冒复杂表头导入对不上列、模板填充遇到合并单元格就错位、嵌套 List 只能靠字符串拼接硬解、部署到 Linux 上偶尔还冒出 libfreetype6、NoSuchFieldError factory 这种稀奇古怪的错。每一个单拎出来都还能忍凑到一起就真的忍无可忍了。这篇文章我会把完整的心路历程、选型逻辑、替换后的代码思路、踩过的坑都写出来。不是让你学完立刻把 EasyExcel 删了而是给你一个清晰的判断标准什么情况下该换、换成什么、换了之后怎么处理那些曾经搞不定的场景。1. 为什么放弃 EasyExcel转向 FastExcel 这套方案1.1 EasyExcel 的优点要认不吹不黑先客观说一句EasyExcel 在“简单导入导出”这个场景里依然是很好的选择。单表结构、列和字段一一对应、不涉及复杂样式的批量导出用 EasyExcel 写起来非常快EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();这一行代码就把文件读取、对象映射、监听器回调全包了尤其是它基于 SAX 的流式读法处理几十万行数据也不会把内存撑爆这在早期是碾压 POI 的卖点。包括我自己维护的老项目里很多“读一张表导一张表”的接口现在还是 EasyExcel 在跑。所以这篇文章的核心结论不是“EasyExcel 垃圾”而是一旦需求超过它设计好的那条线EasyExcel 的高效优势会迅速被各种 workaround 吃掉。1.2 让我忍无可忍的几个真实场景我在生产环境里真正被逼到想换掉它的是下面这四类问题每一个都在网上能搜到大量求助帖第一复杂表头导入。真实业务里的 Excel 很少有“第一行就是字段名”的干净表格。比如财务对账单表头可能是两行合并第一行是“2024年度对账单”第二行拆出“客户名称”“订单号”“本月金额”“累计金额”其中“金额”还跨两列分“应收”“实收”。EasyExcel 默认按对象映射遇到这种带合并单元格的表头要么靠 headRowNumber 跳过要么手动指定列下标多级表头 相同列名时列和字段经常错位。有一次我们导供应商账单两个表头重名数据整整错位了一列排查了半个下午。第二模板填充的合并单元格。合同、对账单、检测报告这类需求本质是“把数据填进一个做好的 Excel 模板”。EasyExcel 的 fill 功能做简单的单值填充没问题但只要模板里有合并单元格动态插入明细行时合并区域不会跟着行数变化填完的文件打开后行错位、合并区域把后面的数据盖住甚至直接提示“文件损坏需要修复”。这个我试过很多次最后都是自己写 POI 代码去调整合并区域等于绕回了原点。第三嵌套 List 渲染。最典型的是“一行主数据带多条明细”比如订单表里每个订单可能有多条商品明细。EasyExcel 官方对这块的推荐做法是把明细拼接成一个字符串塞进单元格或者自定义拦截器去动态写行。拼字符串的方案在“需要每个明细独占一行”时根本不能用自定义拦截器又得自己处理所有合并样式写起来非常复杂维护起来更头大。第四版本和环境玄学。项目里如果同时有别的依赖在引 POIEasyExcel 和 POI 版本打架是常有的事我遇到过启动时直接抛NoSuchFieldError: factory。还有部署到精简版 Linux 容器时Excel 里带图片或者特殊字体渲染会报缺libfreetype6的错。这些不是 EasyExcel 的核心功能问题但都是它依赖链不透明带来的连带伤害。1.3 为什么不是直接换成 Apache POI也不是留在 EasyExcel很多人觉得嫌 EasyExcel 不够底层那就直接用 Apache POI 嘛。但直接用 POI 的问题也很明显API 太啰嗦读一个 Excel 要手动管理 Workbook、Sheet、Row、Cell连遍历数据行都要写十几行样板代码而且它在读大数据量文件时内存占用偏高默认 DOM 方式会把整张表加载进内存。FastExcel 这套方案的定位恰好卡在中间底层操作还是 Apache POI但封装了流式读取和一批针对模板、样式、合并单元格的工具方法既保留了 POI 的底层控制力又不用什么都手写。更关键的是它在模板填充和复杂表头处理上给了我们直接动 POI 对象的入口。如果你在网上搜“FastExcel”能看到它是一个开源 Excel 处理库API 设计上很像 EasyExcel内部依赖 POI。我把这套东西在项目里叫“Apache Fesod”主要是内部代号顺口真正对外依赖的是 FastExcel Apache POI 的组合。1.4 三套方案对比方案上手成本复杂表头导入合并单元格模板填充嵌套 List 渲染大数据量内存EasyExcel低一般多级表头容易错位弱需大量手工 workaround弱靠字符串拼接或自定义处理低原生 Apache POI高代码繁琐强可完全按坐标控制强但全部要自己写强但代码量很大高注意读法FastExcel POI中强可直接解析表头区域强能拿到底层 POI 对象处理中偏强推荐预处理为平铺结构低2. 复杂表头导入先读懂表头再读数据2.1 别让框架猜表头要自己解析用 EasyExcel 做复杂表头导入时最常见的做法是配置headRowNumber或者用ExcelProperty的 index 强制指定列。这在表头结构固定时没问题但一旦表头里出现合并单元格、动态列、或者“一级表头文本相同但二级表头不同”的情况映射就乱了。换到 FastExcel 之后我给自己定了一条规矩复杂表头文件永远先手动解析表头区域把每一列的真实含义算出来再读数据行。这不是绕路恰恰是走得最稳的路因为“列的含义”本来就该由业务代码确定不该让框架猜。Excel 的复杂表头本质上是一张二维表格合并单元格的语义是“这个单元格的值覆盖了它合并到的所有行列”。所以第一步是把每个合并区域拆开把左上角的值复制到整个区域。比如“本月金额”合并了两列那这两列的列头都应该是“本月金额”再根据实际需要加工成“本月金额-应收”“本月金额-实收”。2.2 用 POI 读取合并区域并生成列头映射FastExcel 底层是 POI所以我直接在拿到 Workbook 对象后用 POI 的 API 读取合并区域。大致思路如下try (InputStream in new FileInputStream(对账单.xlsx); Workbook wb WorkbookFactory.create(in)) { Sheet sheet wb.getSheetAt(0); MapInteger, String headerMap new HashMap(); // 遍历所有合并区域把左上角的值填充到区域内每个单元格 for (CellRangeAddress region : sheet.getMergedRegions()) { int firstRow region.getFirstRow(); int firstCol region.getFirstColumn(); int lastRow region.getLastRow(); int lastCol region.getLastColumn(); String value getCellValue(sheet, firstRow, firstCol); for (int r firstRow; r lastRow; r) { for (int c firstCol; c lastCol; c) { String key r - c; headerMap.put(key, value); } } } // 假设表头占前两行生成“第2行_列号 - 列名”的映射 // 如果是二级表头可以把第一行和第二行的值拼起来 MapInteger, String columnNameMap new HashMap(); int headerLastRow 1; // 第二行作为真正的列名行 for (int c 0; c sheet.getRow(headerLastRow).getLastCellNum(); c) { String second getCellValue(sheet, headerLastRow, c); String first getCellValue(sheet, 0, c); String name (second.isEmpty() ? first : first - second); columnNameMap.put(c, name); } // 从第三行开始读数据根据 columnNameMap 决定每个字段取哪一列 for (int r 2; r sheet.getLastRowNum(); r) { Row row sheet.getRow(r); // 按业务需要的字段名从 columnNameMap 中找到对应列索引再取值 } }getCellValue这个方法里要注意POI 读单元格时公式单元格、日期单元格、字符串单元格的类型都不一样建议用DataFormatter统一转成字符串后面再按业务需要转换类型private static String getCellValue(Sheet sheet, int rowIdx, int colIdx) { Row row sheet.getRow(rowIdx); if (row null) { return ; } Cell cell row.getCell(colIdx); if (cell null) { return ; } DataFormatter formatter new DataFormatter(); return formatter.formatCellValue(cell); }2.3 动态列的处理思路上面这套方法还能顺手解决“动态列”的问题。比如对账单里“1月、2月、3月……”是动态生成的列EasyExcel 的对象映射根本没法提前定义字段。用我的方案表头解析完成后直接根据表头文本生成一个Map月份, 列索引数据行遍历时再按月份取列逻辑非常清晰。2.4 一个容易被忽略的坑用getMergedRegions()遍历合并区域时如果某个单元格没有被合并它不会出现在区域列表里。所以我上面代码里的headerMap只处理了合并过的单元格普通单元格需要额外走一次getCellValue逻辑。还有个坑是同一个文件里合并区域可能有重叠虽然 Excel 正常不会保存这种文件但数据清洗阶段可能会遇到遍历时要对区域做去重或交集判断不然会覆盖掉已有值。3. 模板填充与合并单元格实战用 POI 手工控制动态行3.1 模板填充的本质是什么经常有人问“模版里怎么填充”尤其是模板里带合计行、带合并单元格时问的人特别多。EasyExcel 的 fill 操作本质上是用一个对象去替换模板里的变量占位符比如模板里写{name}fill 之后替换成真实姓名。这个机制处理“填几个固定格子”非常方便。但业务模板很少只有几个固定格子。一个典型的对账单模板长这样第一行大标题“XX公司对账单”第二行客户名称、对账周期第三行开始明细列表动态行列名分别是“商品”“数量”“单价”“金额”明细下面有个“合计”行再下面是“确认签字”区域有几行合并单元格动态明细行之前和之后的内容都是相对固定的难点在于明细行数不固定每加一行明细合计行和签字区域都得往下挪“金额列”的合并单元格也要跟着扩展。3.2 FastExcel 给的方案填充后拿 POI 对象调整FastExcel 的方式是允许填充完成后直接拿到底层的 POI Workbook 对象做二次处理。这样就不需要像 EasyExcel 那样在模板里做复杂表达式而是把“动态插入行”这个动作交给代码完成。核心步骤拆成三块解析模板、插入动态行、修正合并区域和样式。先看解析模板用 FastExcel 把不会变的值填充进去InputStream templateStream new FileInputStream(对账单模板.xlsx); // 填充静态变量比如客户名称、对账周期 FillData fillData new FillData(); fillData.setCustomerName(某某科技); fillData.setPeriod(2024-01-01 ~ 2024-03-31); // 这里调用 FastExcel 的 API 完成模板静态部分填充得到 Workbook 对象 Workbook wb ...;这里我不展开具体 API因为重点在下一步也就是拿到Workbook之后的操作。3.3 动态插入明细行的 POI 核心逻辑假设模板里明细区已经预留了 1 行标准样式就是占位行有格式、有边框需要插入 20 行明细。做法是先在第占位行的位置后面复制插入 19 行把原占位行的内容往下挤然后把每行的数据填进去。POI 里shiftRows能把某个范围内的行整体下移是动态插入行最核心的方法Sheet sheet wb.getSheetAt(0); int startRow 7; // 占位行索引示例值 int detailCount 20; int lastRowBeforeShift sheet.getLastRowNum(); // 先把 startRow1 之后的行整体下移 detailCount - 1 行 // 参数含义起始行、终止行、移动行数 sheet.shiftRows(startRow 1, lastRowBeforeShift, detailCount - 1); // 然后从 startRow 开始逐个创建或覆盖行填充明细数据 for (int i 0; i detailCount; i) { Row targetRow sheet.getRow(startRow i); if (targetRow null) { targetRow sheet.createRow(startRow i); } // 把占位行的单元格样式复制到当前行 Row styleRow sheet.getRow(startRow); // 原占位行如果被 shift 移动了需要另取 for (int c 0; c styleRow.getLastCellNum(); c) { Cell oldCell styleRow.getCell(c); Cell newCell targetRow.createCell(c); if (oldCell ! null) { newCell.setCellStyle(oldCell.getCellStyle()); } } // 填充真实数据 targetRow.getCell(0).setCellValue(detailList.get(i).getProductName()); targetRow.getCell(1).setCellValue(detailList.get(i).getQuantity()); targetRow.getCell(2).setCellValue(detailList.get(i).getPrice()); targetRow.getCell(3).setCellValue(detailList.get(i).getAmount()); }这里有个非常值得注意的点shiftRows移动行时被移动行的合并单元格不会自动跟着调整。比如模板里的“合计”行跨了两列下移之后合并区域还在原来的行索引上所以必须在插入行之后先删除受影响的旧合并区域再重新创建新的合并区域。3.4 合并区域重建先删旧再建新我先封装一个方法把某一行的所有合并区域全部移除然后按新的行索引重新合并。这个方法在模板填充里属于高频操作private static void removeMergedRegionsForRow(Sheet sheet, int rowIdx) { ListCellRangeAddress toRemove new ArrayList(); for (CellRangeAddress region : sheet.getMergedRegions()) { if (region.getFirstRow() rowIdx region.getLastRow() rowIdx) { toRemove.add(region); } } for (CellRangeAddress region : toRemove) { sheet.removeMergedRegion(sheet.getMergedRegions().indexOf(region)); } }删掉旧区域之后再按新行索引创建合并区域比如“确认签字”区域原来是第 15 行到第 17 行跨 4 列插入 19 行之后就变成了第 34 行到第 36 行CellRangeAddress signRegion new CellRangeAddress( signStartRow offset, // 起始行 signEndRow offset, // 结束行 0, // 起始列 3 // 结束列 ); sheet.addMergedRegion(signRegion);做完合并区域修正之后还有一个更容易被忽略的问题合计公式。模板里的合计行如果用了SUM()公式插入行之后公式的引用范围不会自动扩展。我建议在填充完成后重新计算并重写公式。既然数据都已经算好了也可以直接放数值而不是公式省得 Excel 打开时提示“公式未计算”。3.5 样式复制的小细节动态插入的行如果直接createRow新建的行默认没有任何样式和模板里的明细行看起来完全不一样。做法是先把模板占位行的所有单元格样式复制到新建行获取CellStyle对象后直接setCellStyle这样边框、字体、背景色都能带过去。但要注意行高不会跟着样式复制走因为行高是Row级别的属性需要手动设置。如果模板占位行设置过自定义行高插入新行时也要把getHeightInPoints()一起复制。4. 嵌套 List 渲染别再硬塞对象树了转成平铺结构4.1 为什么嵌套 List 在 EasyExcel 里这么难写“一行主数据带多条明细”的需求在 EasyExcel 里之所以难是因为 EasyExcel 的映射模型是一行数据对应一个 Java 对象而嵌套 List 天然是“一对多”的树形结构。你没法用ExcelProperty把一个ListItem直接映射到动态行里只能退而求其次把明细拼成一个字符串塞进一个单元格比如“商品A x2、商品B x3”这种方案最省事但没法做到一个明细占一行客户经常不接受。自定义CellWriteHandler在写入每一行时动态查明细并写多行这种方案能实现但要自己控制行号偏移、样式复制、合并单元格代码量很大而且 EasyExcel 的拦截器在处理分页写入时行号会乱。我在换方案之前经常是“拼接字符串 自定义拦截器”混合用。能跑但代码很丑而且新接手的人根本不敢改。4.2 换方案后的核心思路数据预处理 行范围合并换到 FastExcel POI 之后我的做法彻底变了不在导出层纠结对象树结构而是先把主数据明细拍平成一张“可导出行”的列表然后在输出时通过合并单元格把同一主数据的明细行归组。这个思路说起来很简单做起来也很稳。步骤是遍历主数据对每条主数据生成 N 行明细行连续放入同一张表。记录每条主数据的起始行和结束行以及它占用的列范围。输出完成后把“主数据相同”的行按列范围做合并让同一主数据在某一列比如订单号列只显示一次。4.3 代码思路示例平铺数据的核心是把对象树变成一行行的ExportRowListExportRow rows new ArrayList(); for (Order order : orderList) { int firstRowIndex rows.size(); for (OrderItem item : order.getItems()) { ExportRow row new ExportRow(); row.setOrderNo(order.getOrderNo()); row.setCustomerName(order.getCustomerName()); row.setProductName(item.getProductName()); row.setQuantity(item.getQuantity()); row.setPrice(item.getPrice()); rows.add(row); } mergedRegions.add(new MergeInfo(firstRowIndex, rows.size() - 1, 0, 1)); // 合并订单号列 }写 Excel 时先平铺写完所有行再统一处理合并区域for (MergeInfo info : mergedRegions) { if (info.endRow - info.startRow 0) { CellRangeAddress region new CellRangeAddress( info.startRow headerRows, info.endRow headerRows, info.startCol, info.endCol ); sheet.addMergedRegion(region); } }这样处理出来的表格视觉效果和“一行主数据带多条子行”完全一致但代码逻辑非常简单没有拦截器没有行号偏移计算任何人都能看懂。4.4 还能顺手处理“每个明细下面要不要合计行”有的业务要求每条主数据下面带一行小计。在这个平铺模型里也非常好处理每写完一个主数据的明细就追加一行“合计”然后继续写下一个主数据。合并区域计算时把合计行的行号接上就好。4.5 什么情况下才真正需要直接写 POI 渲染如果嵌套层级超过两层比如“集团-公司-部门-人员”平铺出来的字段太多合并逻辑会变得复杂。我遇到过这种需求最终是直接用 POI 循环渲染不再依赖任何封装库。做法本质还是一样的平铺思路只是渲染更多列、更多合并区域。所以我的建议是嵌套层级超过两层时不要指望任何 Excel 库帮你简化直接用 POI 写一个专门渲染器比在框架上面缝缝补补给力得多。5. 绕不开的环境与版本坑NoSuchFieldError、libfreetype6、换行5.1 NoSuchFieldError: factory 的排查思路这个错在 EasyExcel 相关项目里经常被搜到但它不是 EasyExcel 本身的问题而是依赖冲突。报错内容通常是启动时就抛java.lang.NoSuchFieldError: factory出现原因多半是项目中多个依赖传递性地引用了不同版本的 Apache POI 或 CGLIB类加载的时候恰好拿到了一个旧的或者说残缺的类。我自己遇到的一次是某个报表组件依赖了老版 POIEasyExcel 又引了新版 POI两个 jar 包在 classpath 里打架运行时反射访问factory字段失败。排查顺序很固定先看完整堆栈找到是哪个类抛的错。在 IDE 的依赖分析面板里搜这个类确认它来自哪个 jar 包。检查是否存在多个版本的 POI、CGLIB、ASM。用dependency:tree定位冲突来源。mvn dependency:tree -Dincludesorg.apache.poi解决手段无非排除旧依赖或统一版本。最省事的方式是在项目里显式声明一套固定的 POI 版本并让所有涉及 Excel 的库都使用它。如果你还在 EasyExcel 2.x 上升级到 3.x 之前一定要先确认兼容的 POI 版本。换到 FastExcel 方案后我直接对 POI 版本做了统一管理这类问题基本销声匿迹。因为代码里所有底层操作都是 POI 官方对象不需要靠框架的反射魔法去解析字段自然少了很多玄学。5.2 libfreetype6Linux 环境里的字体渲染问题libfreetype6这个错多出现在 Linux 服务器上而且大多数和“生成 Excel 里带图片、图表、复杂字体样式”相关。Java 在渲染字体时会依赖系统的 FreeType 库精简版容器里往往没有装于是抛错java.lang.UnsatisfiedLinkError: libfreetype.so.6: cannot open shared object file场景很典型项目部署到 Alpine Linux 这种精简镜像用 POI 设置特殊字体或导出包含 PNG 图片的 Excel 时触发字体渲染项目直接崩溃。我当时处理分两步。临时办法是在容器里装包apt-get update apt-get install -y libfreetype6如果是 Alpine对应包名会不一样需要换成对应的freetype相关包。长效办法是调整基础镜像或者尽量避免在服务端 Excel 里使用依赖本地字体渲染的功能。POI 写纯文字和数字并不会触发 FreeType通常是“插入图片”“图表”“自定义字体测量”等功能才会知道了这个边界排查范围能缩小很多。5.3 单元格换行导出用 \n 加 wrapText导入用 DataFormatter单元格换行这个问题网上搜“easyexcel 单元格换行”能找到一堆帖子但很多人其实混淆了“写入换行”和“读取换行”两个方向。写入时Excel 单元格里要显示多行文本字符串里得有换行符\n同时单元格样式必须开启自动换行setWrapText(true)。只写\n不设置setWrapText打开 Excel 看到的是把换行符显示成一个小方框或者根本看不出换行效果。正确做法CellStyle style wb.createCellStyle(); style.setWrapText(true); cell.setCellStyle(style); cell.setCellValue(第一行\n第二行);读取时用 POI 的getStringCellValue()读出来的字符串是含\n的但很多人会发现在 EasyExcel 映射的时候换行丢失了原因多半是模板文件里单元格设置了“忽略换行”或文本格式不对也可能是 EasyExcel 的转换器把字符串做了 trim。我建议复杂换行文本读取统一用DataFormatter至少能保留单元格里真实的文本内容DataFormatter formatter new DataFormatter(); String text formatter.formatCellValue(cell);然后按\n拆分处理。如果拆出来还是不对再去检查 Excel 单元格里的换行到底是\n还是\r\n这个在 Windows 上编辑过的文件里经常混着来。5.4 推荐版本组合参考这里给一套我在生产里跑得比较稳的组合仅供参考具体版本以官方最新稳定版为准组件建议版本策略FastExcel/fast-excel使用 GitHub 上维护较活跃的版本Apache POI与 FastExcel 兼容的 5.x 版本显式声明poi-ooxml与 POI 主版本保持一致Apache Commons Compress随 POI 5.x 默认依赖即可日志框架使用 SLF4J方便看 Excel 解析时的 warn 日志版本统一是 Excel 依赖管理的头等大事。我在 pom 里直接用dependencyManagement锁死 POI 家族版本其他库传进来的 POI 依赖全部按这个版本解析冲突问题少了一大半。6. 迁移后的实测收益和真心建议6.1 迁移改造的花费和收益我们项目里有三个模块涉及 Excel对账单导入、模板对账单导出、订单明细导出。改造工作量大概是三个工作日因为不需要把所有模块一次性换完可以按模块逐个替换每个模块验证通过再切流量。收益最明显的是删除了一大堆 workaround 代码。原来为了处理嵌套 List我写了一个两百多行的自定义拦截器后来还专门加了一个“字符串拼接转行”的兼容逻辑替换之后这两个类直接删掉换成数据预处理 合并区域循环代码量减少一半还多。模板填充的合同导出原来每次客户改模板都要花一两天调试合并单元格现在只要模板本身的结构没问题代码层面基本不用怎么动。内存方面没有特别大的变化因为 FastExcel 底层读取也是流式的大数据量导入的峰值内存和 EasyExcel 差不多。稳定性反而提升了一步因为版本冲突问题被彻底排掉了。6.2 它也有的短板说得这么顺不代表它没缺点。FastExcel 的社区生态和文档丰富度明显不如 EasyExcel遇到问题能搜到的资料很少很多时候要自己去看源码。另外它保留了 POI 的底层控制力这是优点但也意味着你不能像 EasyExcel 那样“一键跑通”一些高级功能你必须理解 Excel 对象模型比如合并区域、行复制、样式复制这些概念没接触过的人需要补课。如果你的项目只是简单列表导入导出我真心建议继续留在 EasyExcel没必要迁移迁移本身也是有成本的。这个选择完全取决于你的需求边界在哪里。6.3 我给你的选择标准简单导入导出、单表结构、字段能对齐EasyExcel 完全够用别折腾。需要填模板、模板里有合并单元格且行数动态变化建议考虑 FastExcel POI或者直接用 POI。经常处理带合并单元格的复杂表头强烈建议自己写表头解析无论你用哪个库都不要靠框架的自动映射去猜。嵌套 List 需要每个明细单独一行别再纠结框架了直接把数据预处理成平铺行用合并区域做视觉归组这才是最稳的路。6.4 最后再分享一个实际体会我踩过好几次坑之后才明白Excel 导入导出这种需求框架能帮你解决 80% 的常规场景剩下 20% 的复杂场景与其在框架的拦截器、表达式、占位符里绕来绕去不如早点承认“这块需要底层能力”直接用 POI 处理。所谓“再见了 EasyExcel”对我来说不是否定它而是承认一个工具的能力边界然后选择一个不会让我继续堆 workaround 的方案。如果你也正在被复杂表头、模板合并、嵌套 List 折腾希望这篇能帮你省几个晚上的加班时间。