JQuick-Excel JContext 与 trans 实战:将字典值带入行转换
tags: #JQuickExcel #Java #JContext #Excel导入
简介
业务系统通常保存性别、状态、类型等编码,Excel 模板通常展示可读文本。JQuick-Excel 已确认支持通过 JContext 提供外部字典,再在 TRANSFORM 中调用 trans(${dict},${gender})。本文只基于 README-CN.md 与测试 XML 已出现的 JContext、trans、${field} 和 ${key} 语义,说明上下文键、当前行字段和导入导出字典方向如何配合。
前言
把字典翻译散落在 Java 循环中,会使 XML 模板只剩表头信息,真正的展示规则又分散到调用方。JQuick-Excel 的做法是让 Java 在解析器创建前把外部值放入 JContext,让 XML 的 TRANSFORM 保留逐行转换规则。这样,字典的来源仍由业务代码控制,字段如何在工作簿流转则可以在配置中审查。
README 明确说明 ${field} 读取当前行字段,${key} 可以从 JContext 取值。于是 trans(${dict},${gender}) 的第一个参数来自上下文,第二个参数来自当前行。测试 XML 的导出规则和导入规则都采用该形式。这里不应假设 trans 会自动发现某个 Map,也不应把 dict 当作工作簿列名;二者必须由调用方和 XML 显式约定。
最关键的业务点是字典方向。导入时,用户在工作簿填写"男""女",应用通常要保存"1""0",字典应为"展示文本到编码"。导出时,当前行通常保存"1""0",用户希望看到"男""女",字典应为"编码到展示文本"。两个场景都可以使用同一个函数表达式,但字典内容不能不经确认地复用。
环境与依赖
本文使用 JDK 8 或更高版本、Maven 和 xls 或 xlsx 工作簿。Maven 坐标如下:
xml
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-excel</artifactId>
<version>3.6.0</version>
</dependency>
XML 位于类路径,例如 src/main/resources/jquick-excel.xml。导入解析器 JQuickExcelImportXmlParseFactory 接收 JContext 和输入流;README 中的基础导出示例则以行数据和输出流创建 JQuickExcelExportXmlParseFactory。本文不会杜撰其他构造器或私有调用流程,字典传递只使用 README 已展示的 context.put("dict", gender) 模式。
服务接口的全限定名应与 XML namespace 对应,接口方法名应与 <excel name> 对应。输入模板需满足 SHEET 和 HEADER 所声明的工作表与表头契约。若字典由数据库、配置中心或远程服务提供,取值、缓存和权限仍是应用层职责;传入 JContext 前应保证内容已经满足当前导入或导出的方向要求。
代码示例
以下是导入规则。工作簿"学生表"包含"姓名""性别"两列,MAPPING 将它们写入 name、gender 字段;TRANSFORM 用上下文字典将"男""女"还原为内部编码。
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE excels PUBLIC "-//PAOHAIJIAO//DTD API EXCEL 1.0//EN"
"classpath:paohaijiao/dtd/Jquick-excel.dtd">
<excels namespace="com.example.StudentImportService">
<excel name="importStudents" returnClass="java.util.List"><![CDATA[
IMPORT WITH
SHEET="学生表",
HEADER=true,
MAPPING={"姓名":"name","性别":"gender"},
TRANSFORM={"gender":trans(${dict},${gender})}
]]></excel>
</excels>
Java 先构造"展示文本到编码"的字典,再把它放到键为 dict 的上下文中。键名与 ${dict} 必须相同。
java
import com.github.paohaijiao.context.JContext;
import java.util.HashMap;
import java.util.Map;
Map<String, Object> gender = new HashMap<>();
gender.put("男", "1");
gender.put("女", "0");
JContext context = new JContext();
context.put("dict", gender);
完整导入调用保持 README 的代理创建方式:
java
import com.github.paohaijiao.context.JContext;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.JQuickFactory;
import com.github.paohaijiao.xml.JQuickXmlFactory;
import com.github.paohaijiao.xml.parse.JQuickParseHandler;
import com.github.paohaijiao.xml.parse.excel.JQuickExcelImportXmlParseFactory;
import com.github.paohaijiao.xml.param.Param;
import java.io.InputStream;
import java.util.List;
public interface StudentImportService {
List<JQuickRow> importStudents(@Param("field") String field,
@Param("value") String value);
}
try (InputStream input = App.class.getClassLoader().getResourceAsStream("students.xlsx")) {
JQuickParseHandler parser = new JQuickExcelImportXmlParseFactory(context, input);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
List<JQuickRow> rows = factory.createApi(StudentImportService.class)
.importStudents("field", "value");
}
导出时可保持同一表达式 trans(${dict},${gender}),但字典需反向准备,例如键为 "1"、"0",值为"男""女"。这是数据契约变化,不是 XML 函数名称变化。将两份字典显式命名为导入字典和导出字典,有助于代码评审时发现方向错误。
原理说明
JContext 是 Java 与 DSL 之间传递外部值的容器。代码执行 context.put("dict", gender) 后,表达式解析 ${dict} 可以取得该对象;表达式解析 ${gender} 时,则从正在处理的当前行读取字段。trans 在两个参数完成求值后,以字典和当前值完成转换。这个模型的价值在于外部数据与行数据来源不同,但可以在一个声明式表达式中被组合。
导入链路中,HEADER=true 使第一行作为表头,MAPPING 先将"性别"列对应到 gender 字段。随后每一行执行 TRANSFORM,因此 ${gender} 取得的是该行已经映射到的字段值。字典转换不负责识别工作簿列,也不负责决定标题名称;列归属始终由 MAPPING 定义。把这两层职责分开,模板调整时更容易定位问题。
导出链路也是同样的逐行模型,只是当前行值与字典方向相反。若当前数据是编码,字典把编码翻译为展示文本;若当前数据已经是展示文本,又使用编码到文本字典,结果就不会符合预期。函数本身并不会推断"这是导入还是导出",因此业务代码必须在创建上下文前确定字典方向。
FORMAT 与这一过程独立。trans 属于 TRANSFORM,它决定每行写入或导入后的字段值;FORMAT 只控制导出单元格的显示格式。字典翻译不是日期、货币等显示格式,也不能用格式规则替代。类似地,字典成员是否允许、某个编码是否已停用、是否有权限使用某值,仍需在基础转换之后由校验和应用层规则处理。
注意事项
第一,上下文键和表达式键必须完全一致。context.put("dict", gender) 对应 ${dict};如果 Java 使用了别的键,表达式不会从期望位置读取字典。当前行字段也必须一致:导入映射右侧是 gender,转换键和 ${gender} 同样应为 gender。表头"性别"只放在导入映射左侧,不能直接当作表达式字段。
第二,导入与导出都要为字典未命中制定明确策略。本文只说明已确认的转换表达式,不杜撰未文档化的默认值行为。业务可以在导入前补齐模板可选值、在导入后拒绝无法映射的行,或由调用方按业务需求保留原值,但不能把未命中悄悄当成成功。测试至少应覆盖字典命中、字典缺项和输入为空三类数据。
第三,不要让单行字典翻译承担跨行或跨系统约束。批次内重复、用户权限、引用数据存在性、状态是否可迁移等都依赖业务上下文或数据库,应在得到 List<JQuickRow> 后处理。若需要基础单元格约束,只使用 README 已确认的 VALIDATION 范围和规则,并把范围限制在实际数据区。
第四,模板升级时应同时检查工作表名、表头和映射字段。交换列顺序在表头未变时通常不影响映射,但表头名称、全半角、空格或历史别名的变化都会改变契约。建议用包含"男""女"以及未命中值的小工作簿回归测试,检查结果行的 gender 字段,而不是只观察文件是否被成功读取。
第五,流生命周期由调用方管理。创建解析器、创建代理和调用方法应在输入流仍打开时完成。若出现没有转换、字段为空或读错列的情况,按顺序检查 XML 是否在类路径、接口与 namespace 是否一致、节点名与方法是否一致、映射是否命中、上下文键和字段名是否一致。先证明契约正确,再考虑数据量和性能问题。
补充实践
字典转换应先定义数据方向,再写表达式。导入模板中,用户填写的是展示值时,传入 JContext 的字典需要能以展示值找到内部值;导出行中保存的是内部值时,字典需要能以内部值找到展示值。两种场景可以复用 trans(${dict},${gender}) 的函数形态,却不能不加检查地共用同一份键值方向。函数调用不包含"导入"或"导出"的标记,因此方向判断只能由业务代码和字典内容承担。
上下文键需要形成明确约定。Java 调用 context.put("dict", gender) 后,XML 中才使用 ${dict};键名改变时,Java 与 XML 必须同步修改。与之相对,${gender} 来自当前行字段。导入场景中,该字段由 MAPPING 的右侧名称产生;导出场景中,它来自待写出的行数据。dict 和 gender 即使都写在 ${...} 中,来源也不同:一个来自 JContext,一个来自当前行。将二者分开理解,是维护 trans 表达式的关键。
建议把字典准备过程限制在调用边界。数据库、配置文件或远端接口如何提供字典,属于业务应用职责;在创建 JQuickExcelImportXmlParseFactory 前,将已经准备好的 Map 放进 JContext。XML 只表达行值要如何通过字典转换。这样模板能够被审查,而数据加载、缓存有效期和权限控制仍留在合适的应用层。README 已展示的调用方式足以表达这个边界,不需要为本文假设额外的上下文生命周期或自动加载机制。
验收时至少要检查三类契约:上下文键与 ${dict} 是否一致;MAPPING 右侧、TRANSFORM 键与 ${gender} 是否一致;字典方向是否符合本次导入或导出。对于字典未命中的处理,本文不宣称框架具有某种默认值或异常策略,因为现有材料没有给出该承诺。项目应使用小样本在目标版本中验证实际结果,并由应用层制定可见的业务处置,而不是把未命中静默解释为已经翻译成功。
延伸检查
同一个工作簿可能同时有多类字典,例如性别、班级、状态。项目应让每类上下文键表达清楚含义,并使 XML 的 ${key} 与 Java 的 context.put 保持一一对应。不要因为所有字典都存放在 JContext 中,就把它们混成无法辨认的通用对象;表达式的可读性依赖于调用方和模板对键名的共同约定。
模板回归时,除了检查可命中的值,还应保留一条未命中值作为观察样本。现有材料确认了 trans 的调用形式,却没有为本文提供未命中时的返回或报错承诺,因此不应把某次运行观察到的现象包装成固定规则。应用层需要根据目标业务明确后续处置,并通过所用版本的实际测试确认。这样的边界说明比假设一个默认行为更可靠。
trans 只负责当前行值与上下文字典的组合,不应承担数据来源的安全控制。字典内容来自哪里、是否可被当前用户使用、何时刷新、是否需要审计,都是应用层需要控制的事项。XML 负责声明转换,Java 负责提供已准备好的上下文;两端各自职责稳定后,字典展示规则才不会散落在不同的业务循环中。
字典契约核对
字典转换的可测试单位应是一条明确的键值关系,而不是一次"文件成功导入"或"文件成功导出"的笼统结论。对于导入,可选择工作簿文本"男"并断言结果行的 gender 为内部编码;对于导出,则使用内部编码并断言目标单元格为展示文本。两种断言分别验证了同一表达式在不同方向上的输入契约。
当一个模板需要多个字典时,上下文键应继续保持精确。例如性别、状态和类型分别由不同 key 提供时,表达式中的 ${key} 应能直接反映使用哪一份共享数据。JContext 提供的是外部值入口,不会根据当前行字段名自动选择字典;为不同字典保留清晰的键名,能让 XML 审查者检查每一项转换的来源。
字典内容也应在调用边界完成快照式准备。本文不对 JContext 的具体实现或更新机制作额外推断,但从表达式职责看,转换期间应消费与本次任务相匹配的字典。若业务要求按租户、组织或有效状态选择字典,应先由应用层筛选,再放入上下文。这样 trans 仍然只处理"当前行值加已提供字典"的事实,而不会隐藏业务选择条件。
排查结果差异时,应先将问题拆成"当前行字段是否正确"和"上下文字典是否正确"两部分。例如性别列没有得到预期文本,先检查导入映射是否已把该单元格写入 gender,再检查 ${dict} 对应的 key 是否存在于调用方放入的字典。两个来源分别验证后,才需要检查 trans 表达式本身。这样的顺序不依赖未确认的框架默认行为,并能避免因表头、字段名和上下文键混用而扩大排查范围。
总结
JContext 为 DSL 提供外部字典,trans(${dict},${gender}) 则将该字典与当前行字段组合为逐行转换。上下文键必须与表达式键一致,字段引用必须与映射后的业务字段一致;字典来源和模板中的行数据来源不同,配置时不应混为同一概念。
导入与导出的表达式形式可以相同,字典方向却应根据本次数据流明确准备:展示文本转内部编码,或内部编码转展示文本。字典未命中、权限、有效状态和跨记录校验均不由 trans 自动解决,调用方应以小样本验证映射结果,并在应用层明确后续处置。