从 XML 到 Excel:JQuick-Excel 代理与解析器如何协作

从 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.JQuickFactory
  • com.github.paohaijiao.xml.factory.JQuickXmlFactory
  • com.github.paohaijiao.xml.handler.JQuickParseHandler
  • com.github.paohaijiao.xml.ex.JQuickExcelExportXmlParseFactory
  • com.github.paohaijiao.xml.im.JQuickExcelImportXmlParseFactory
  • com.github.paohaijiao.statement.JQuickRow
  • com.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 规则导致的,也能让报表配置的迭代保持可控。

相关推荐
优选资讯6 小时前
Excel 批量打印标签的完整流程
excel
雪宫街道7 小时前
SpringBoot - POI实现Excel 导入与导出
spring boot·后端·excel
Data-Miner1 天前
数据分析,除了Excel数据透视表,还有什么工具?——2026 专业精选推荐榜
microsoft·数据分析·excel
流烟默1 天前
Spring Boot 异常响应被序列化为 XML 的排查与修复
xml·spring boot·后端
mmsx1 天前
Android 地图上绘制 KML:解析、样式与渲染
android·xml
SunnyDays10111 天前
使用 Java 高效管理 Excel 批注:添加、读取、修改和删除
java·开发语言·excel
福兮说1 天前
AI 把工号整理成 Excel 后,前面的 0 还在吗?我会先核对这三列
excel
不会C语言的男孩1 天前
libxml2 源码编译与 XML 读取验证(x86 / ARM)
xml·c语言·嵌入式
工具派2 天前
JSON转Excel后18位订单号不对了?数字精度与类型边界实测
json·excel