Spring 项目中如何组织 JQuick-Excel 的 XML 与 Bean
tags: #JQuickExcel #JavaExcel #SpringBoot #开源 #POI #Excel工具
简介
我是 JQuick-Excel 的作者。在 Spring 项目里接入 Excel,最容易失控的地方往往不是控制器,而是散落在业务代码里的字段映射、资源路径和流对象。本篇给出我实际建议的组织方式:把 JQuick-Excel DSL 放进 resources,把接口与 Spring Bean 分层管理,并始终通过 JQuickXmlFactory、导入/导出解析器执行规则。
前言
我不会把 JQuick-Excel 当成一个需要"注解扫描后自动生效"的 Spring 组件。根据 README-CN 和仓库测试,核心调用链是明确的:Java 准备 JQuickRow、JContext 与输入或输出流;JQuickExcelExportXmlParseFactory 或 JQuickExcelImportXmlParseFactory 保存本次执行所需资源;JQuickXmlFactory 从 classpath XML 创建接口代理;代理再按方法名执行 EXPORT WITH 或 IMPORT WITH DSL。
因此,我在 Spring 工程中把 Spring 的职责限定为依赖注入和 HTTP/任务调度,把 Excel 规则留在 XML。这里没有使用、也没有假定存在任何 JQuick-Excel 的 Spring Boot starter、自动配置、@Enable... 注解或自动注册 Bean。即使项目本身采用 Spring Boot,JQuick-Excel 的核心仍由下面展示的工厂、解析器和 resources XML DSL 显式调用。
一个实用的目录划分如下:服务接口放在 com.example.report.excel;DSL 放在 src/main/resources/excel/student-excel.xml;负责组装运行时数据的 Spring Bean 放在 service 包。接口和 XML 是稳定契约,业务 DTO、下载流、上传流则是每次请求不同的输入。
环境与依赖
本文以 Java 8+、Spring Framework 或 Spring Boot Web 环境为前提。JQuick-Excel 使用 Maven 坐标 io.github.paohaijiao:jquick-excel:3.6.0;Apache POI 是它的项目依赖,不需要为了本文另写一套 POI 读写代码。
xml
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-excel</artifactId>
<version>3.6.0</version>
</dependency>
将 XML 放入 src/main/resources/excel/student-excel.xml。JQuickXmlFactory 接收的是 classpath 资源名,所以后续 Java 必须传入 excel/student-excel.xml,而不是开发机绝对路径。namespace 必须等于接口全限定名,excel/@name 必须等于接口方法名。
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.report.excel.StudentExcelApi">
<excel name="exportStudents" 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="importStudents" returnClass="java.util.List"><![CDATA[
IMPORT WITH
SHEET="学生表",
HEADER=true,
MAPPING={"学号":"id","姓名":"name","性别":"gender","入学日期":"enrollmentDate"},
TRANSFORM={"gender":trans(${dict},${gender})}
]]></excel>
</excels>
代码示例
我先定义一个只表达 XML 代理契约的接口。README-CN 与测试均使用 @Param 和 List<JQuickRow>;下面没有杜撰 DTO 自动转换 API。
java
package com.example.report.excel;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.param.Param;
import java.util.List;
public interface StudentExcelApi {
void exportStudents(@Param("field") String field, @Param("value") String value);
List<JQuickRow> importStudents(@Param("field") String field, @Param("value") String value);
}
我把代理创建封装在 Spring 服务中,但没有把解析器或代理声明为单例 Bean。它们携带当前请求的行数据和流,复用会把不同请求的状态混在一起。导出每次新建解析器、工厂和 API,然后立即调用。
java
package com.example.report.service;
import com.example.report.excel.StudentExcelApi;
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 org.springframework.stereotype.Service;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class StudentExcelService {
public void export(List<Map<String, Object>> students, OutputStream output) {
List<JQuickRow> rows = JQuickRow.toRows(JObjectConverter.convert(students));
JContext context = new JContext();
Map<String, Object> dict = new HashMap<>();
dict.put("0", "男");
dict.put("1", "女");
context.put("dict", dict);
JQuickParseHandler parser =
new JQuickExcelExportXmlParseFactory(context, rows, output);
JQuickFactory factory = new JQuickXmlFactory(parser, "excel/student-excel.xml");
StudentExcelApi api = factory.createApi(StudentExcelApi.class);
api.exportStudents("field", "value");
}
}
导入同样由同一个服务接收 InputStream,创建 JQuickExcelImportXmlParseFactory(context, input),再调用 api.importStudents。我通常让控制器或应用服务完成 JQuickRow 到领域对象的校验和保存,避免把持久化逻辑塞进 XML。
原理说明
XML 中的 MAPPING 在导出时是"内部字段到表头",导入时是"表头到内部字段"。HEADER=true 表示生成或识别第一行表头。TRANSFORM 中的 ${dict} 来自 JContext,导出字典方向为编码到显示值;导入应反过来,例如"男"到"0"。同名 dict 可以存在,但不能不经转换就复用同一张映射表。
Spring Bean 的价值是把数据查询、字典准备和事务边界放在熟悉的应用层;它不替代 XML DSL。每次执行仍由 JQuickXmlFactory 读取资源,导入/导出解析器分别绑定数据流和文件流。这样报表字段、表头和转换变更集中在一个 resources 文件,HTTP、定时任务和消息消费等入口则可以共用 Spring 服务。
注意事项
- 不要把
OutputStream、InputStream、导出解析器或接口代理缓存为单例字段;它们与单次文件处理绑定。 excel/student-excel.xml必须被构建工具打进 classpath,部署包中遗漏资源会导致工厂无法定位 XML。- 接口全限定名、
namespace、方法名、returnClass必须同时对齐;改包名时尤其容易漏改 XML。 - DSL 只使用 README-CN 和测试 XML 已出现的
EXPORT WITH、IMPORT WITH、SHEET、HEADER、MAPPING、TRANSFORM与dateFormat、trans。 - 本文未声称存在 JQuick-Excel Starter 或自动配置;Spring 注解仅来自标准 Spring 框架。
运行步骤与工程化组织
我在一个新的 Spring 工程中会先建立稳定的资源契约,再写 Controller。第一步,将接口放入普通 Java 包,将 XML 放入 src/main/resources/excel,并让 namespace 和接口全限定名一致。第二步,写一个只接收 List<Map<String, Object>> 与 OutputStream 的导出服务,以及一个只接收 InputStream 的导入服务;二者内部每次调用都创建新的 JContext、解析器、JQuickXmlFactory 和 API 代理。第三步,用 JUnit 或现有集成测试生成一个最小 xlsx,再以该文件反向导入,验证工作表名、表头、字段映射和字典方向。只有该闭环通过后,再把服务接到 HTTP、定时任务或消息消费入口。
Bean 的边界比注解数量重要。数据查询、权限过滤、字典查询和事务由 Spring 服务负责;XML 资源名、接口名、DSL 规则由 Excel 适配层负责;Web Controller 只做请求解析、响应协议和调用编排。不要将 InputStream、OutputStream、JQuickParseHandler、JQuickFactory 或代理对象注入为单例字段,它们绑定的是一次文件处理。可以注入稳定的查询服务和配置对象,但应在方法体内根据当前请求的数据和流组装 JQuick-Excel 对象。
多份报表的组织方式也应保持可扫描。我的惯例是一个 XML 对应一个接口,资源名与报表语义一致,例如 excel/student-excel.xml 和 StudentExcelApi;版本升级时只复制并改名规则文件,不覆盖仍被旧接口使用的模板。公共字典键可以统一约定,例如 dict,但每个操作仍要明确编码到显示值还是显示值到编码。对于租户、语言或日期格式等运行期差异,应在 Spring 服务层准备 JContext,不要通过修改 classpath XML 来承载每个请求的动态状态。
构建与部署时,我会检查最终 jar 是否包含 excel/*.xml 以及 DTD 所需的 classpath 资源。IDE 直接运行成功并不能证明打包成功,资源过滤、重命名或模块间依赖范围都可能让部署包缺少 XML。发布前至少执行一次打包后的启动测试,并调用导出、导入各一次。出现"代理找不到规则"时,先看资源名、接口包名和 XML 的 excel/@name,不要立即把问题归因给 Spring Bean 扫描,因为本方案本来就不依赖 JQuick-Excel 自动扫描。
在事务上,我把文件解析和持久化分开考虑。导入得到 List<JQuickRow> 后,应用层应先完成字段校验、重复数据判断和错误聚合,再决定是否进入写库事务;不要在读取到每一行时立即提交不可逆业务操作。导出则先完成权限与数据范围校验,再取得响应流并开始写入;HTTP 响应开始提交后无法可靠替换成结构化错误 JSON,因此耗时查询和可预见的校验应尽量前置。以上都属于标准 Spring 应用组织,并不要求任何 JQuick-Excel Starter 或自动配置。
总结
我在 Spring 中采用"XML 管规则、Bean 管运行时资源"的方式:resources XML 定义导入导出 DSL,接口定义代理契约,Spring 服务在每次调用时创建 JQuickXmlFactory 与对应解析器。这样既保留 Spring 的组织能力,也不掩盖 JQuick-Excel 真正的执行入口,后续接入下载、上传或批量任务时都能复用同一套规则。