从 XML 到 Excel:JQuick-Excel 代理与解析器如何协作
tags: #JQuickExcel #JavaExcel #开源 #POI #Excel工具
更多示例 : JQuick-Excel 官网示例 https://www.jquick.org/jquick-excel/#demo-formulas0005
简介
很多 Excel 导出代码最终会演化成一段很长的 Java 配置:字段映射、表头、公式、样式和转换逻辑混在同一个方法里,规则一改就要重新发版。JQuick-Excel 提供的 XML 入口适合把这部分"报表声明"抽离出来:Java 仍负责准备数据、上下文和文件流,XML 描述某个接口方法应执行的导入或导出规则。
本文以仓库现有的 JQuickXmlFactory、JQuickExcelExportXmlParseFactory 和 JQuickExcelImportXmlParseFactory 为依据,梳理 XML 代理与解析器的职责边界,并给出可以直接改造成项目测试的导入、导出示例。
前言
我接手报表需求时,通常先分清两件事:规则是不是业务配置,数据和资源是不是运行期输入。前者适合 XML,后者应由 Java 明确传入。这样做并不是为了"所有内容 XML 化",而是让接口方法成为稳定调用点,避免调用方知道 DSL、工作簿名或解析细节。
从当前源码能够确认:JQuickExcelExportXmlParseFactory 接收 List<JQuickRow>、OutputStream,也可额外接收主题编码和 JContext;JQuickExcelImportXmlParseFactory 接收 InputStream,也可接收 JContext。两者都实现 JQuickParseHandler,并创建 JQuickJavaExcelElement 与对应的调用处理器。JQuickXmlFactory 再根据 XML 生成服务接口代理。至于代理框架内部如何解析每一个 XML 节点,不宜脱离依赖源码臆测;本文只讨论项目可验证的协作契约。
环境与依赖
项目的 pom.xml 已声明 jquick-excel 自身依赖:jquick-xmlProxy、Apache POI、ANTLR 运行时和 jquick-transform-function。因此实际工程中应首先引入本库对应版本,而不是只复制下面的类名。
示例还使用下列仓库已有类型:
com.github.paohaijiao.xml.factory.JQuickFactorycom.github.paohaijiao.xml.factory.JQuickXmlFactorycom.github.paohaijiao.xml.handler.JQuickParseHandlercom.github.paohaijiao.xml.ex.JQuickExcelExportXmlParseFactorycom.github.paohaijiao.xml.im.JQuickExcelImportXmlParseFactorycom.github.paohaijiao.statement.JQuickRowcom.github.paohaijiao.param.JContext
把 XML 放入 src/main/resources 或 src/test/resources,并确保 namespace 指向接口的全限定名。当前测试资源中的根节点是 <excels namespace="...">,每个 <excel name="..."> 对应接口的一个方法名。
代码示例
先定义服务接口。它不承载业务实现,只定义代理需要暴露的方法;参数上使用已有的 @Param,使 XML 调用可按名称接收参数。
java
package demo;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.param.Param;
import java.util.List;
public interface StudentExcelService {
void exportExcel(@Param("field") String field, @Param("value") String value);
List<JQuickRow> importExcel(@Param("field") String field, @Param("value") String value);
}
创建 student-excel.xml。导出和导入规则均放在 CDATA 中,避免 DSL 与 XML 转义互相干扰。此处的 TRANSFORM 使用 README 已列出的内置函数:trans 和 dateFormat。
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="demo.StudentExcelService">
<excel name="exportExcel" returnClass="void"><![CDATA[
EXPORT WITH SHEET="学生表", HEADER=true,
MAPPING={"id":"学号","name":"姓名","gender":"性别","enrollmentDate":"入学日期"},
TRANSFORM={
"gender":trans(${dict},${gender}),
"enrollmentDate":dateFormat(${enrollmentDate},'yyyy-MM-dd')
}
]]></excel>
<excel name="importExcel" returnClass="java.util.List"><![CDATA[
IMPORT WITH SHEET="学生表", HEADER=true,
MAPPING={"学号":"id","姓名":"name","性别":"gender","入学日期":"enrollmentDate"},
TRANSFORM={"gender":trans(${dict},${gender})}
]]></excel>
</excels>
下面的 Java 演示以 Map 构造两行数据,再通过 JQuickRow.toRows(JObjectConverter.convert(data)) 转为导出解析器所需格式。这个用法来自仓库测试 JQuickExcelDemo。
java
package demo;
import com.github.paohaijiao.param.JContext;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.util.JObjectConverter;
import com.github.paohaijiao.xml.ex.JQuickExcelExportXmlParseFactory;
import com.github.paohaijiao.xml.factory.JQuickFactory;
import com.github.paohaijiao.xml.factory.JQuickXmlFactory;
import com.github.paohaijiao.xml.handler.JQuickParseHandler;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.*;
public class XmlExportDemo {
public static void main(String[] args) throws Exception {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", "1001");
row.put("name", "张三");
row.put("gender", "1");
row.put("enrollmentDate", new Date());
List<Map<String, Object>> data = Collections.singletonList(row);
Map<String, Object> dict = new HashMap<>();
dict.put("1", "男");
JContext context = new JContext();
context.put("dict", dict);
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(data));
try (OutputStream output = new FileOutputStream("students.xlsx")) {
JQuickParseHandler parser =
new JQuickExcelExportXmlParseFactory("jade", context, rows, output);
JQuickFactory factory = new JQuickXmlFactory(parser, "student-excel.xml");
StudentExcelService service = factory.createApi(StudentExcelService.class);
service.exportExcel("field", "value");
}
}
}
导入只替换解析器和流方向。JContext 的字典要按源单元格值到目标值的方向建立,例如 Excel 写"男",导入结果要为"1"时使用 "男" -> "1"。
java
try (InputStream input = new FileInputStream("students.xlsx")) {
Map<String, Object> dict = new HashMap<>();
dict.put("男", "1");
JContext context = new JContext();
context.put("dict", dict);
JQuickParseHandler parser = new JQuickExcelImportXmlParseFactory(context, input);
JQuickFactory factory = new JQuickXmlFactory(parser, "student-excel.xml");
StudentExcelService service = factory.createApi(StudentExcelService.class);
List<JQuickRow> rows = service.importExcel("field", "value");
System.out.println(rows.size());
}
原理说明
我把这条链路理解为四个稳定层次。第一层是服务接口:调用方只依赖 StudentExcelService,并不直接操作工作簿。第二层是 XML:namespace 锁定接口,excel/@name 锁定方法,CDATA 中保存 EXPORT WITH 或 IMPORT WITH 规则。第三层是解析器工厂:导出解析器持有行数据、输出流、可选主题与上下文;导入解析器持有输入流和上下文。第四层是 JQuickXmlFactory 创建的 API 代理:方法调用被交给与 XML 规则对应的处理流程。
这意味着 Java 和 XML 的分工很清楚。Java 负责确定文件来自哪里、输出到哪里、当前数据是什么、字典等运行时变量为何值;XML 负责决定工作表、表头、映射、转换和样式。JContext 是两边的桥梁,但它不替代接口参数。对于真正影响调用语义的输入,仍应通过接口方法参数表达;对于规则求值使用的字典、租户配置或日期格式,可放入上下文。
从当前解析器类还能看到几个可用构造方式。导出可只传 rows 与 OutputStream,也可传主题编码,或传 JContext,或同时传两者。导入可只传 InputStream,也可加 JContext。这些重载足以覆盖简单文件转换和带字典、带主题的场景,不必为此自行封装另一套代理入口。
注意事项
第一,XML 的 namespace、excel/@name 与 Java 接口必须一致。接口改名而 XML 未同步时,代理无法按预期定位规则。第二,资源文件必须位于类路径;new JQuickXmlFactory(parser, "student-excel.xml") 传的是资源名,不是任意磁盘绝对路径。
第三,导出解析器构造时会校验行数据和输出流非空,导入解析器会校验输入流非空;调用方应使用 try-with-resources 管理文件流。第四,JContext 中的映射方向取决于导入还是导出,不能复用同一份字典就假设结果正确。
第五,README 的历史代码片段中存在与当前仓库不同的包路径;实践中应优先采用当前源码测试已使用的 com.github.paohaijiao.xml.factory、xml.ex、xml.im 包名。第六,接口里的 field、value 仅是现有测试使用的示例参数,不应据此推断所有 XML 规则都会使用它们;不用的参数可保留在示例接口之外,按自己的 XML 规则设计。
运行步骤与工程边界
我建议先在 src/test/resources 放入 XML,再用两行 LinkedHashMap 数据完成一次本地导出。运行前确认 XML 已被构建到 classpath,namespace 是接口完整类名,excel/@name 与调用方法同名。导出时使用 JQuickRow.toRows(JObjectConverter.convert(data)) 得到行数据,创建 JQuickExcelExportXmlParseFactory,然后通过 JQuickXmlFactory 创建 API;文件生成后用 Excel 打开,检查工作表、表头、映射和转换结果。导入测试则使用同一份已生成文件或受控样例,反转字典方向,创建 JQuickExcelImportXmlParseFactory 后断言返回行数和字段值。这个最小闭环比直接把复杂模板接入生产更容易定位问题。
从源码看,导出工厂的四参构造器会拒绝空 rows 和空 OutputStream,并只在传入上下文非空时复制其内容;导入的单参构造器要求输入流非空,双参构造器还要求上下文非空。我的应用层会在创建工厂前完成这些前置判断,而不会把空集合、已关闭流或空上传流交给框架。空数据是否允许生成只有表头的文件,是报表接口的业务决策;若需要该行为,应通过实际版本测试确认,而不是从构造器不为空推断输出效果。
资源和对象的生命周期也要清楚。XML 是可随应用发布的静态规则,接口是编译期契约;JContext、List<JQuickRow>、输入流和输出流则属于单次操作。Web 场景中不要把解析器、代理或流缓存为单例字段。批任务中也应让每个文件拥有独立的流与上下文,处理完成及时关闭流。主题参数只应传入 README 已列出的主题编码,并在生成文件中检查局部 STYLE 与主题效果是否符合预期。
当代理调用失败时,我会依次排查资源名、XML 的 namespace、excel/@name、接口返回类型、DSL 语法和运行期上下文。不要把 XML 资源路径写成开发机绝对路径,也不要假定接口参数会自动参与任何规则;只有 XML 实际引用的参数和上下文值才有意义。字典转换尤其要用双向测试覆盖:导出验证编码到显示值,导入验证显示值到编码,避免用同一张映射表制造看似成功但值方向错误的结果。
总结
XML 代理的价值不在于隐藏所有 Excel 细节,而在于把稳定调用契约与可变报表规则分开。JQuickXmlFactory 负责从 XML 创建接口代理,导出和导入解析器分别提供数据流、文件流、上下文及可选主题,接口方法是应用层唯一需要调用的入口。
我建议先用一份小 XML 和一个两行数据的测试打通这条链路,再逐步加入 TRANSFORM、公式、样式和图表规则。这样出问题时能快速判断是资源定位、接口匹配、上下文数据还是 DSL 规则导致的,也能让报表配置的迭代保持可控。