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

把 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 已包含:

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 提供者中。

代码示例

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

java 复制代码
package demo;

import com.github.paohaijiao.function.core.JQuickMethodFunctionProvider;
import java.util.List;

public final class FullNameFunction implements JQuickMethodFunctionProvider {
  @Override
  public String getMethodName() {
    return "fullName";
  }

  @Override
  public Object invoke(List<Object> args) {
    if (args == null || args.size() != 2) {
      throw new IllegalArgumentException("fullName requires firstName and lastName");
    }
    return String.valueOf(args.get(0)) + " " + String.valueOf(args.get(1));
  }
}

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

text 复制代码
demo.FullNameFunction

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

xml 复制代码
<excels namespace="demo.StudentExcelService">
  <excel name="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 代理的常规方式导出即可。

java 复制代码
Map<String, Object> row = new LinkedHashMap<>();
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 (OutputStream output = new FileOutputStream("people.xlsx")) {
  JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(rows, output);
  JQuickFactory factory = new JQuickXmlFactory(parser, "people.xml");
  StudentExcelService service = 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 导出流程。

相关推荐
AAA代码批发商2 小时前
DAYS 57【Linux 驱动深度】I2C 子系统驱动开发全指南:从架构、核心数据结构到设备驱动实战
linux·驱动开发·架构·内核开发·i2c 子系统
2601_962780913 小时前
2027采购岗准备:大数据管理与应用专业如何把Excel、SQL变成业务能力
excel
开开心心_Every4 小时前
文件查重软件批量删除重复文件释放空间
java·开发语言·c#·计算机外设·ocr·excel·perl
AAA代码批发商5 小时前
DAYS 56【Linux 驱动深度】SPI 子系统驱动开发全指南:从架构、核心数据结构到设备驱动实战
linux·驱动开发·架构·内核开发·spi 子系统
泡海椒6 小时前
Spring 项目中如何组织 JQuick-Excel 的 XML 与 Bean
xml·spring·excel
死磕音视频6 小时前
Microsoft Copilot 办公入门:在 Word、Excel、PowerPoint 里用 AI 完成日常文档工作
word·excel·copilot·办公效率
Access开发易登软件6 小时前
我全新开发了一套 Access 通用 Excel 导入工具,现在开源了
数据库·microsoft·excel·mfc·vba·access·access开发
泡海椒6 小时前
图表与主题如何设计扩展:基于 JQuick-Excel 现有工厂的工程化做法
excel
ouynagda6 小时前
iMX6ull Linux 嵌入式驱动开发笔记(LCD / ADXL345 / SPI 子系统)
linux·arm开发·驱动开发·笔记·嵌入式硬件