JQuick-Excel 实战:用 VALIDATION 建立可维护的 Excel 导入校验
tags: Java, Excel, JQuick-Excel, 数据导入, VALIDATION, XML DSL
简介
Excel 导入的难点通常不在"能否读到单元格",而在于如何在业务数据进入应用之前,把模板中明显不符合约定的内容拦住。JQuick-Excel 在 IMPORT WITH 中提供 VALIDATION,用于把校验规则应用到选定的工作表位置。README-CN 已明确:规则目标可以是行、列、单元格或矩形范围;导入规则还可以组合 SHEET、HEADER、MAPPING 与 TRANSFORM。这使得表头、字段映射和输入约束能够写在同一份 XML 服务定义中。
本文只以 README-CN 和项目的 src/test/resources/jquick-excel.xml 为事实依据。重点是解释已确认的范围语法、20 个规则名称及其参数形式,并给出 XML 和 Java 调用示例。文中不引入未确认的规则名、异常类型、回调机制或外部模板语法。对于数据库重复、权限、关联数据和事务等问题,仍应由导入后的 Java 业务逻辑负责。
前言
面向业务人员的 Excel 模板既方便又开放。用户可以复制内容、修改列顺序、手输日期、把数值输入为文本,也可以在同一列混用空值和非空值。如果应用只是把工作簿读成对象,再直接交给后续服务,错误会被推迟到很晚才暴露:日期转换失败、数字无法参与计算、字典外的值进入业务流程,或者长文本在下游接口处被拒绝。
将基础校验写进导入 DSL 的价值,在于让模板约定可见。SHEET 指明工作表,HEADER=true 声明首行是表头,MAPPING 将表头映射到目标字段,VALIDATION 再按 Excel 坐标选择需要检查的位置。使用者看到的模板、测试人员检查的规则、开发人员加载的 XML,围绕的是同一份配置契约。
校验应保持责任边界。单元格格式、必填、长度、日期范围、数值范围、邮箱、手机和字典成员关系,适合在 VALIDATION 中描述。工号是否已存在、当前操作者是否有权限、关联组织是否存在、同一文件是否存在跨行冲突以及落库事务,则不应被误认为单元格规则可以替代。导入通过后拿到 List<JQuickRow>,业务层仍需要继续执行系统一致性检查。
README-CN 给出了完整的基础范围示例:ROW 5、ROW 1..10、COL A、COL A..D、C1、A1:B5。因此配置前的第一步不是挑选规则名,而是确定模板的真实布局。只要表头从第一行改到第二行,原来的 A2:A100 就可能需要整体移动。规则本身正确而坐标错误,仍然会导致错误的导入结果。
环境与依赖
本文使用 Java 8 或更高版本,以及 JQuick-Excel 3.6.0。README-CN 明确该版本支持 xls 与 xlsx 工作簿,Maven 坐标如下。
xml
<dependency>
<groupId>io.github.paohaijiao</groupId>
<artifactId>jquick-excel</artifactId>
<version>3.6.0</version>
</dependency>
XML 文件应放在类路径中。README-CN 的示例使用 <excels> 的 namespace 指定服务接口,并通过 <excel name="..."> 将 DSL 规则绑定到接口方法。CDATA 可避免 XML 解析器把 DSL 内的花括号、引号和 ${...} 当作普通 XML 内容处理。
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.excel.EmployeeExcelService">
<excel name="importEmployees" returnClass="java.util.List">
<![CDATA[
IMPORT WITH
SHEET='员工导入',
HEADER=true,
MAPPING={"工号":"code","姓名":"name","年龄":"age"}
]]>
</excel>
</excels>
导入时,README-CN 已确认 JQuickExcelImportXmlParseFactory 接收 JContext 和输入流,JQuickXmlFactory 根据 XML 创建服务代理。JContext 可提供转换所需的上下文值;即使本次校验没有使用字典,也可以沿用同一调用结构。输入流应在代理方法执行期间保持可用,因此使用 try-with-resources 管理更稳妥。
java
import com.github.paohaijiao.context.JContext;
import com.github.paohaijiao.statement.JQuickRow;
import com.github.paohaijiao.xml.JQuickFactory;
import com.github.paohaijiao.xml.JQuickXmlFactory;
import com.github.paohaijiao.xml.parse.JQuickParseHandler;
import com.github.paohaijiao.xml.parse.excel.JQuickExcelImportXmlParseFactory;
import java.io.InputStream;
import java.util.List;
try (InputStream input = getClass().getClassLoader()
.getResourceAsStream("employees.xlsx")) {
JContext context = new JContext();
JQuickParseHandler parser = new JQuickExcelImportXmlParseFactory(context, input);
JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
EmployeeExcelService service = factory.createApi(EmployeeExcelService.class);
List<JQuickRow> rows = service.importEmployees("field", "value");
consume(rows);
}
以上 Java 代码遵循 README-CN 的 XML 导入链路。示例中的 field 与 value 是已公开接口示例中的参数,不应被理解为 VALIDATION 的隐含变量。规则是否命中依赖 XML 是否加载、方法名是否匹配、工作表和表头是否与 DSL 一致,以及范围是否指向正确位置。
代码示例
下面是一份围绕员工导入模板的规则。它只使用 README-CN 已列出的 regex、max_length、email、integer、date_format、dict 与 mobile。每个规则使用 required 和 msg;需要额外信息时使用 map。数据从第 2 行开始,是因为第一行被 HEADER=true 作为表头处理。
xml
<excel name="importEmployees" returnClass="java.util.List"><![CDATA[
IMPORT WITH
SHEET='员工导入',
HEADER=true,
MAPPING={
"工号":"code",
"姓名":"name",
"邮箱":"email",
"年龄":"age",
"入职日期":"hireDate",
"状态":"status",
"手机":"mobile",
"备注":"remark"
},
VALIDATION={
A2:A500:{regex{required:true,msg:'工号格式不正确',map:{pattern:'^EMP\\d+$'}}},
B2:B500:{max_length{required:true,msg:'姓名长度不能超过 30',map:{maxLength:30}}},
C2:C500:{email{required:true,msg:'邮箱格式无效'}},
D2:D500:{integer{required:true,msg:'年龄必须为整数'}},
E2:E500:{date_format{required:true,msg:'入职日期必须为 yyyy-MM-dd',map:{'format':'yyyy-MM-dd'}}},
F2:F500:{dict{required:true,msg:'状态不在允许范围内',map:{'启用':'ENABLED','停用':'DISABLED'}}},
G2:G500:{mobile{required:false,msg:'手机号码格式无效'}},
H2:H500:{max_length{required:false,msg:'备注长度不能超过 200',map:{maxLength:200}}}
}
]]></excel>
数值与日期常常同时需要"类型或格式"以及"范围"两层约束。README-CN 中,min_value 使用 minValue,max_value 使用 maxValue;min_date 使用 minDate,max_date 使用 maxDate,并同时提供 format。以下示例把同一范围上的规则组合起来,文案分别指出类型和边界问题。
xml
<excel name="importEmployeeLimits" returnClass="java.util.List"><![CDATA[
IMPORT WITH
SHEET='员工导入',
HEADER=true,
MAPPING={"年龄":"age","转正日期":"regularDate"},
VALIDATION={
A2:A500:{
integer{required:true,msg:'年龄必须为整数'},
min_value{required:true,msg:'年龄不能小于 16',map:{minValue:16}},
max_value{required:true,msg:'年龄不能大于 70',map:{maxValue:70}}
},
B2:B500:{
date_format{required:true,msg:'转正日期必须为 yyyy-MM-dd',map:{'format':'yyyy-MM-dd'}},
min_date{required:true,msg:'转正日期不能早于 2022-01-01',map:{'format':'yyyy-MM-dd',minDate:2022-01-01}},
max_date{required:true,msg:'转正日期不能晚于 2025-01-01',map:{'format':'yyyy-MM-dd',maxDate:2025-01-01}}
}
}
]]></excel>
范围也可以覆盖行、列或矩形区域。以下示例仅展示 README-CN 确认的 ROW、COL、单元格和矩形范围写法。是否应让多个列共用同一规则,取决于模板的业务含义;名称、邮箱、日期等含义不同的列,不宜为了缩短 XML 而强行复用约束。
xml
<excel name="importCodes" returnClass="java.util.List"><![CDATA[
IMPORT WITH
SHEET='编码表',
HEADER=true,
MAPPING={"编码":"code","名称":"name","类型":"type"},
VALIDATION={
ROW 2..200:{min_length{required:true,msg:'数据行内容不能为空',map:{minLength:1}}},
COL A:{start_with{required:true,msg:'编码必须以 A 开头',map:{startWith:'A'}}},
C2:{contain{required:true,msg:'类型必须包含 TYPE',map:{contains:'TYPE'}}},
A2:B200:{max_length{required:true,msg:'编码或名称过长',map:{maxLength:50}}}
}
]]></excel>
README-CN 还提供了程序化解析和导入处理的基础能力。它可用于验证一段 DSL 与最小工作簿,而生产工程仍可继续使用 XML 代理。下例不创造另一套规则,只是把相同的 IMPORT WITH 字符串交给导入执行器。
java
import com.github.paohaijiao.statement.JQuickRow;
import java.util.List;
String rule = "IMPORT WITH HEADER=true, SHEET='Sheet1', "
+ "MAPPING={\"年龄\":\"age\"}, "
+ "VALIDATION={A2:A4:{integer{required:true,msg:'年龄必须为整数'}}}";
// 在项目实际测试中,使用与当前版本匹配的导入执行器和处理器执行该规则。
// 规则文本仍应保持 README-CN 所示的关键字、范围和参数拼写。
List<JQuickRow> rows = importByConfiguredRule(rule);
consume(rows);
上例刻意将项目中未由 README-CN 明确公布的具体测试构造细节留在应用已有封装中。关键是校验 DSL 本身:范围面对工作表坐标,规则面对单元格内容,Java 代码负责提供输入流、上下文和后续业务消费。
原理说明
从 README-CN 描述的能力出发,导入可以理解为一条配置驱动的链路:选择 SHEET,按 HEADER 识别表头,用 MAPPING 将表头映射到字段,按需要执行 TRANSFORM,并在消费结果之前把 VALIDATION 应用到选定范围。理解这条链路有助于定位问题。表头名称不一致时先检查 MAPPING;坐标偏移时先检查 HEADER 与模板布局;值需要翻译时检查 TRANSFORM 和 JContext;基础输入约束则检查 VALIDATION。
范围是规则的定位方式。ROW 5 指一行,ROW 1..10 指连续的行区间;COL A 指一列,COL A..D 指连续列区间;C1 是单个单元格;A1:B5 是矩形区域。它们全部以 Excel 用户看到的位置为准,而不是以 Java 字段名为准。因此 MAPPING={"年龄":"age"} 不会让 age 自动成为某个坐标,年龄位于第几列仍由模板表头和映射顺序决定。
README-CN 列出的规则可以按目的理解。boolean、integer、decimal、date_format、email、mobile 用于值的类型或形态;min_value、max_value、min_date、max_date、min_length、max_length 用于边界;dict 用于字典成员关系;regex、start_with、not_start_with、end_with、not_end_with、contain、not_contain 用于文本约束。选择时优先采用业务含义最明确的规则,例如邮箱使用 email,整数使用 integer,日期格式使用 date_format,不要把每一种输入问题都塞进难以维护的正则。
required 表达是否将空值视为不可接受。必填字段可使用 required:true;允许为空但一旦填写就需要符合形态的字段,可使用 required:false 并保留例如 mobile 或 max_length 的约束。msg 是面向模板使用者的反馈,应描述可操作的修改方向。map 用于规则需要的额外数据,例如日期格式、数值边界、文本长度、正则模式、前后缀或字典内容。
日期规则需要保持格式的一致性。date_format 使用 map:{'format':'yyyy-MM-dd'};min_date、max_date 在此基础上补充 minDate 或 maxDate。模板说明、用户填写方式、错误文案和 XML 中的格式应当一致。日期格式中的大小写也应谨慎核验,不能仅因 DSL 可被读取就假定最终输入符合要求。
字典规则的映射方向要由模板填写内容决定。若 Excel 填写的是"启用""停用",dict 的键应对应这些可填写文本,值则可表达业务希望得到的值。它与 README-CN 中 TRANSFORM={"gender":trans(${dict},${gender})} 的导出展示转换属于不同语境:前者是导入校验的允许成员关系,后者是使用上下文字典转换字段值。设计时应分别确认方向,不应因为二者都出现 dict 就机械复用。
注意事项
第一,工作表名、表头、数据起始行和范围必须一起维护。SHEET='员工导入'、HEADER=true、MAPPING 与 A2:A500 共同构成模板约定。表头移动、列重排或在顶部增加说明行后,所有坐标都应重新检查。
第二,只使用 README-CN 已列出的规则名和参数名。例如日期规则是 date_format,数值边界是 min_value、max_value,长度参数是 minLength、maxLength。DSL 的关键字、花括号、冒号、逗号、引号和范围写法属于解析契约,不应按个人习惯改写。
第三,示例中的 2..500 是模板容量约定,而不是自动识别的最后一行。模板允许更大数据量时,范围也必须相应设计和验证。验收至少应覆盖只有表头、空值、合法值、边界值、越界值和格式错误值。
第四,避免让正则取代已有专用规则。对于邮箱、手机、整数、小数、日期、长度和数值边界,已有规则能更直接表达意图。正则更适合类似内部编码形态的约束,并应搭配清晰的 msg。
第五,VALIDATION 不是系统安全边界。上传文件类型、文件大小、操作者权限、数据唯一性、关联对象和事务必须继续由应用控制。即使单元格校验全部通过,也不代表数据必然满足业务状态。
第六,涉及 TRANSFORM 时要区分当前行字段和上下文值。README-CN 说明 ${field} 可读取当前行字段,${key} 可读取 JContext 中的值。先确认转换结果和校验规则的协作方式,再把它们放进同一导入流程。
总结
VALIDATION 的价值不在于把所有判断塞进 XML,而在于把模板使用者能够直接修正的输入约束写清楚。邮箱、整数、日期格式、长度、数值边界和字典成员关系,应优先采用已经确认的专用规则;内部编码等文本形态再使用 regex。每一条规则都应能说清目标坐标、必填要求、参数来源和失败提示,避免只留下难以解释的通用正则。
规则组合需要按字段语义验证。日期既要求格式又有上下界时,分别核对 date_format、min_date、max_date 的格式参数与边界值;数值则同时检查 integer 或 decimal 和 min_value、max_value。验收样本至少覆盖空值、合法值、两个边界及越界或格式错误值,并确认首个数据行和最大行号与模板容量没有偏移。
这里的边界必须保持克制。通过单元格校验不代表工号唯一、关联对象存在或当前操作者有权限,文件类型、大小和落库事务也不由 DSL 取代。MAPPING 决定字段落点,TRANSFORM 决定已映射值如何计算,业务服务负责依赖外部状态的判断。这样安排后,模板规则既能给用户明确反馈,也不会掩盖系统层仍需承担的校验责任。