JQuick-Excel FORMULAS 实战:把计算规则留在工作簿中
tags: #JQuickExcel #JavaExcel #开源 #POI #Excel工具
简介
我在导出经营明细、成绩单和对账表时,常把应由 Excel 继续计算的规则写进 FORMULAS。本文以项目作者视角说明公式目标、引用范围与 Java 导出链路,避免把公式计算和字段转换混为一谈。
前言
报表中的计算有两类:一类是导出前已经确定的业务值,例如将编码转成中文;另一类是交付给使用者后仍要随着单元格编辑而重算的值,例如总计、平均值、排名和状态判断。前者适合 TRANSFORM,后者适合 FORMULAS。我设计 JQuick-Excel 时,将公式作为导出 DSL 的独立能力,目的就是让 XML 同时描述表头、数据和工作簿内的计算规则。
FORMULAS 的左侧是写入公式的目标,右侧是 Excel 公式文本。目标可使用单元格、行、列和矩形范围;README 已确认的写法包括 D5、ROW 5、ROW 1..10、COL A:、COL A..D:、C1:、A1:B5。公式本身采用 Excel 函数语义,例如 SUM、AVERAGE、ROUND、IF、TODAY。我建议先固定 MAPPING 的列顺序,再根据实际输出坐标书写公式。
环境与依赖
运行环境为 JDK 8+,Maven 依赖固定为 3.6.0。XML 服务定义放在类路径,Java 侧使用 JQuickExcelExportXmlParseFactory、JQuickXmlFactory 创建代理。
xml
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-excel</artifactId>
<version>3.6.0</version>
</dependency>
示例中 A 到 C 列分别是产品、数量和单价,D 列由公式计算金额。第 2 至第 4 行是数据,第 5 行是汇总行;这些坐标包含表头行,因此不能按 Java 集合下标猜测。
代码示例
我把金额公式和汇总公式写进同一份导出规则。D2:D4 的区域公式为每条明细计算金额,D5 则对结果求和;E5 演示平均值的保留两位小数。XML 片段可放入常规 jquick-excel.xml 的 <excel> 节点。
xml
<excel name="exportOrder" returnClass="void"><![CDATA[
EXPORT WITH
SHEET="订单明细",
HEADER=true,
MAPPING={"product":"产品","quantity":"数量","price":"单价"},
FORMULAS={
D2:D4:'B2*C2',
D5:'SUM(D2:D4)',
E5:'ROUND(AVERAGE(D2:D4),2)',
F5:'IF(D5>1000,"重点订单","普通订单")'
},
STYLE={ROW 1:{bold:true}}
]]></excel>
Java 不需要逐单元格调用 POI。我的职责是准备业务行、输出流和 XML 代理,计算规则由 DSL 在工作簿构建阶段执行。
java
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(orderList));
try (OutputStream output = new FileOutputStream("orders.xlsx")) {
JQuickParseHandler parser = new JQuickExcelExportXmlParseFactory(rows, output);
OrderExcelService service = new JQuickXmlFactory(parser, "jquick-excel.xml")
.createApi(OrderExcelService.class);
service.exportOrder("field", "value");
}
除了聚合,README 还确认了数学、日期、文本与逻辑查询公式。比如 F2:'RANK(D2,D2:D4)' 可以排序,G5:'TODAY()' 写出当天日期,H2:'CONCAT(A2,"-",B2)' 拼接文本。公式字符串使用单引号包裹,内部文本常量使用双引号,能减少 DSL 分隔符歧义。
原理说明
导出时框架先按 MAPPING 写表头和数据,再定位 FORMULAS 指定的工作表坐标并设置公式。单元格保存的是公式而不是预先算好的常量,因此最终值由 Excel 或兼容客户端计算。这个行为使用户修改 B、C 列后,D、E、F 列仍能保持联动,也使公式可以继续被审计和调整。
FORMULAS 的表达式不是 Java 方法调用。SUM(D2:D4)、IF(D5>1000,"重点订单","普通订单") 是 Excel 公式;它们使用 A1 引用和 Excel 函数名。与之相对,TRANSFORM 处理的是当前行字段及 JContext 值,写法是 toUpper(${name}) 或 trans(${dict},${gender})。两者的执行阶段、输入来源和结果可编辑性都不同。
我会把汇总单元格放在数据区之后,并把汇总范围明确写死。业务若允许任意数量的行,应在生成规则前规划固定区域,或采用与实际数据布局一致的行/列目标;不能把一个只覆盖三行的示例范围直接复制到生产报表。公式依赖列位置,因此字段新增、调整映射顺序时必须同时复核引用。
注意事项
第一,公式目标和引用都以 Excel 坐标为准,HEADER=true 会占用第 1 行。第二,D2:D4:'B2*C2' 中每个目标单元格都写入同一公式文本;如需按行变化的引用,先用小样本验证生成结果。第三,公式值由客户端重新计算,服务端不应把它当作即时可用的 Java 计算结果。
第四,README 明确提示公式、范围样式、合并和图表需要回头访问已写出的行;它们与纯流式的大批量导出存在内存权衡。大文件场景先做样本压测。第五,不要用 FORMULAS 承担字典翻译、脱敏等导出前业务清洗,这些逻辑应使用 TRANSFORM,以免依赖终端 Excel 的计算环境。
实战拆解
从测试规则建立坐标表
项目测试中的学生表使用 HEADER=true,映射依次为主键、姓名、性别、年龄、入学时间、班级和忽略字段;因此年龄位于 D 列,三条数据占用 D2:D4,统计值写在 D5。这个例子说明公式坐标并不来自 Java 对象字段名,而来自已经落到工作表上的列顺序。先把表格布局写成一张坐标表,再编写 FORMULAS,可以避免"字段存在但引用了错误列"的问题。
| 工作表位置 | 含义 | 公式设计要点 |
|---|---|---|
| 第 1 行 | 表头 | HEADER=true 时不属于明细数据。 |
| 第 2 至 4 行 | 三条示例明细 | 作为 SUM、AVERAGE、COUNT 等范围的输入。 |
| D 列 | 年龄或数值列 | 测试分别覆盖 ABS、MAX、MIN、POWER、RANK、ROUND、STDEV 和 SUM。 |
| D5 | 汇总或演示结果 | 不能覆盖仍需写入的业务明细。 |
在实际订单中,也应先固定"表头行、数据起始行、数据结束行、统计行、辅助列"的责任。若一份模板预留了 500 行明细,公式范围可以明确写为预留区域;若导出行数完全动态,则应由生成规则的一方确保坐标与数据量一致。框架不会根据 MAPPING 的字段名替调用方猜测最后一条数据所在行。
公式目标的使用方式
README 规定目标可以表示一个单元格、单行、行区间、单列、列区间或矩形区域。单元格目标适合汇总和固定提示;范围目标适合批量写入公式。范围只负责选中单元格,公式文本仍是 Excel 的 A1 语法。下面展示项目已说明的目标形式,实际使用前应以生成工作簿验证公式引用是否符合预期。
xml
<excel name="exportMetrics" returnClass="void"><![CDATA[
EXPORT WITH
SHEET="指标",
HEADER=true,
MAPPING={"name":"名称","value":"数值"},
FORMULAS={
C5:'SUM(B2:B4)',
D5:'ROUND(AVERAGE(B2:B4),2)',
E5:'COUNT(B2:B4)'
}
]]></excel>
公式右侧必须是由单引号包裹的 Excel 公式字符串。公式中的文本字面量使用双引号,例如 IF(C5>0,"有数据","无数据")。不要把 Java 字符串转义规则、XML CDATA 边界与 Excel 公式引号混在一起:XML 中建议放在 CDATA 内,Java 规则字符串则需要按照 Java 自身规则转义双引号。
可验证的 Java 导出链路
测试通过 JQuickExcelCommonExportExecutor 解析规则,得到 JExcelExportModel 后交给 JExcelExportHandler。这一链路便于在不依赖 XML 代理的单元测试中验证公式 DSL;生产场景可以继续使用 XML 代理,二者的规则内容一致。
java
String rule = "EXPORT WITH SHEET=\"学生表\", HEADER=true, "
+ "MAPPING={\"age\":\"年龄\"}, "
+ "FORMULAS={A5:'SUM(A2:A4)'}";
JQuickExcelCommonExportExecutor executor = new JQuickExcelCommonExportExecutor();
JExcelExportModel config = (JExcelExportModel) executor.execute(rule);
JExcelExportHandler handler = new JExcelExportHandler(config, JQuickRow.toRows(data));
try (OutputStream output = new FileOutputStream("students.xlsx")) {
handler.getWorkBook().write(output);
}
验收不应只检查文件存在。应打开工作簿,确认目标单元格是公式单元格,公式文本与规则一致;再修改被引用的数值单元格,确认客户端能重新计算。对于日期函数、随机函数和依赖客户端计算设置的函数,首次显示值可能随打开环境变化,测试重点应放在公式文本和引用关系。
函数选择与组合边界
README 已列出数学、日期时间、文本、逻辑与查找函数。数学类可用于 ABS、AVERAGE、COUNT、MAX、MIN、POWER、RAND、RANK、ROUND、SQRT、STDEV、SUM;日期类包括 TODAY、NOW、YEAR、MONTH、NETWORKDAYS 等;文本类包括 CONCAT、LEFT、TEXT、UPPER;逻辑查询类包括 IF、AND、OR、LOOKUP。应按 Excel 公式语义组合这些函数,不能把转换函数目录中的 add(...)、sum(...) 当作公式 DSL 的函数。
对金额报表,常见的职责划分是:源字段通过 TRANSFORM 完成字典翻译或日期转换,FORMAT 提供数字、货币和日期显示格式,FORMULAS 写入金额计算和汇总。这样修改展示文案时不会影响财务计算引用;修改合计逻辑时也不会污染源字段。
注意事项
- 公式坐标从工作表左上角的 A1 开始计算,
HEADER=true会占用第一行;在规则变更评审中,MAPPING顺序与FORMULAS必须作为同一项检查。 - 当前实现会在写入后的工作表上处理绝对引用、行列范围、样式、合并和图表。源码提示这些能力需要回头访问已写出的行,流式或大批量模式下不能假定它们与任意规模数据都兼容。
- 公式保留在文件中,不等同于服务端完成了计算。下游若要在 Java 中读取结果,应明确其读取时机和计算策略,不能把尚未由客户端重算的缓存值当作业务结论。
- 合并区域、页脚、图表数据区和公式统计区应分开规划。特别是汇总行之后追加页脚时,要避免页脚覆盖公式目标或被统计范围误纳入。
- 示例中的
D2:D4只覆盖三条测试数据。复制到生产规则前,必须按实际明细行数、预留行数或生成策略重算范围。
发布前检查
在发布报表规则前,建议将每一个公式目标列入检查表:目标单元格是否为空闲区域、公式引用是否只包含预期明细、列顺序是否与 MAPPING 一致、数字列是否仍是数值、日期列是否采用正确显示格式。对 IF、LOOKUP 等包含文本分支的公式,还要检查中文或业务文案的引号是否完整。规则保存于 XML 时,CDATA 负责隔离 XML 特殊字符,但不会修复 Excel 公式本身的语法错误。
对于有多个工作表的交付件,SHEET 名称、工作表内坐标和业务数据集必须一一对应。不要让一份"订单"公式规则在字段相似的"退款"表上复用,而没有重新审查金额列、统计行和状态文案。公式可审计的前提是业务人员能将一个坐标追溯到一列明确的业务含义。
回归样本至少应包含零、负数、小数、空值和边界金额。SUM 与 AVERAGE 的结果要同业务侧的独立计算比对;RANK 要覆盖并列值;ROUND 要覆盖精度;日期函数要确认时区和打开日期不会被误解。这样既验证 DSL 被正确解析,也验证 Excel 公式在最终使用环境中可读。
总结
公式真正的价值,在于把计算规则交还给工作簿,而不是把 Java 已经算出的结果再写一次。汇总、排名和条件判断这类需要业务人员继续查看或修改的逻辑,保留为 FORMULAS 会比预计算更容易追踪;前提是公式引用的坐标随布局一起维护。
验收时不只看单元格里是否写入了公式文本,还要用包含首行、末行和空值的样本打开文件,核对计算范围及结果。表头、明细起始行、汇总行一旦调整,就应把它当作计算规则变更重新验证,避免得到外观正常但引用错位的报表。