☰
把 TRANSFORM 做成可扩展能力:JQuick-Excel 的函数与 SPI 调用链
2026/10/15 18:26:11 网站建设 项目流程

把 TRANSFORM 做成可扩展能力:JQuick-Excel 的函数与 SPI 调用链

tags: #JQuickExcel #JavaExcel #开源 #POI #Excel工具

简介

报表字段转换是 Excel 集成里最容易膨胀的部分:代码值转文案、日期标准化、姓名拼接、脱敏、默认值处理。把这些逻辑散落在 Controller 或导出循环中,规则难以复用,也让 XML 只剩下表头配置。JQuick-Excel 的TRANSFORM允许我把“字段如何变换”写在 DSL 中,并通过 Java SPI 把业务函数接入同一条调用链。

本文只依据 README-CN 已确认的信息介绍六个关键类型:JQuickMethodFunctionProvider、JQuickMethodInvocationManager、JFunctionExecutor,以及TRANSFORM、JContext、内置求值器的关系。不假设未公开的加载实现细节。

前言

我不会为了一个if就创建 SPI,但当同一个转换需要在多个 XML 报表中复用,或转换来自独立业务模块时,SPI 是合理边界。XML 负责声明调用,提供者负责实现业务语义,应用启动或模块装配时让 Java 服务发现机制完成注册。

README-CN 已明确:内置求值器包括toUpper、dateFormat、trans;外部依赖jquick-transform-function可通过 SPI 增加formatDate、translate等函数。它也给出了JQuickMethodFunctionProvider的核心契约:getMethodName()返回函数名,invoke(List<Object>)接收求值后的参数并返回结果;提供者通过META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider被发现。JQuickMethodInvocationManager负责查找和调用已注册函数,JFunctionExecutor则是apply(List<Object> args) throws Exception的执行适配契约。

环境与依赖

当前项目pom.xml已包含:

<dependency><groupId>io.github.paohaijiao</groupId><artifactId>jquick-transform-function</artifactId><version>1.4.0</version></dependency>

自定义函数所在模块必须能访问com.github.paohaijiao.function.core.JQuickMethodFunctionProvider,并把服务描述文件打入最终 jar。仅写实现类而缺少META-INF/services文件,Java SPI 不会自动发现它。

本文的 XML 同时需要通过JQuickXmlFactory调用;因此可沿用项目中JQuickExcelExportXmlParseFactory、JQuickExcelImportXmlParseFactory和JContext的使用方式。示例中的fullName是一个无状态纯函数,适合放在 SPI 提供者中。

代码示例

先实现提供者。参数数量和类型由函数自己定义,但我会在实现中明确校验业务前提,避免把不完整数据悄悄拼成错误报表。

packagedemo;importcom.github.paohaijiao.function.core.JQuickMethodFunctionProvider;importjava.util.List;publicfinalclassFullNameFunctionimplementsJQuickMethodFunctionProvider{@OverridepublicStringgetMethodName(){return"fullName";}@OverridepublicObjectinvoke(List<Object>args){if(args==null||args.size()!=2){thrownewIllegalArgumentException("fullName requires firstName and lastName");}returnString.valueOf(args.get(0))+" "+String.valueOf(args.get(1));}}

然后创建服务描述文件src/main/resources/META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider,内容只有实现类全限定名:

demo.FullNameFunction

在 XML 中调用它。${firstName}与${lastName}读取当前行字段,fullName(...)的参数先被求值,再交给提供者。这里同时演示内置的toUpper,方便比较两类函数的使用方式。

<excelsnamespace="demo.StudentExcelService"><excelname="exportExcel"returnClass="void"><![CDATA[ EXPORT WITH SHEET="人员", HEADER=true, MAPPING={"firstName":"名","lastName":"姓","displayName":"显示名称"}, TRANSFORM={ "displayName":fullName(${firstName},${lastName}), "lastName":toUpper(${lastName}) } ]]></excel></excels>

Java 侧不需要为该函数编写特殊调用代码,只要把提供者随应用放到运行时类路径,按 XML 代理的常规方式导出即可。

Map<String,Object>row=newLinkedHashMap<>();row.put("firstName","Ada");row.put("lastName","Lovelace");List<Map<String,Object>>source=Collections.singletonList(row);List<JQuickRow>rows=JQuickRow.toRows(JObjectConverter.convert(source));try(OutputStreamoutput=newFileOutputStream("people.xlsx")){JQuickParseHandlerparser=newJQuickExcelExportXmlParseFactory(rows,output);JQuickFactoryfactory=newJQuickXmlFactory(parser,"people.xml");StudentExcelServiceservice=factory.createApi(StudentExcelService.class);service.exportExcel("field","value");}

需要字典时,将其放入JContext。例如导出“1”为“男”使用dict.put("1", "男"),XML 写trans(${dict},${gender})。导入时通常反转为"男" -> "1",这是数据方向变化,不是 SPI 的差异。

原理说明

一条TRANSFORM的逻辑可概括为:当前行字段或JContext值,经${...}取值后组成表达式参数;解析得到函数名和参数列表;随后优先查询JQuickMethodInvocationManager中的提供者;找到时调用JQuickMethodFunctionProvider.invoke(List<Object>),未找到时交给内置求值器处理;结果再写回转换字段。仓库的JExcelCommonHandler可验证这一职责分工:它解析转换表达式,取得小写函数名与参数列表,再通过manager.getInvoker(functionName)查找提供者;否则走内置JEvaluator.evaluateFunction。

这也解释了为什么提供者不应解析${field}或自行读取工作簿。它收到的已经是参数对象,职责应聚焦为“输入参数到返回值”。JContext适合传字典、组织信息或规则所需的公共值,但不要把数据库连接、HTTP 客户端等隐式副作用塞进提供者;否则同一份 XML 的结果将难以测试和复现。

README-CN 还确认管理器具备hasMethod、getSupportedMethods、getInvoker、registerInvoker、registerOrReplaceInvoker、unregisterInvoker等能力。对常规模块化函数,我优先使用 SPI;对于测试替身或应用启动时的临时装配,可通过注册 API 显式管理。若名称重复,只有明确需要覆盖既有函数时才使用registerOrReplaceInvoker,避免不同模块静默抢占函数名。

JFunctionExecutor的位置更偏适配层:它以apply(args)表达函数执行契约。我的实践是把它当作“执行入口的统一形状”,而不是再发明第二套 XML 函数协议;报表层对外保持JQuickMethodFunctionProvider,适配和组合在 Java 侧完成。

注意事项

第一,函数名是公共 DSL API。改动getMethodName()会直接影响 XML,不应把 Java 类名变化误当作无影响重构。第二,调用链会将函数名转小写后查找,因此命名应采用稳定、可读的小写形式,例如fullName在使用前需实际验证当前版本的大小写匹配策略;为避免歧义,团队可统一使用全小写名字如fullname。

第三,SPI 文件路径和内容必须精确:文件名是接口全限定名,文件内容是提供者全限定名,每行一个提供者。打包后要检查资源确实进入 jar。第四,提供者要避免共享可变状态;导出可能逐行调用转换函数,状态泄漏会造成跨行污染。

第五,TRANSFORM与FORMAT不是同一层。前者改变值,后者控制 Excel 的显示格式。日期应先用转换得到目标值,再用FORMAT={"date":"yyyy-MM-dd"}表示单元格展示。第六,当前源码的applyTransform中存在一个断言分支与条件方向不一致的可疑点;本文不据此推导运行时行为。接入前应以当前发布版本和真实用例验证 SPI 函数是否被成功调用。

工程化落地与验证

我在接入转换函数时,会先把函数视为模板对外承诺的一部分,而不是临时工具。XML 中出现的函数名、入参顺序和返回值语义,应与提供者实现一同进入版本评审。比如fullname(firstName,lastName)的空值策略必须提前确定:是保留空串、返回占位文本,还是由上游校验拒绝该行。这个决定应由业务模板负责,不能让提供者在不同报表间悄悄改变含义。函数应当尽量是无状态的,同一组输入在同一版本中得到同一输出;读取当前时间、随机值或远程服务会降低导出的可重现性。

运行前我会按四项检查 SPI 打包结果。第一,编译产物包含实现类。第二,jar 中包含准确路径的META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider。第三,文件中只有实现类全限定名,不能混入 Markdown 标记、注释或错误空白。第四,运行时类路径同时具备 JQuick-Excel 及函数模块。开发环境能运行但发布包失效时,通常是资源过滤、阴影打包或模块依赖范围导致服务描述文件没有进入最终制品。

建议用最小导出回归测试验证一条完整路径:准备一行包含正常值的JQuickRow,让 XML 使用自定义函数;再准备一行空值、参数不足或字典缺失的数据,确认结果与团队约定一致。测试不应只断言文件已生成,还应读取生成工作簿对应单元格,或者至少确认转换后的字段值。对于日期、金额和枚举值,再增加边界样本,例如月末、闰日、负数、未知编码和前后空格。这样可以区分函数本身错误、XML 表达式错误与输入数据错误。

TRANSFORM的定位是行级值变换,适合名称标准化、字典翻译和可预测的格式化;跨行汇总、排序后的排名、公式联动应留给FORMULAS或上游业务计算。一个函数若必须查询数据库,也不应在每一行里发起查询。我的做法是先由应用层批量准备字典或结果集,放进JContext,再由trans或确定性的函数读取,既避免 N+1 访问,也让导出耗时和失败边界更清楚。对于同一批次会重复使用的输入,我还会在进入 DSL 前完成规范化,例如统一日期时区、去除确定无意义的前后空格,并将业务码转换限定在一份受版本控制的字典中。这样 SPI 的职责始终是纯粹转换,出现结果差异时可以复现原始行、上下文和函数版本。

排查问题时,我按“资源发现、函数注册、表达式参数、工作簿结果”的顺序定位。先确认服务文件与实现类都在运行包;再检查函数名是否与 XML 一致;随后使用一行固定数据确认${field}与${contextKey}取到的值;最后检查 Excel 结果。不要用捕获后返回空字符串的方式掩盖参数错误,否则导出的文件看似成功,业务数据却已经被静默污染。对面向用户的导入,我会在应用层将失败行、工作表和列信息整理成可反馈的结果,而不是只把底层异常原样暴露。

总结

TRANSFORM让 XML 规则具备表达能力,SPI 让这种表达能力可以按模块扩展。JQuickMethodFunctionProvider定义函数名和调用入口,服务描述文件负责发现,JQuickMethodInvocationManager负责查找和注册,JFunctionExecutor提供统一执行适配形状。

我会把无状态、可复用、输入输出明确的报表转换放到 SPI;把字段取值、工作簿读写和显示格式留在框架规则中。这样新增一个业务函数只需要增加 Java 类和服务描述,不必修改每个报表的 Java 导出流程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询