JQuick-Excel VALIDATION 实战:用作用范围约束 Excel 导入校验

JQuick-Excel VALIDATION 实战:用作用范围约束 Excel 导入校验

tags: #JQuickExcel #Java #Excel导入 #数据校验

简介

批量导入的难点不只在于"使用什么校验规则",还在于"让规则作用在哪些单元格"。一份工作簿通常同时包含标题、表头、数据区、说明文字、合计行和备注区。若规则覆盖范围没有被明确描述,表头可能被误当成数据,说明文字可能被要求符合数字格式,真正需要校验的区域反而难以定位。JQuick-Excel 在 IMPORT WITH 中提供 VALIDATION, 已明确其可以对选定范围应用规则。

本文只讨论 VALIDATION 的作用范围和现有文档明确展示的配置形式。已列出四类坐标目标:行、列、单元格、矩形范围;并展示了 required、msg、map 以及若干规则名称的写法。本文不会推断未说明的执行顺序、错误对象结构、空白行策略、跨行去重能力或自动修复行为。使用者应把这些未被现有文档定义的事项留给实际代码验证或应用层处理。

范围是 Excel 坐标,不是 Java 字段名,也不是表头名称。ROW 2..100 表示一段行坐标,COL A..D 表示一段列坐标,C1 表示一个单元格,A1:B5 表示矩形区域。它们回答的是"哪些位置要接受本条规则",而 MAPPING 回答的是"表头如何映射到字段",TRANSFORM 回答的是"当前行字段如何转换"。将这些概念分开,是避免导入配置误解的基础。

前言

导入模板的结构常常比程序员想象得复杂。运营人员可能在第一行写报表标题,第二行写填写说明,第三行才是字段表头;模板末尾可能放有合计、签字或自由备注。即使表格当前只有十行数据,也不表示从第一行到最后一行都属于同一种业务区域。一个规则如果没有坐标边界,就很难说明它到底约束了谁。

README 明确指出,校验和公式目标支持行、列、单元格以及矩形范围语法。对于校验,已经给出的写法包括 ROW 2..100:{integer{required:true,msg:'年龄必须是整数'}} 与 C2:C100:{email{required:true,msg:'邮箱格式无效'}}。这些例子证明,VALIDATION 的键是目标范围,值是需要应用的规则配置。配置的关注点首先是坐标,而不是 Java Map 键。

因此,设计校验前应先读模板,再写 DSL。先确定工作表、表头所在行、第一条数据所在行、明细结束行和不应校验的说明区;再用 README 已定义的范围表达这些边界。不要把 HEADER=true 简化理解为"所有后续行都是数据",也不要在没有模板约束时盲目写一个很大的范围。README 没有说明范围会自动跟随实际记录数量或自动排除汇总区域,所以这些边界应由模板与规则共同明确。

VALIDATION 也不等于完整业务校验。README 将它描述为对选定范围应用规则,并列出了如 integer、email、date_format、dict 等已有校验规则。跨行重复、权限、数据库引用存在性、审批状态或多个字段之间的复杂关系,并未被本文所依据的文档定义为 VALIDATION 的能力。将基础单元格约束放在 DSL,将需要业务数据的判断放在应用层,既符合已知边界,也能避免对框架行为作无依据假设。

环境与依赖

本文使用 Java 8 或更高版本,依赖版本为 3.6.0,输入工作簿可以是 xls 或 xlsx。使用范围校验不需要引入额外扩展依赖。

xml 复制代码
<dependency>
  <groupId>io.github.paohaijiao</groupId>
  <artifactId>jquick-excel</artifactId>
  <version>3.6.0</version>
</dependency>

XML 应位于类路径,例如 src/main/resources/jquick-excel.xml。根节点 namespace 与 Java 服务接口全限定名对应,<excel name> 与接口方法名对应。导入节点使用 IMPORT WITH,常见配置包括 SHEET、HEADER、MAPPING、TRANSFORM 和 VALIDATION。其中,VALIDATION 不是独立 Java 调用,而是导入规则中的一部分。

README 展示的导入调用将 JContext 和输入流传给 JQuickExcelImportXmlParseFactory,再由 JQuickXmlFactory 创建服务代理。即使本文的示例没有字典转换,也可以使用空的 JContext。返回值是 List<JQuickRow>;调用方可以在基础范围规则处理后继续进行领域对象组装和业务处理。

代码示例

先看 README 已明确的四种范围。行可以是单行或行区间,列可以是单列或列区间,单元格是一个坐标,矩形范围由左上与右下坐标组成。表达式中的字母和数字应与实际工作簿位置对应。

xml 复制代码
<excel name="importUsers" returnClass="java.util.List"><![CDATA[
  IMPORT WITH
  SHEET="Users",
  HEADER=true,
  MAPPING={"姓名":"name","年龄":"age","邮箱":"email"},
  VALIDATION={
    ROW 2..100:{integer{required:true,msg:'年龄必须是整数'}},
    C2:C100:{email{required:true,msg:'邮箱格式无效'}}
  }
]]></excel>

上面的写法来自 README 中已给出的配置形态。它说明 VALIDATION 由若干"范围:规则集合"组成,范围 ROW 2..100 与 C2:C100 是不同坐标目标。实际模板中,年龄若不位于这些范围所表达的位置,就不应照抄示例坐标;应根据模板重新选择目标。规则中的 required:true 和 msg:'...' 是 README 已展示的配置项,消息用于表达该规则失败时的业务提示文本。

下面给出一个完整 XML 外壳,保留 MAPPING 与 VALIDATION 的职责边界。映射左侧是表头,右侧是结果字段;校验部分使用单元格坐标和矩形坐标,而不使用字段名替代坐标。

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.UserExcelService">
  <excel name="importUsers" returnClass="java.util.List"><![CDATA[
    IMPORT WITH
    SHEET="Users",
    HEADER=true,
    MAPPING={"姓名":"name","年龄":"age","邮箱":"email"},
    VALIDATION={
      C2:C100:{integer{required:true,msg:'年龄必须是整数'}},
      D2:D100:{email{required:true,msg:'邮箱格式无效'}}
    }
  ]]></excel>
</excels>

此处将年龄与邮箱分别限定到不同列的矩形范围,只是为了让范围语义一目了然。C2:C100 与 D2:D100 是按模板坐标写出的示例,不是字段名推导结果。若"年龄"实际在 B 列,就应把范围改成 B 列对应区域;若表头不在第一行,则数据起始行也应同步调整。说明范围语法,但并未声明会根据 MAPPING 自动把字段名转换成坐标。

Java 侧负责输入流、上下文和代理调用。基础调用遵循 README 的导入示例,不把校验规则硬编码到循环中。

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 com.github.paohaijiao.xml.param.Param;

import java.io.InputStream;
import java.util.List;

public interface UserExcelService {
    List<JQuickRow> importUsers(@Param("field") String field,
                                @Param("value") String value);
}

try (InputStream input = App.class.getClassLoader()
        .getResourceAsStream("users.xlsx")) {
    JContext context = new JContext();
    JQuickParseHandler parser = new JQuickExcelImportXmlParseFactory(context, input);
    JQuickFactory factory = new JQuickXmlFactory(parser, "jquick-excel.xml");
    UserExcelService service = factory.createApi(UserExcelService.class);
    List<JQuickRow> rows = service.importUsers("field", "value");
}

同时列出了 COL A、COL A..D、C1 与 A1:B5 等目标表达形式。对固定的单个控制单元格可使用单元格坐标;对一块连续明细区可使用矩形范围;对整段具有相同结构的区域可使用行或列范围。选择何种形式的原则不是语法越短越好,而是让配置准确反映模板边界。

xml 复制代码
<excel name="importSheet" returnClass="java.util.List"><![CDATA[
  IMPORT WITH
  SHEET="Users",
  HEADER=true,
  VALIDATION={
    ROW 2..100:{integer{required:true,msg:'请填写整数'}},
    COL A..D:{max_length{required:true,map:{'maxLength':7}}},
    C1:{email{required:true,msg:'邮箱格式无效'}},
    A2:B5:{regex{required:true,map:{pattern:'^\\d+$'}}}
  }
]]></excel>

该片段的目的只是集中展示已列出的坐标和规则配置形式,不能被当成某个真实模板的推荐规则组合。特别是把不同字段性质的规则套用到整行或整列,必须先确认目标区域中每个单元格的业务含义一致。范围写得过宽并不会让校验更严格,只会使不相关区域被纳入约束。

原理说明

VALIDATION 的第一层语义是"范围"。ROW 5、ROW 1..10、COL A、COL A..D、C1 和 A1:B5 都是在描述工作表坐标。它们不读取当前行字段,也不读取 JContext;因此不能写成 ${age} 或 ${email}。字段表达式属于 TRANSFORM 的上下文访问方式,校验范围属于工作簿位置描述,两者不能互换。

第二层语义是"规则配置"。README 的规则目录展示了 boolean、date_format、max_date、min_date、integer、decimal、max_value、min_value、dict、email、mobile、max_length、min_length、regex、start_with、not_start_with、end_with、not_end_with、contain、not_contain 等名称,并说明按规则需要提供 required、msg 和 map。本文不解释这些规则的内部算法,也不添加未被 README 明确的参数含义。

因此,配置审查可按两步进行。第一步看坐标:该范围是否只覆盖数据区,是否避开标题、表头、合计和备注,是否与工作表名及模板版本一致。第二步看规则:所用规则是否来自已确认的列表,required、msg、map 是否按该规则的现有示例编写。两步分开检查可以避免"规则名称正确但目标列错误"或"范围正确但规则不适合字段"的问题。

HEADER 与范围的关系也必须由模板决定。 说明 HEADER=true 用于声明第一行是否为表头。在常见模板中,第一行是标题时数据可能从第 2 行开始,因而 C2:C100 可以表达数据区;但这不是框架自动保证的通用事实。若第一行是文档名称,第二行才是字段标题,范围应与实际行号相符。现有文档没有为本文提供自动偏移的声明,所以不要凭经验假设。

VALIDATION 与 MAPPING 的关系是相邻而独立的。MAPPING 把"年龄"映射成 age,但不会据此使 integer 自动定位到年龄列;校验仍要以实际坐标配置。反过来,范围命中了某一列,也不等价于该列已被映射为某个字段。导入模板变动时,表头变化要检查 MAPPING,列位置变化要检查 VALIDATION;两项都可能需要调整。

VALIDATION 与 TRANSFORM 也需要分工。已确认 TRANSFORM 可以对当前行字段调用 toUpper、dateFormat、trans 等内置转换;${field} 读取当前行,${key} 可以读取 JContext。范围校验则处理选定坐标上的规则。不能把"转换为大写"误当成"校验格式正确",也不能指望 dateFormat 代替 date_format 规则。转换、显示格式和校验是三个不同问题。

最后,VALIDATION 与应用层业务逻辑之间应保留清晰边界。文档已确认的是单元格、行、列和矩形范围及现有规则配置。是否与数据库中已有记录重复、是否拥有某项权限、多个字段组合是否允许、是否存在跨工作表依赖,都需要应用层根据真实业务数据处理。不要将未被文档承诺的能力写入模板说明,以免读者把猜测误认为框架行为。

注意事项

第一,先以实际模板为准,再写范围。建议在模板上明确表头行、首条数据行、数据末行、合计行和备注区,然后将这些坐标写入 XML。范围越精确,规则含义越稳定。不要为了省事把整列或整张表都纳入校验,也不要把用户填写说明当成需要满足数据规则的区域。

第二,范围不是字段映射。C2:C100 不能因为 MAPPING 中有 "年龄":"age" 就自动表示年龄列,必须确认 C 列在真实文件中确实是年龄。表头改名、列顺序调整、插入说明列时,MAPPING 与 VALIDATION 要分别复核。把两者写在同一个 IMPORT WITH 中,不代表它们共享自动定位机制。

第三,仅使用现有文档明确的规则和配置。例如 给出了 integer{required:true}、email{required:true}、date_format{required:true,map:{'format':'yyyy-MM-dd'}}、max_length{required:true,map:{'maxLength':7}} 与 regex{required:true,map:{pattern:'^\\d+$'}} 等形式。对于 README 没有说明的参数、异常结果或默认行为,不能在项目文档中杜撰结论;应通过项目测试或源码验证后再写入。

第四,消息文本应与目标范围相匹配。范围覆盖的是邮箱列,消息应提示邮箱格式问题;范围覆盖的是年龄列,消息应提示整数要求。错误提示再清晰,也无法弥补坐标写错。配置完成后,应使用一份小工作簿分别验证数据区内的正常值和不符合规则的值,并在相邻的非目标区域放置对照内容,确认范围没有误伤。

第五,输入流必须在解析器创建、代理创建和导入方法调用期间保持打开。try-with-resources 能明确输入流的生命周期。若没有得到预期结果,先检查 XML 是否在类路径、namespace 是否匹配接口、节点名是否匹配方法、SHEET 与 HEADER 是否对应模板,然后再检查范围坐标和规则配置。不要用无关的性能参数替代基础契约排查。

第六,FORMAT 不属于 VALIDATION。明确说明 FORMAT 独立负责 Excel 单元格显示格式,TRANSFORM 负责逐行表达式转换,VALIDATION 对选定范围应用规则。日期看起来相同,不代表日期格式化、日期显示和日期校验是同一个操作。模板中同时存在这些配置时,应逐项说明各自的目标,避免维护人员仅凭名称相近而混用。

延伸检查

范围规则应与模板版本一起回归。最小验证文件可以包含表头、范围内的正常值、范围内的不符合规则值,以及紧邻范围外的对照值。这样可以同时确认规则确实覆盖了应覆盖的单元格,也没有把说明区、汇总区或相邻列误纳入。仅观察导入调用是否完成,无法证明坐标边界正确。

范围的选择应优先表达真实结构。单元格适合固定位置,矩形范围适合连续明细块,行或列适合结构一致的区域。无论选择何种表达,均应将其视为模板坐标契约,不要把字段名或表头名称写进范围位置。规则内容由 README 已确认的名称和配置形式构成;对于未文档化的参数或行为,保持谨慎比编造便利写法更重要。

模板中若同时存在 MAPPING、TRANSFORM、FORMAT 和 VALIDATION,应逐项检查。标题变更主要影响映射,列位置变更可能影响范围,字段值变化可能影响转换,最终显示要求属于 FORMAT。将四者分别验收,能让问题定位回到明确责任上。跨行、跨系统或依赖数据库的约束仍由应用层处理,不应借助范围语法作无依据延伸。

总结

范围校验的风险通常不在规则名,而在坐标是否仍对应真实模板。ROW、COL、单元格和矩形范围都是工作表位置描述,必须与表头行、数据起始行、合计区和说明区一起确认。HEADER=true 不会替维护者推断所有数据边界;模板顶部插入说明、列被重排或尾部加入汇总时,原有范围都需要重新审查。

验证应同时证明"命中了该命中的位置"和"没有碰到不该碰的位置"。小样本可在目标区放入合法值、非法值和边界值,并在相邻的标题、备注或汇总单元格放入对照内容。只有这些对照都符合预期,才说明 C2:C100 一类写法真正表达了模板边界,而不是偶然通过了一次导入。

VALIDATION 适合已有规则能够描述的单元格约束,required、msg、map 也应随具体规则和目标列维护。表头到字段仍属于 MAPPING,逐行值变化属于 TRANSFORM,最终显示属于 FORMAT;跨行重复、权限、数据库关联和事务判断留在应用层。将职责拆开,既避免把坐标误当字段名,也避免承诺未被现有文档定义的自动定位或业务校验能力。

相关推荐
xuxigifxfh3 小时前
HJ11 数字颠倒
java·开发语言·华为机考
一只旭宝4 小时前
数据结构与算法复习手册:从写过,到讲清楚
开发语言·数据结构·c++·算法
百度一下吧4 小时前
断网后 WebView 加载单页面应用:页面跳转 JS 加载失败的处理方案
开发语言·javascript·ecmascript
神仙别闹4 小时前
基于C语言开发的捕捉流星小游戏
c语言·开发语言
程序员Sunday4 小时前
Promise.all、allSettled、race、any 怎么选,失败后其他请求会怎样
开发语言·前端·javascript
小静AI工程实验室5 小时前
Python 爬虫解析 JSON-LD:多块 script、@graph 与坏数据的 9 个边界
爬虫·python·json
daxiangxm5 小时前
GEO策略:让AI直接引用你的内容
开发语言·团队开发·运维开发·个人开发·数据库开发
jason.zeng@15022075 小时前
(八)现有架构上新增一个通用Excel导出工具
python·架构·langchain·excel·llama
袁袁袁袁满5 小时前
AI Agent 如何“看懂“互联网?
爬虫·python·自动化·爬虫实战·多线程爬虫