JQuick-Excel 中 FORMULAS 与 TRANSFORM 的边界
tags: #JQuickExcel #JavaExcel #开源 #POI #Excel工具
简介
我用一份订单报表说明何时改变导出值,何时把计算留给 Excel,避免把两种表达式写进错误阶段,并给出可复用的判断方法。
前言
TRANSFORM 与 FORMULAS 都能看到"计算",但它们不是替代关系。TRANSFORM 在导入或导出时针对当前行字段求值,适合字典翻译、日期标准化、大小写和 SPI 业务函数;FORMULAS 向指定坐标写入 Excel 公式,适合合计、排名和用户后续编辑后仍需重算的结果。我在项目中将两者分开,是为了保留清晰的数据责任。
环境与依赖
JDK 8+,依赖版本为 3.6.0。trans、toUpper、dateFormat 为 README 已确认的内置转换函数;SUM、IF、ROUND 是 Excel 公式。
xml
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-excel</artifactId>
<version>3.6.0</version>
</dependency>
代码示例
订单状态由上下文字典翻译,姓名导出为大写,金额总计则交由 Excel 管理。两类规则可以共存,但其输入和目标不同。
xml
<excel name="exportOrder" returnClass="void"><![CDATA[
EXPORT WITH SHEET="订单", HEADER=true,
MAPPING={"name":"客户","status":"状态","amount":"金额"},
TRANSFORM={
"name":toUpper(${name}),
"status":trans(${statusDict},${status})
},
FORMULAS={C5:'SUM(C2:C4)',D5:'IF(C5>1000,"达标","待跟进")'}
]]></excel>
java
JContext context = new JContext();
context.put("statusDict", statusDict);
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(orderList));
try (OutputStream output = new FileOutputStream("orders.xlsx")) {
JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(context, rows, output);
OrderExcelService service = new JQuickXmlFactory(parser, "jquick-excel.xml")
.createApi(OrderExcelService.class);
service.exportOrder("field", "value");
}
原理说明
${name}、${status} 从当前行读取,${statusDict} 从 JContext 读取;TRANSFORM 的返回结果作为单元格值写出。它不会留下可编辑的公式,也不以 A1 坐标表达字段关系。反过来,FORMULAS={C5:'SUM(C2:C4)'} 的左侧是单元格坐标,右侧保存到工作簿;用户修改 C2 后,Excel 会重新计算 C5。
我用三条判断选择能力:结果是否依赖当前行字段和上下文,是否应在写出前固定,是否需要随工作簿编辑而联动。前两项为"是"时选 TRANSFORM;最后一项为"是"时选 FORMULAS。日期字段可先 dateFormat(${date},'yyyy-MM-dd'),再用 FORMAT 控制单元格显示;显示格式也不是公式。
从可测试性看,两者也有不同。转换结果可以在导出前以固定业务输入做断言,例如传入状态码 PAID 后检查导出字段是否为"已支付";公式则应检查生成文件中目标单元格保存了正确的公式文本,并使用 Excel 或兼容软件打开验证重算结果。不要只看首次导出的显示值,因为引用错列、少覆盖最后一行等问题往往要在用户编辑数据后才暴露。
我还会将字段层和区域层分开建模。姓名、状态、日期属于字段层,因此采用 TRANSFORM,不依赖列放在 A 还是 B;总计、排名、达标判断属于区域层,因此采用 FORMULAS,必须明确表头、数据起止行和汇总行。这样的分层让调整表头文案或替换字典时不影响公式,调整计算区域时也不必重写每行转换。
导入场景同样遵循这个边界。TRANSFORM 可将读入单元格标准化为领域字段,例如将日期文本转为统一格式或将展示文案映射为内部编码;FORMULAS 是导出模型中的工作簿规则,不应用它承担导入清洗。对于既需要导出展示又需要后续二次编辑的模板,我先保证源数据字段可追溯,再附加公式列,而不是把源字段覆盖成不可恢复的文案。
注意事项
不要把 sum(${a},${b}) 误写成 Excel SUM(A2:B2):前者属于转换函数目录,后者属于工作簿公式。FORMULAS 坐标受 HEADER=true 和 MAPPING 顺序影响;更改列布局必须复核。字典方向也要按导入、导出分别准备,导出通常是编码到文案。
公式、合并、图表和范围样式需要访问既有行,大批量导出应验证内存表现。转换函数应保持无副作用,不能在逐行执行时隐式访问不稳定外部系统。
实战决策表
在同一份导出规则里,两项能力可以并用,但必须按数据责任划分。TRANSFORM 的键是映射字段名,表达式从当前 JQuickRow 或 JContext 取值;FORMULAS 的键是工作表坐标,表达式由 Excel 客户端保存并计算。测试 XML 已使用 trans(${dict},${gender})、add(${age},1) 和 dateFormat(${enrollmentDate},'yyyy-MM-dd'),公式测试则把 ABS(D2)、AVERAGE(D2:D4)、SUM(D2:D4) 写入 D5。两种写法的外观相似,执行对象却完全不同。
| 判断问题 | 应选能力 | 原因 |
|---|---|---|
| 是否只依赖当前业务行与上下文? | TRANSFORM |
适合编码翻译、文本规范化和导出前派生值。 |
| 是否需要用户修改工作簿后继续联动? | FORMULAS |
单元格保存 Excel 公式,引用变化后可重算。 |
| 是否只是控制日期、金额的显示? | FORMAT |
显示格式不改变业务字段和公式责任。 |
| 是否要聚合多个单元格或写汇总行? | FORMULAS |
以 A1 坐标明确数据区域与结果位置。 |
是否需要读取 JContext 中的字典? |
TRANSFORM |
${dict} 是上下文引用,不是 Excel 名称。 |
一份规则中的分层实例
下面的 XML 让状态先转换为文案,再把金额求和保留为公式;FORMAT 单独规定金额显示。这里故意将三个职责拆开,便于字段口径和表格计算独立调整。
xml
<excel name="exportSettlement" returnClass="void"><![CDATA[
EXPORT WITH
SHEET="结算清单",
HEADER=true,
MAPPING={"customer":"客户","status":"状态","amount":"金额"},
TRANSFORM={
"customer":toUpper(${customer}),
"status":trans(${statusDict},${status})
},
FORMAT={"amount":"currency"},
FORMULAS={
C5:'SUM(C2:C4)',
D5:'IF(C5>1000,"需复核","正常")'
}
]]></excel>
Java 一侧只需把字典放入 JContext,再将数据转换为 JQuickRow。这也是 README 所给 XML 代理模式的职责边界:Java 传入数据、上下文和流,DSL 描述工作表行为。
java
JContext context = new JContext();
context.put("statusDict", statusDict);
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(orderList));
try (OutputStream output = new FileOutputStream("settlement.xlsx")) {
JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(context, rows, output);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
OrderExcelService service = factory.createApi(OrderExcelService.class);
service.exportSettlement("field", "value");
}
执行链路与可维护性
导出过程中,MAPPING 建立字段到列标题的关系,TRANSFORM 针对当前行产生写入值,FORMAT 指定显示模式,FORMULAS 再按坐标设置公式。不要通过将结果字段覆盖为临时展示文案来规避公式;一旦用户编辑金额或追加数据,失去公式的结果不会自动同步。相反,字典翻译若留给公式实现,则会让报表依赖终端用户的客户端能力,也失去 JContext 的明确契约。
导入时只使用 TRANSFORM 做值标准化。导入规则中的 MAPPING 是表头到字段,导出规则中的 MAPPING 是字段到表头,字典方向也必须随业务方向检查。将"已支付"导入为内部编码和将内部编码导出为"已支付"是两件事,不能复用未经验证的字典。
测试应分别覆盖两个层面。转换层用固定输入、固定上下文断言最终单元格值,例如状态码是否被翻译;公式层检查目标单元格保留的公式文本与引用区域,并在 Excel 或兼容客户端中修改输入值验证重算。这样能发现映射列调整、表头行偏移、字典缺项和范围遗漏等不同类别的问题。
注意事项
sum(${a},${b})若来自转换函数提供者,是 Java 求值表达式;SUM(A2:B2)是 Excel 公式。不要按名称相似性混用。TRANSFORM对每一行执行,应保持确定且无副作用;不要在逐行表达式中隐式调用不稳定的远程服务。FORMULAS的 A1 坐标依赖HEADER与MAPPING顺序,增加列、调整表头或插入汇总行都要复核引用。- 公式、合并、图表和范围样式需要访问已写出的行;大批量导出前应按实际数据量评估内存与流式配置。
FORMAT只控制显示。日期想作为真正日期值、金额想保持数值计算能力时,不能仅依赖把它转换成格式化字符串。
发布前检查
评审规则时,可以把字段分成三组:原始字段、展示字段和计算字段。原始字段应可追溯到业务数据;展示字段应说明使用了哪一个 TRANSFORM、上下文字典或 FORMAT;计算字段应写出公式目标、输入范围和统计口径。这个简单清单能防止"为了显示友好而把数值变成文本"以及"为了求和而覆盖原始金额"两类常见问题。
导出后的人工验收也应分别进行。先检查表头和转换后的内容是否正确,再检查工作簿中是否确实保留了 SUM、IF 等公式,最后修改一个输入单元格观察汇总是否重算。只看导出瞬间的显示值,无法证明工作簿具有用户二次编辑所需要的联动能力。
当规则需由多个团队维护时,字典 key、日期格式和金额精度应成为服务契约的一部分。JContext 中的 statusDict 不应依赖调用者临时拼写;公式的坐标不应依赖隐式列排序。通过固定 XML、数据模型和样本工作簿,可以让转换和公式在长期演进中各自保持稳定。
总结
TRANSFORM 解决的是"这个单元格现在写什么",FORMULAS 解决的是"这个单元格以后如何计算"。状态码翻译、姓名拼接等展示值适合在导出时落定;合计、排名这类需要在 Excel 中持续响应修改的逻辑,则应保留公式。两者之间再留出 FORMAT 处理显示,规则才不会互相侵占职责。
排查时先看错误是在写入前出现,还是由工作簿计算产生:前者核对字段、上下文和转换结果,后者核对坐标、引用范围及单元格类型。尤其是日期和金额,若转换后已经成为文本,就不应再把它当作公式的数值输入;保留原始计算列,再设置显示格式,通常更稳妥。