JQuick-Excel 实战:用 VALIDATION 建立可维护的 Excel 导入校验

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 决定已映射值如何计算,业务服务负责依赖外部状态的判断。这样安排后,模板规则既能给用户明确反馈,也不会掩盖系统层仍需承担的校验责任。

相关推荐
longlongzihan1 小时前
前缀积与后缀积 —— LeetCode 238. 除了自身以外数组的乘积
开发语言·c++·leetcode
Duang007_1 小时前
生产可观测性:从“系统慢“到“根因“的完整链路(Go / TypeScript)
后端·python·golang·typescript·prometheus
szial1 小时前
JavaScript 的 call、apply 和 bind:如何控制函数调用
开发语言·前端·javascript
weixin_307779131 小时前
C++代码实现MATLAB中的cvpartition函数功能
开发语言·c++·算法·matlab
jason.zeng@15022071 小时前
(九)多轮对话式新增维修记录实现方案
python·prompt·交互·llama
黑妹天下第一乖1 小时前
小智改造实战解读-首 token 延迟去哪了:云端与端侧大模型的分段对照
开发语言·人工智能·python·嵌入式硬件·自然语言处理·iot
。小二2 小时前
Go 泛型并发利刃:async 库深度评测——370K ops/s、零依赖、生产就绪
开发语言·javascript·golang
(Charon)2 小时前
【C++面试】线程同步机制:互斥锁、条件变量、原子操作与读写锁
开发语言·c++·算法·面试
坊钰2 小时前
【LangChain框架入门级】10. 文本向量与向量数据库(Embedding / Redis / Pinecone / MMR)
数据库·python·langchain·embedding