JQuick-Excel 自定义 SPI Provider:用 fullName 扩展 TRANSFORM 函数

JQuick-Excel 自定义 SPI Provider:用 fullName 扩展 TRANSFORM 函数

tags: #Java #JQuickExcel #SPI #ServiceLoader #TRANSFORM

更多示例 : JQuick-Excel 官网示例 https://www.jquick.org/jquick-excel/#demo-formulas0005

简介

当一个转换规则在多份 Excel 报表中反复出现时,自定义 SPI provider 可以把规则从 XML 中抽出,形成可复用的 TRANSFORM 函数。README-CN 已确认的扩展契约是 com.github.paohaijiao.function.core.JQuickMethodFunctionProvider:provider 以 getMethodName() 提供 DSL 函数名,以 invoke(List<Object>) 接收求值后的参数并返回结果;实现类需要写入标准 Java SPI 服务文件 META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider。

本文以 README-CN 给出的 fullName 为唯一自定义函数示例。它接收当前行的名和姓,返回展示名称。目标不是构造一套额外注册框架,而是说明已有 SPI 契约如何在 JQuick-Excel 的 TRANSFORM 中落地。所有关于接口、服务文件、函数查找和管理器能力的陈述均以 README-CN 的确认内容为限。

前言

报表中总会出现重复的展示规则,例如人员姓名组合、编码规范化或统一文本处理。若规则只在一份报表使用且非常简单,可以直接使用内置 toUpper、dateFormat、trans。若规则已经跨多个 XML 重复,并且输入输出具有稳定含义,将它实现为自定义 provider 更合适。报表继续声明"哪个字段调用哪个函数",Java 模块负责"函数如何处理参数"。

这种分工首先减少复制。假设十份报表都要把 firstName 与 lastName 组合成显示名称,分别在十个导出入口写字符串拼接,会让修改规则时难以发现全部位置。通过 fullName,XML 只保留 fullName(${firstName},${lastName}),函数实现只维护一处。第二,函数名成为可审查的契约。getMethodName() 返回的名字就是 DSL 中使用的名字,改 Java 类名不必影响 XML,但改函数名必须视为报表契约变更。

SPI 的价值也有边界。它适合确定、无状态、输入明确的转换。它不适合替代应用层的数据准备。数据库查询、HTTP 调用、当前用户权限、事务状态和批次控制不应隐藏在每行都会调用的函数里。JQuick-Excel 的转换路径面向行数据和上下文值;应用层应在创建解析器之前完成查询、授权、缓存和字典准备。

环境与依赖

README-CN 给出的核心库坐标为 io.github.paohaijiao:jquick-excel:3.6.0,运行要求 Java 8 或更高版本。自定义 provider 模块编译时需要该接口可见,运行时需要 provider 类和服务描述文件都在 classpath 中。无论 provider 位于业务应用、独立 jar 还是公共组件,最终运行产物都必须包含 Java 类与 META-INF/services 资源。

xml 复制代码
<dependency>
  <groupId>io.github.paohaijiao</groupId>
  <artifactId>jquick-excel</artifactId>
  <version>3.6.0</version>
</dependency>

若项目还需要外部函数仓库,可另行加入 README-CN 指定的 io.github.paohaijiao:jquick-transform-function:1.4.0。但自定义 fullName 并不以该依赖为前提:本篇的核心是实现 JQuick-Excel 已公开的 provider 接口和发布服务文件。内置函数、外部函数与自定义函数都服务于 TRANSFORM,但它们的交付方式不同。

目录结构需要特别准确。Java 实现可放在任意符合项目包规范的位置,例如 com.example.excel.FullNameFunction。服务文件的路径与文件名必须精确为:src/main/resources/META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider。文件内容不是 XML,不是 JSON,也不是接口名;它是一行一个的 provider 实现类全限定名。

代码示例

XML 先为派生字段 displayName 声明输出列,再调用 fullName。${firstName} 和 ${lastName} 表示当前行字段。示例同时保留原始字段,便于在导出文件中追溯显示名来源;是否保留原字段属于报表设计选择,关键是派生字段必须有清晰列位。

xml 复制代码
<excel name="exportPeople" returnClass="void"><![CDATA[
EXPORT WITH
SHEET="People",
HEADER=true,
MAPPING={
  "firstName":"First Name",
  "lastName":"Last Name",
  "displayName":"Display Name"
},
TRANSFORM={
  "displayName":fullName(${firstName},${lastName})
}
]]></excel>

provider 实现遵循 README-CN 的示例:返回稳定的函数名,并在 invoke(List<Object>) 中使用求值后的参数。下面保留示例的最小语义,不额外假设框架会对空值、参数数量或类型实施何种规则;业务项目可以根据自身函数契约处理输入,但不应将未约定输入静默伪装为正确结果。

java 复制代码
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) {
        return String.valueOf(args.get(0)) + " " + String.valueOf(args.get(1));
    }
}

服务文件写入实现类全限定名。资源文件应使用 provider 模块打包后仍能被运行时读取的路径。多 provider 时按行列出多个实现类;每一个类都应真实存在且能加载。

xml 复制代码
<!-- src/main/resources/META-INF/services/
     com.github.paohaijiao.function.core.JQuickMethodFunctionProvider -->
com.example.excel.FullNameFunction

常规导出调用并不需要为 fullName 新增特殊 Java 分支。行数据和输出流仍按 README-CN 的工厂与解析器方式准备;运行时由已发现的 provider 处理匹配函数。下面代码只展示导出入口,服务接口与 XML namespace 应保持一致。

java 复制代码
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(people));
try (OutputStream output = new FileOutputStream("people.xlsx")) {
    JQuickParseHandler parser =
            new JQuickExcelExportXmlParseFactory(rows, output);
    PeopleExcelService service =
            new JQuickXmlFactory(parser, "jquick-excel.xml")
                    .createApi(PeopleExcelService.class);
    service.exportPeople("field", "value");
}

验收应至少覆盖两类数据:正常的名和姓,确认结果和列位;空值或缺失字段,确认输出是否符合项目已定义的函数契约。还应以实际部署产物运行一次,而不是只在 IDE 中测试,因为服务资源遗漏往往只会在打包后暴露。函数名、实现类全限定名、服务文件路径和 XML 调用名是四个独立位置,排查时应逐一核对。

原理说明

README-CN 给出了清晰的链路:当前行字段或 JContext 值,经 ${field} 进入 TRANSFORM 表达式;函数查找后调用 JQuickMethodFunctionProvider.invoke(List<Object>);provider 返回转换结果;FORMAT 再控制 Excel 显示。对于 fullName(${firstName},${lastName}),provider 得到的不是字面 ${...} 文本,而是当前行这两个字段完成求值后的参数列表。

Java SPI 是 provider 被发现的标准机制。README-CN 明确说明,每个 provider 实现列在 META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider 中,由 Java SPI 发现并加载。这里没有项目私有的 XML 注册步骤需要额外编写。最小实现需要 Java 类和服务描述文件同时存在;只写接口实现而漏掉资源文件,或者服务文件写错类名,都无法构成完整 provider 发布。

函数名是查找键。README-CN 说明管理器按照 getMethodName() 建立注册表,再将求值后的参数列表传给匹配的 provider。因此 getMethodName() 的返回值 fullName 必须与 XML 调用名一致。Java 类叫 FullNameFunction 是实现细节,XML 依赖的是 fullName。在已被多份报表使用后,修改该名称应像修改公开 API 一样评估影响。

README-CN 还确认接口具有 getDescription()、getParameterTypes()、getPriority() 默认能力,最小 provider 只需提供函数名与执行方法。若业务没有明确需要,就不必为了"完整"重写默认能力。额外声明参数类型或优先级不应被当作解决函数同名问题的通用方案,因为 README-CN 没有在本篇所需范围内定义具体的冲突结果或执行先后;不能把推断写成行为保证。

JQuickMethodInvocationManager 的已确认能力包括 hasMethod、getSupportedMethods、getInvoker、getAllInvokers、registerInvoker、registerInvokers、registerOrReplaceInvoker 与 unregisterInvoker。这些方法可用于检查、获取、注册、批量注册、明确替换或注销 invoker。README-CN 还明确指出,使用 registerInvoker 添加函数,使用 registerOrReplaceInvoker 明确替换已有函数。除此之外,本文不声明未确认的注册时机、覆盖优先级或执行顺序。

provider 返回的是转换值,而不是单元格格式。若 fullName 返回字符串,Excel 单元格最终如何显示仍由报表的列、STYLE、FORMAT 等其他配置决定。不要把 provider 写成处理工作簿、输出流或行位置的对象;它的已确认执行契约是 invoke(List<Object>),职责应限制在参数到结果的转换。

注意事项

第一,服务文件路径必须精确:META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider。路径中的接口全限定名是文件名的一部分,不能改成 provider 类名,也不能放到其他 resources 子目录。

第二,服务文件内容应是实现类全限定名,例如 com.example.excel.FullNameFunction。类名、包名或大小写不一致都会导致加载失败。打包后应检查最终产物保留了 Java 类和服务资源;仅本地源码目录正确并不足以证明部署环境正确。

第三,XML 函数名必须与 getMethodName() 返回值一致。fullName、FullName 和 full_name 是不同文本,不能依赖猜测的大小写转换。字段名也要和当前行数据一致,${firstName}、${lastName} 的取值问题不应被误判为 SPI 问题。

第四,函数应无状态并保持确定性。TRANSFORM 会处理报表行,provider 不宜保存本次导出的可变上下文,也不应执行数据库、网络或其他高成本副作用。需要的字典和数据应由应用层预先准备,并通过当前行字段或 JContext 提供。

第五,慎重处理同名函数。README-CN 已确认 registerInvoker 用于添加,registerOrReplaceInvoker 用于明确替换。调用方应把替换意图写清楚,并避免让多个模块无意提供同名函数。本文不假定未确认的同名加载顺序,也不把默认优先级解释为覆盖保证。

第六,区分函数结果与 Excel 显示。provider 仅返回转换值;日期、数字等最终在 Excel 中的展示应由 FORMAT 等相应配置验证。对于显示名称这类字符串,重点检查拼接结果、空值约定、列位置和实际导出文件。

第七,函数契约要小而稳定。一个 fullName 只负责组合姓名。若一段逻辑开始承担权限判断、查数据、发通知和格式生成,应拆回应用层或设计为独立业务流程。SPI 是转换扩展点,不是替代服务层的入口。

总结

自定义 provider 的价值在于将重复的派生展示逻辑收敛为可部署单元。函数名、参数顺序、返回值和 SPI 服务声明共同构成闭环,缺少其中任一项都会让 XML 中的调用失去可验证的落点。

将字段取值与列定位留给 XML,provider 只处理参数到结果的转换,应用层继续负责数据准备,显示格式交给 FORMAT。为 provider 准备直接调用和实际导出两类样本,尤其覆盖空参数与字段顺序,能够在发布前发现服务发现或调用约定的问题。

相关推荐
一 铭5 小时前
Pi实战 12:TDD 测试驱动篇——用 Pi 做测试驱动开发
驱动开发·tdd
泡海椒6 小时前
从 XML 到 Excel:JQuick-Excel 代理与解析器如何协作
xml·excel
kmomo..10 小时前
Linux 驱动开发学习笔记:UART、触摸屏与 ADC 驱动实战
linux·驱动开发·学习
优选资讯10 小时前
Excel 批量打印标签的完整流程
excel
catino11 小时前
SDD:规范驱动开发(Spec-Driven Development)
驱动开发·ai
雪宫街道12 小时前
SpringBoot - POI实现Excel 导入与导出
spring boot·后端·excel
ABB自动化1 天前
带二期控制的PowerFlex 700S驱动器
驱动开发·制造·信息与通信·模块测试·plc 控制器
额额额对了1 天前
Linux 驱动开发:基于 platform 总线 + misc 设备的 DS18B20 单总线温度驱动
linux·c语言·开发语言·驱动开发·嵌入式硬件
Data-Miner1 天前
数据分析,除了Excel数据透视表,还有什么工具?——2026 专业精选推荐榜
microsoft·数据分析·excel