- 后端
【免费下载链接】fesod
Fast. Easy. Done. Processing spreadsheets without worrying about large files causing OOM.
本文面向使用 Apache Fesod(Incubating)读取 Excel 的开发者,系统讲解读取表头数据的三种核心方式:通过监听器invokeHead回调捕获表头行、通过headRowNumber参数处理多行表头、以及通过head()方法指定表头 POJO 完成表头与实体字段的映射。读完本文,你将掌握表头读取的完整调用链与底层判定逻辑,能够应对单行表头、多行合并表头、无表头等各类真实场景。
表头读取概述
在读取 Excel 时,表头(Head)是指数据行之前用于描述列含义的若干行。Fesod 在 SAX 逐行解析电子表格的过程中,会依据配置的表头行数自动区分"表头行"与"数据行",并将表头行单独交给监听器的invokeHead方法处理。
读取表头数据的基本做法是:继承AnalysisEventListener,重写invokeHead方法,即可在解析到每个表头行时收到回调。这一机制由 ReadListener 接口定义:
/** * When analysis one head row trigger invoke function. * * @param headMap * @param context */ default void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {}headMap的键为列索引(从 0 开始),值为该单元格的ReadCellData<?>对象——它保留了单元格的类型、格式化后的值等元信息,而不仅是字符串。
一、通过 invokeHead 读取表头数据
监听器实现
实现一个专门接收表头数据的监听器,示例代码如下:
@Slf4j public class DemoHeadDataListener extends AnalysisEventListener<DemoData> { @Override public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { log.info("解析到表头数据: {}", JSON.toJSONString(headMap)); } @Override public void invoke(DemoData data, AnalysisContext context) { } @Override public void doAfterAllAnalysed(AnalysisContext context) { } }其中invoke(DemoData data, AnalysisContext context)与doAfterAllAnalysed(AnalysisContext context)是ReadListener接口的抽象方法,即使不处理数据也必须提供实现。
invokeHeadMap:直接获取字符串形式的表头
细心的读者会发现,AnalysisEventListener在实现invokeHead时做了一个默认转换:它先把Map<Integer, ReadCellData<?>>通过ConverterUtils.convertToStringMap转换为Map<Integer, String>,再调用invokeHeadMap。见 AnalysisEventListener:
@Override public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) { invokeHeadMap(ConverterUtils.convertToStringMap(headMap, context), context); } /** * Returns the header as a map.Override the current method to receive header data. */ public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {}因此你有两个选择:
- 重写
invokeHead,拿到带类型信息的ReadCellData<?>,适合需要精确判断单元格类型(如数字表头)的场景; - 重写
invokeHeadMap,直接拿到Map<Integer, String>的纯文本表头,适合大多数只关心表头文字的校验、比对场景。
触发时机与底层判定逻辑
invokeHead并非"读完表头后一次性触发",而是每解析到一行表头行都会触发一次。行类型的判定发生在 DefaultAnalysisEventProcessor.dealData 中:
int rowIndex = readRowHolder.getRowIndex(); int currentHeadRowNumber = analysisContext.readSheetHolder().getHeadRowNumber(); boolean isData = rowIndex >= currentHeadRowNumber; ... if (isData) { // handle data row readListener.invoke(readRowHolder.getCurrentRowAnalysisResult(), analysisContext); } else { // handle data header readListener.invokeHead(cellDataMap, analysisContext); }可见:当行索引rowIndex小于表头行数headRowNumber时,该行被当作表头行,回调invokeHead;否则回调invoke。默认headRowNumber为 1,即第一行是表头,从第二行开始是数据。
完整读取代码
配合监听器即可完成一次读取:
@Test public void headerRead() { String fileName = "path/to/demo.xlsx"; FesodSheet.read(fileName, DemoData.class, new DemoHeadDataListener()) .sheet() .doRead(); }FesodSheet.read的第二个参数DemoData.class指定了数据模型,第三个参数传入监听器;.sheet()选择工作表(默认第一个),.doRead()开始流式解析。
二、多行表头读取
现实中的 Excel 经常使用两行甚至多行合并表头(例如"区域 > 分组 > 具体指标"的三级结构)。Fesod 提供两种方式解析多行表头:
- 显式设置
headRowNumber参数; - 依靠实体类上的
@ExcelProperty注解自动推导表头结构。
headRowNumber 参数
headRowNumber的语义在 ReadBasicParameter 中有明确注释:
0:该 Sheet 没有表头,第一行就是数据;1:该 Sheet 有一行表头,这是默认值;2:该 Sheet 有两行表头,从第三行开始才是数据。
通过 AbstractExcelReaderParameterBuilder.headRowNumber 即可在读取链路上配置:
@Test public void complexHeaderRead() { String fileName = "path/to/demo.xlsx"; FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) .sheet() // 设置多行表头的行数,默认为 1 .headRowNumber(2) .doRead(); }设置为 2 后,前两行都会被当作表头行回调invokeHead(触发两次),从第三行起的数据行才回调invoke。若表头为 0 行,则第一行数据即被当作数据行处理。
依据实体类注解自动解析多行表头
当数据模型通过@ExcelProperty注解声明了多层表头时,Fesod 会在读取结束时根据最后一行表头自动完成列匹配(详见下文"表头映射原理")。仓库测试 ComplexHeadData 展示了典型的三级表头写法:
@Getter @Setter @EqualsAndHashCode public class ComplexHeadData { @ExcelProperty({"Region", "Region", "Merged"}) private String string0; @ExcelProperty({"Region", "Region", "Merged"}) private String string1; @ExcelProperty({"Region", "Group", "Group"}) private String string2; @ExcelProperty({"Region", "Group", "Group"}) private String string3; @ExcelProperty({"Region"}) private String string4; }@ExcelProperty的value是String[]类型(见 ExcelProperty,默认{""}),数组长度即表头行数,数组顺序即从上到下的表头层级。例如{"Region", "Group", "Group"}表示第一行表头为 "Region"、第二行表头为 "Group"、第三行表头为 "Group"。对应的读写往返测试见 ComplexHeadDataTest。
提示:
ComplexHeadDataTest中同时出现了automaticMergeHead(Boolean.FALSE)的写侧用法,说明读侧的多级表头结构可以由写侧通过相同注解生成,二者相互对应,便于构造回归测试。
三、通过 head() 指定表头 POJO
在某些场景下,读取时并未在FesodSheet.read(...)中直接传入实体类(例如不确定表结构、需要运行时决定模型),此时可以使用head()方法单独指定表头 POJO,将表头解析与数据行解析解耦:
@Test public void headerPojoRead() { String fileName = "path/to/demo.xlsx"; FesodSheet.read(fileName, new DemoDataListener()) .head(DemoData.class) .sheet() .doRead(); }head()有三种重载形式,均定义在 AbstractParameterBuilder 中:
head(Class<?> clazz):以实体类的@ExcelProperty注解定义表头,即上文示例的用法;head(List<List<String>> head):直接以字符串列表指定表头,适合表头完全动态、无法预先建模的场景;head(Consumer<HeadBuilder> headBuilderConsumer):通过HeadBuilder编程式构建表头,可精细控制每个字段的名称、索引等。
其中head(Class<?>)会被包装为HeadKindEnum.CLASS类型的表头属性(ExcelReadHeadProperty),读取时用于驱动列匹配与实体字段填充。
四、表头映射原理:buildHead 的列匹配过程
当使用基于类的表头(HeadKindEnum.CLASS)时,Fesod 在解析完最后一行表头后执行 buildHead,完成"表头文本 → 实体字段列索引"的映射。整个过程可分为三步:
- 判定表头结束行:
dealData中当!isData && currentHeadRowNumber == rowIndex + 1时,说明当前行是最后一行表头,随即调用buildHead; - 记录最大非空表头列:过滤掉
CellDataTypeEnum.EMPTY的空单元格后,取最大列号写入setMaxNotEmptyDataHeadSize,供后续读取列数校验使用; - 按名称匹配列索引:遍历实体属性对应的
Head对象,取注解value数组的最后一个元素(即最底层表头文本),与最后一行表头逐列比对;命中后通过headData.setColumnIndex(stringKey)将该列的索引绑定到该属性上。
匹配时还体现了两个值得注意的细节:
- 强制索引优先:源码中
if (headData.getForceIndex() || !headData.getForceName())的分支会跳过名称匹配,直接从源码结构看,这说明注解体系同时支持"按索引定位"与"按名称匹配"两种模式,且按索引模式拥有更高优先级; - 文本规整:匹配前会根据全局配置
AutoStrip/AutoTrim对表头文本做去空白处理(StringUtils.strip或String.trim),以容忍表头单元格两侧多余的空格。
五、常见问题与建议
- 无表头文件:若第一行就是数据,设置
.headRowNumber(0)即可跳过表头逻辑,第一行将直接回调invoke; - 只想校验表头不想建模:优先重写
invokeHeadMap(Map<Integer, String>, ...),把表头转成字符串 Map 后与预期文案比对,简单直观; - 多级表头与数据行错位:务必确认
headRowNumber与实际表头行数一致,否则数据行的起始索引会整体偏移,导致首行数据被误判为表头或反之; - 表头带空格/换行:Fesod 在
buildHead阶段已对表头文本进行 strip/trim 处理,实体注解中应写干净文本即可。
小结
Fesod 的表头读取能力由监听器回调(invokeHead/invokeHeadMap)、读取参数(headRowNumber)与表头构建器(head())三部分组成:监听器回调负责在正确时机交付表头内容,headRowNumber决定表头与数据的行分界,head()则在运行时灵活指定表头来源。理解 DefaultAnalysisEventProcessor 中的行判定与列匹配逻辑后,你便可以在实际项目中自如处理从单行表头到多级合并表头的各种表格结构。
- 后端
【免费下载链接】fesod
Fast. Easy. Done. Processing spreadsheets without worrying about large files causing OOM.
相关推荐
Apache Fesod 表头读取指南:invokeHead 监听、多行表头与表头 POJO 的完整实战
Apache Fesod 表头读取指南:invokeHead 监听、多行表头与表头 POJO 的完整实战 导读 本文聚焦 Apache Fesod(Incuba
后端EasyExcel表头映射终极指南:3个技巧解决Excel数据读取难题
EasyExcel表头映射终极指南:3个技巧解决Excel数据读取难题 还在为Excel表头变化而头疼吗?想要快速掌握EasyExcel的智能映射技巧?本指南将
后端Apache Fesod 表头写入完全指南:多级表头、动态表头与合并策略(HeaderMergeStrategy)
Apache Fesod 表头写入完全指南:多级表头、动态表头与合并策略(HeaderMergeStrategy) 导读 本文是 Apache Fesod(In
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考