首页 / 资讯中心 / 文章详情

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

把 TRANSFORM 做成可扩展能力:JQuick-Excel 的函数与 SPI 调用链 ★ FEATURED ARTICLE
把 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(ListObject)接收求值后的参数并返回结果提供者通过META-INF/services/com.github.paohaijiao.function.core.JQuickMethodFunctionProvider被发现。JQuickMethodInvocationManager负责查找和调用已注册函数JFunctionExecutor则是apply(ListObject args) throws Exception的执行适配契约。环境与依赖当前项目pom.xml已包含dependencygroupIdio.github.paohaijiao/groupIdartifactIdjquick-transform-function/artifactIdversion1.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(){returnfullName;}OverridepublicObjectinvoke(ListObjectargs){if(argsnull||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方便比较两类函数的使用方式。excelsnamespacedemo.StudentExcelServiceexcelnameexportExcelreturnClassvoid![CDATA[ EXPORT WITH SHEET人员, HEADERtrue, MAPPING{firstName:名,lastName:姓,displayName:显示名称}, TRANSFORM{ displayName:fullName(${firstName},${lastName}), lastName:toUpper(${lastName}) } ]]/excel/excelsJava 侧不需要为该函数编写特殊调用代码只要把提供者随应用放到运行时类路径按 XML 代理的常规方式导出即可。MapString,ObjectrownewLinkedHashMap();row.put(firstName,Ada);row.put(lastName,Lovelace);ListMapString,ObjectsourceCollections.singletonList(row);ListJQuickRowrowsJQuickRow.toRows(JObjectConverter.convert(source));try(OutputStreamoutputnewFileOutputStream(people.xlsx)){JQuickParseHandlerparsernewJQuickExcelExportXmlParseFactory(rows,output);JQuickFactoryfactorynewJQuickXmlFactory(parser,people.xml);StudentExcelServiceservicefactory.createApi(StudentExcelService.class);service.exportExcel(field,value);}需要字典时将其放入JContext。例如导出“1”为“男”使用dict.put(1, 男)XML 写trans(${dict},${gender})。导入时通常反转为男 - 1这是数据方向变化不是 SPI 的差异。原理说明一条TRANSFORM的逻辑可概括为当前行字段或JContext值经${...}取值后组成表达式参数解析得到函数名和参数列表随后优先查询JQuickMethodInvocationManager中的提供者找到时调用JQuickMethodFunctionProvider.invoke(ListObject)未找到时交给内置求值器处理结果再写回转换字段。仓库的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或确定性的函数读取既避免 N1 访问也让导出耗时和失败边界更清楚。对于同一批次会重复使用的输入我还会在进入 DSL 前完成规范化例如统一日期时区、去除确定无意义的前后空格并将业务码转换限定在一份受版本控制的字典中。这样 SPI 的职责始终是纯粹转换出现结果差异时可以复现原始行、上下文和函数版本。排查问题时我按“资源发现、函数注册、表达式参数、工作簿结果”的顺序定位。先确认服务文件与实现类都在运行包再检查函数名是否与 XML 一致随后使用一行固定数据确认${field}与${contextKey}取到的值最后检查 Excel 结果。不要用捕获后返回空字符串的方式掩盖参数错误否则导出的文件看似成功业务数据却已经被静默污染。对面向用户的导入我会在应用层将失败行、工作表和列信息整理成可反馈的结果而不是只把底层异常原样暴露。总结TRANSFORM让 XML 规则具备表达能力SPI 让这种表达能力可以按模块扩展。JQuickMethodFunctionProvider定义函数名和调用入口服务描述文件负责发现JQuickMethodInvocationManager负责查找和注册JFunctionExecutor提供统一执行适配形状。我会把无状态、可复用、输入输出明确的报表转换放到 SPI把字段取值、工作簿读写和显示格式留在框架规则中。这样新增一个业务函数只需要增加 Java 类和服务描述不必修改每个报表的 Java 导出流程。
阅读完成 · 觉得有帮助?
咨询建站