JQuick-Excel TRANSFORM 函数目录使用指南:内置函数、外部 SPI 与报表边界
tags: #Java #JQuickExcel #Excel #TRANSFORM #SPI
简介
Excel 导入和导出的字段值往往不能直接展示。姓名可能需要统一大小写,日期需要按报表口径输出,业务编码需要借助字典变成可读文案。JQuick-Excel 将这类逐字段值处理放在 TRANSFORM 中:${field} 可以读取当前行字段,${key} 也可以读取 JContext 中的值,函数接收求值后的参数并返回转换结果。README-CN 明确给出的核心内置求值器是 toUpper、dateFormat 和 trans。
当内置函数无法覆盖公共转换需求时,可以接入外部 SPI 函数库。README-CN 指定的仓库为 https://github.com/paohaijiao/jquick-transform-function,Maven 坐标为 io.github.paohaijiao:jquick-transform-function:1.4.0。该依赖通过 SPI 增加函数;README-CN 在转换说明中明确举例 formatDate 和 translate。本篇以这些已确认函数为目录入口,说明怎样按字段来源、转换目的和 Excel 展示职责选择函数。
这里的“函数目录”不是把所有可能的函数名堆进 XML,也不是把报表配置当作通用业务脚本。目录的作用是帮助维护者在已确认能力中定位合适的转换:文本大写选 toUpper,日期值格式化选 dateFormat 或外部 formatDate,上下文字典映射选 trans 或外部 translate。选择之后,仍要明确输入来自当前行还是 JContext,结果写到哪个映射字段,以及最终单元格如何显示。
本文严格以 README-CN 已确认的事实为边界。不会扩展未确认的函数名、不会把函数库的内部实现说成框架保证,也不会编造多个函数之间的执行顺序。对于外部仓库中其他能力,应以使用的实际版本和该仓库文档逐项核对;本文的示例只使用 README-CN 已列出的函数与签名形态。
前言
很多 Excel 配置问题表面上像函数问题,实际上是职责混淆。比如一列生日显示不符合预期,可能是源字段没有取到,也可能是日期转换表达式不正确,还可能只是单元格显示格式没有设置。再如性别编码没有变成文案,可能是 JContext 中没有 dict,也可能是当前行 gender 的值不在字典中。若只盯着函数名修改,很容易让问题在不同配置层之间反复移动。
因此,写 TRANSFORM 前先识别四类责任。第一类是当前行字段,例如 ${name}、${enrollmentDate}、${gender}。它们随每条记录变化。第二类是上下文值,例如 ${dict};应用在解析前通过 JContext 准备,通常适合共享字典。第三类是转换函数,它只接收已经求值的参数并返回结果。第四类是输出配置:MAPPING 决定字段对应的表头和列,FORMAT 独立负责转换后 Excel 单元格的显示格式。
这一划分直接决定选型。若只是把当前行的名称转大写,使用内置 toUpper(${name}) 即可,不需要上下文字典,也不需要额外依赖。若要按指定模式处理日期,内置 dateFormat(${enrollmentDate},'yyyy-MM-dd') 已是 README-CN 给出的写法。若需要把编码映射为文案,先把字典写入 JContext,再使用 trans(${dict},${gender})。这些都是核心库已确认的函数边界。
若团队需要采用外部函数库中 README-CN 例示的日期和翻译能力,则接入 jquick-transform-function。外部 formatDate(${enrollmentDate},'yyyy-MM-dd') 与内置 dateFormat 都属于 TRANSFORM 的值转换;外部 translate(${dict},${gender},'gender','Unknown') 与内置 trans 都涉及字典映射。选择哪个函数应以项目明确采用的依赖和已验证的表达式为准,而不是因为函数名称相近就假定参数、返回类型或缺省行为完全一致。
目录也要和公式分开理解。README-CN 明确区分了 Java 表达式函数和 Excel FORMULAS:TRANSFORM 中的函数在导入或导出路径上处理字段值,而 SUM、IF、TODAY 等 Excel 公式属于独立的 FORMULAS 功能。前者用于写出前的格式化、翻译和规范化;后者用于将公式写入工作簿。两者名称可能相似,但目标、结果和验证方式不同。
环境与依赖
JQuick-Excel 的核心 Maven 坐标为 io.github.paohaijiao:jquick-excel:3.6.0。README-CN 说明项目要求 Java 8 或更高版本,并支持 xls 与 xlsx。只使用 toUpper、dateFormat、trans 时,核心依赖即可;这些函数是 README-CN 明确列出的内置求值器。
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-excel</artifactId>
<version>3.6.0</version>
</dependency>
需要使用 README-CN 所列的外部 formatDate、translate 时,加入指定外部依赖。仓库地址和版本都应保持准确。依赖的作用是让外部 SPI provider 进入运行时类路径;仅在开发机编译通过并不能替代对最终应用运行环境的验证。
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-transform-function</artifactId>
<version>1.4.0</version>
</dependency>
完整项目通常同时包含两项依赖。核心库负责 XML、导入导出规则、JContext 和 TRANSFORM 的使用位置;外部库通过 SPI 扩展可调用函数。外部依赖不改变 XML 的字段映射规则,也不自动提供业务字典。字典、行数据和输出流依然由应用层准备。
<dependencies>
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-excel</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-transform-function</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
XML 定义应放在类路径中。README-CN 的基本模型是 XML 定义服务接口,Java 创建解析器和 XML 工厂,再由工厂创建接口代理。函数目录不改变这一模型:函数只是 TRANSFORM 内的值处理节点,不能替代服务接口、数据准备或工作簿写出。
代码示例
下面的规则将姓名、入学日期和性别编码写到同一张报表。姓名调用内置 toUpper;日期调用内置 dateFormat;性别调用内置 trans。FORMAT 仍单独给日期列设置 Excel 显示格式。这样可以在一份最小配置中看到目录选择的三个基本方向:文本、日期和字典。
<excel name="exportStudents" returnClass="void"><![CDATA[
EXPORT WITH
SHEET="Students",
HEADER=true,
MAPPING={
"name":"Name",
"gender":"Gender",
"enrollmentDate":"Enrollment Date"
},
TRANSFORM={
"name":toUpper(${name}),
"gender":trans(${dict},${gender}),
"enrollmentDate":dateFormat(${enrollmentDate},'yyyy-MM-dd')
},
FORMAT={"enrollmentDate":"yyyy-MM-dd"}
]]></excel>
Java 侧先准备字典并放入 JContext。${dict} 的 key 与 context.put("dict", gender) 必须一致;${gender} 和 ${enrollmentDate} 则由当前行提供。README-CN 展示了 JContext 的创建和 put 用法,下面只保留与函数输入直接相关的部分。
Map<String, Object> gender = new HashMap<>();
gender.put("1", "Male");
gender.put("0", "Female");
JContext context = new JContext();
context.put("dict", gender);
外部函数库已经加入运行时类路径时,可以按 README-CN 的例示在另一个规则中使用 formatDate 和 translate。为避免把内置函数和外部函数的来源混为一谈,示例将两种外部函数写在明确独立的字段转换上。translate 的四个参数形态、formatDate 的日期与模式形态均来自 README-CN 的说明。
<excel name="exportMembers" returnClass="void"><![CDATA[
EXPORT WITH
SHEET="Members",
HEADER=true,
MAPPING={
"name":"Name",
"gender":"Gender",
"birthday":"Birthday"
},
TRANSFORM={
"gender":translate(${dict},${gender},'gender','Unknown'),
"birthday":formatDate(${birthday},'yyyy-MM-dd')
},
FORMAT={"birthday":"yyyy-MM-dd"}
]]></excel>
导出入口仍按 README-CN 的 XML 工厂模式准备行数据与输出流。函数并不要求在业务调用处增加单独分支;表达式中的字段和上下文值准备正确后,转换规则由 XML 说明。服务接口需要与 XML 中的 namespace 对应。
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(students));
try (OutputStream output = new FileOutputStream("students.xlsx")) {
JQuickParseHandler parser =
new JQuickExcelExportXmlParseFactory(rows, output);
StudentExcelService service =
new JQuickXmlFactory(parser, "jquick-excel.xml")
.createApi(StudentExcelService.class);
service.exportStudents("field", "value");
}
代码验收至少要覆盖三种输入。第一种是普通姓名、已知日期和字典中存在的编码,用来确认列、值和显示。第二种是大小写不同的名称,用来确认 toUpper 的结果。第三种是需要业务确认的边界值,例如缺失日期或未知编码。边界数据不应靠猜测函数默认行为处理;应根据项目的输出要求验证实际结果,并确认 JContext 的字典是否准备完成。
原理说明
README-CN 给出的转换链路可以概括为:当前行字段或 JContext 值,经 ${field} 进入 TRANSFORM 表达式;表达式中的函数接收求值后的参数,返回转换结果;FORMAT 控制最终 Excel 显示。理解目录时,最重要的是函数处理的是值,而不是字段名字符串、XML 文本或单元格坐标。
内置 toUpper 的目录位置最直接:它将文本转换为大写。它适合当前行中已经存在的文本字段,例如 TRANSFORM={"name":toUpper(${name})}。函数 key 为 name,输入同样来自当前行 name,结果写回这个映射字段对应的输出列。若希望保留原始列并新增展示列,则应先在 MAPPING 为新字段声明列,再对新字段写明确的转换表达式;不能只把转换 key 写进 XML 而不说明输出列。
内置 dateFormat 用于格式化日期值,README-CN 示例为 dateFormat(${enrollmentDate},'yyyy-MM-dd')。它与 FORMAT 的职责不同。前者属于 TRANSFORM 中的函数调用,处理导出前或导入时的字段值;后者如 FORMAT={"enrollmentDate":"yyyy-MM-dd"},控制转换后的 Excel 单元格显示格式。日期问题应分别检查输入值、转换表达式、输出值以及单元格显示,而不能将两层配置视为同一个开关。
内置 trans 通过 JContext 字典映射值,README-CN 示例为 trans(${dict},${gender})。其中 ${dict} 不是当前行普通字段,而是应用放入上下文的共享值;${gender} 是当前行编码。选择 trans 前需要先明确字典方向:导出场景常见需求是编码到文案,但实际 key 和 value 必须与业务数据保持一致。字典缺失、key 不一致和编码未知是不同问题,应分别验证。
外部 formatDate 由 jquick-transform-function 通过 SPI 增加,README-CN 的例子为 formatDate(${enrollmentDate},'yyyy-MM-dd')。它进入目录的前提是指定依赖在实际运行环境中可用。不要因为核心库内置了 dateFormat,就假定外部函数无需依赖;同样,也不要因为两个函数都与日期有关,就假定它们的全部细节一致。项目应固定使用一个经过验证的表达式,并在依赖升级时复核。
外部 translate 的 README-CN 示例为 translate(${dict},${gender},'gender','Unknown')。它同样需要 JContext 和当前行字段正确就位。字典类型与默认值是该示例表达式的字面量参数;它们不替代对字典内容的准备。若生成结果不符合预期,应依次核对:外部依赖是否在运行时存在、${dict} 是否与 context.put 的 key 一致、当前行 gender 是否存在、以及字典的业务方向是否正确。本文不把这些核对步骤解释为框架内部的调用顺序。
README-CN 还说明,外部依赖通过 SPI 增加函数。自定义 SPI provider 的公开契约是 JQuickMethodFunctionProvider,实现者提供 getMethodName() 和 invoke(List<Object>),并通过 META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider 服务文件发布。对目录使用者而言,这解释了为什么外部函数依赖必须在运行时存在:函数 provider 及其服务资源需要可被 Java SPI 发现。对于本篇所列的 formatDate、translate,使用者只需采用指定依赖和已确认表达式,不应自行推断 provider 的内部类名或加载细节。
如需检查或管理已注册函数,README-CN 确认 JQuickMethodInvocationManager 支持 hasMethod、getSupportedMethods、getInvoker、getAllInvokers、registerInvoker、registerInvokers、registerOrReplaceInvoker 和 unregisterInvoker。registerInvoker 用于添加函数,registerOrReplaceInvoker 用于明确替换已有函数。这里仅列出 README-CN 已确认的方法,不据此推导同名 provider 的加载先后、覆盖策略或未声明的诊断行为。
函数目录与 Excel 公式必须始终分开。TRANSFORM={"name":toUpper(${name})} 表达的是框架在处理字段值时的转换;FORMULAS={D5:'SUM(D2:D4)'} 表达的是向工作簿目标写入 Excel 公式。前者适合文本规范化、日期格式化和字典翻译;后者适合让工作簿保存可由 Excel 计算的公式。需要用户打开文件后参与重算时,评估 FORMULAS;需要在写出前确定展示值时,评估 TRANSFORM。
目录选择应追求最小表达式。姓名大写只调用 toUpper;日期格式化只选择经验证的 dateFormat 或 formatDate;编码翻译只选择 trans 或 translate 并准备上下文字典。把查库、远程调用、权限判断、批处理和共享可变状态塞进逐行转换,会超出 README-CN 所描述的函数职责,也会让导出结果、性能和排错范围难以控制。应用层应先准备稳定数据,再让 XML 描述字段与展示规则。
注意事项
第一,先确认函数来源。toUpper、dateFormat、trans 是 README-CN 明确列出的内置求值器;formatDate、translate 是 README-CN 明确列出的外部 SPI 示例。不要把外部函数当作核心内置函数,也不要把未在当前项目依赖中确认的名称直接写入 DSL。
第二,外部函数使用准确坐标:io.github.paohaijiao:jquick-transform-function:1.4.0,仓库地址为 https://github.com/paohaijiao/jquick-transform-function。构建成功只能说明依赖被解析;部署后的运行时类路径同样必须包含依赖及其 SPI 资源。函数不可用时,先检查实际运行产物,再检查 XML 函数名和参数引用。
第三,字段引用与上下文引用要分清。${name}、${gender}、${enrollmentDate} 应能从当前行数据取得;${dict} 应由 JContext 提供。context.put("dict", gender) 的 key 必须与 XML 中 ${dict} 完全一致。不要用相近名称替代,也不要假定框架会自动查找业务字典。
第四,MAPPING、TRANSFORM 和 FORMAT 分工不同。MAPPING 决定字段与表头,TRANSFORM 决定字段值如何处理,FORMAT 决定最终单元格显示。某字段改名时,应同步检查这三处对应 key;日期问题尤其要分别验证函数结果和 Excel 显示格式。
第五,外部 translate 的表达式参数要按 README-CN 已示例的形态使用:translate(${dict},${gender},'gender','Unknown')。它依赖上下文字典和当前行编码。未知编码、空编码以及默认文本是否符合业务要求,需要用样本验证,不应从函数名推断全部边界行为。
第六,内置 trans 的表达式为 trans(${dict},${gender})。它同样要求字典值在创建解析器前已经写入 JContext。导入和导出虽然都可使用 TRANSFORM,但编码方向可能不同;不要因为语法相同就不经验证地复用同一份映射。
第七,不要将 TRANSFORM 函数写入 FORMULAS,也不要期待 toUpper、dateFormat、trans、formatDate 或 translate 在用户打开 Excel 后再次运行。TRANSFORM 是字段转换位置,FORMULAS 是工作簿公式位置。两者分别测试,才能明确问题发生在导出前处理还是工作簿计算。
第八,保持函数无状态、输入明确且成本可控。逐行转换适合纯粹的文本、日期和字典处理。数据查询、远程调用、事务控制和权限计算应由应用层在解析器创建前完成。这样可以避免每行重复做昂贵操作,也能让同一份报表在相同输入下保持可复核。
第九,自定义函数时只采用 README-CN 已确认的 SPI 契约:实现 JQuickMethodFunctionProvider,提供 getMethodName() 和 invoke(List<Object>),并将实现类全限定名写入 META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider。README-CN 的 fullName 示例说明了两个当前行字段可组合为派生展示值。Manager 的使用只限于已确认方法;不要假定未说明的函数冲突或加载次序。
第十,验收以报表结果为准。准备正常文本、不同大小写文本、已知日期、已知字典编码以及项目需要的边界输入;分别检查表头、列位置、单元格值和显示形式。这样能区分函数未发现、字段取值错误、上下文缺失、映射错误和格式错误,而不是把所有异常归因于 SPI。
总结
函数目录的作用是帮助配置者选对已确认能力,而不是把所有数据处理塞进 TRANSFORM。先辨别输入来自当前行还是 JContext,再确认输出是展示文本、日期格式化结果还是字典翻译;字段映射、单元格格式和工作簿公式仍应各归其位。
新增函数调用时,保留一组能覆盖实际字段和上下文的最小样本,并记录函数来源与依赖版本。对目录中没有明确说明的行为不作推断,遇到跨模板复用的复杂规则再评估外部库或自定义 provider,这样函数选择才不会成为难以审计的隐式业务逻辑。






