Apache Commons Validator 1.11.0 常用 API 与校验器详解

系统梳理 Apache Commons Validator 1.11.0 的 routines 校验器、CheckDigit 检查位算法与旧式 XML 校验框架 API,每个方法均附可复现代码与 JDK 17 实测回显

Apache Commons ValidatorApache Commons Validator2026/10/15

Apache Commons Validator 1.11.0 常用 API 与校验器详解

目标:系统掌握 Apache Commons Validator 1.11.0 的常用校验器 API,覆盖 EmailValidator、UrlValidator、InetAddressValidator、DomainValidator、GenericValidator 与数值/日期工具、CreditCardValidator、CheckDigit 检查位体系以及旧式 XML 校验框架,每个 API 均给出可复现代码与实测回显,已在 Ubuntu 22.04(x86_64)+ Temurin JDK 17.0.20.1 + Maven 3.9.16 环境实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS
  • 架构:x86_64
  • JDK:Temurin OpenJDK 17.0.20.1
  • 构建工具:Apache Maven 3.9.16
  • 组件版本:Apache Commons Validator 1.11.0(1.x 系列当前最新稳定版,2026-07-31 发布,要求 Java 8 及以上)
  • Maven 坐标:commons-validator:commons-validator:1.11.0
  • 工作目录:/work/demo(示例工程根目录,与快速入门篇同一工程,本文新增的探针类均放在 src/main/java 下随 mvn package 一起编译)

二、API 全景:一个库、两个包

1.11.0 的 jar 内共约 90 个公开类,按包分成两层。日常使用几乎全部集中在 routines 包;org.apache.commons.validator 根包是 Struts 1 时代表单校验框架的内核,面向 XML 规则文件驱动。

2.1 routines 包:现代化单值校验器

类用途典型入口
EmailValidator邮箱地址EmailValidator.getInstance()
UrlValidatorURLnew UrlValidator(long options)
InetAddressValidatorIPv4/IPv6 地址InetAddressValidator.getInstance()
DomainValidator域名与顶级域(TLD)DomainValidator.getInstance()
GenericValidator通用静态方法集合静态调用,无需实例
DateValidator / CalendarValidator / TimeValidator日期、日历、时间getInstance()
LongValidator / IntegerValidator / ShortValidator / ByteValidator / FloatValidator / DoubleValidator数值getInstance()
CurrencyValidator / PercentValidator货币、百分比getInstance(),返回 BigDecimalValidator
CreditCardValidator信用卡号new CreditCardValidator(long options)
routines.checkdigit 包检查位算法(Luhn、ISBN、IBAN、ISIN、模 10 通用实现等)单例常量或构造器

2.2 根包:旧式 XML 校验框架

类职责
ValidatorResources解析 validation.xml,持有全部规则
Validator一次校验会话:绑定表单名与待校验 Bean
ValidatorResults / ValidatorResult校验结果集 / 单字段结果
ValidatorAction对应 XML 中一个 <validator> 动作定义
Field对应 XML 中一个 <field>,提供 getVarValue 取参
GenericTypeValidatorformatInt/formatLong 等类型转换
util.ValidatorUtilsgetValueAsString 等 Bean 属性读取工具

注意 1.11.0 的 jar 里没有 validator-rules.xml——那是 Struts 1 的资源文件;自写 validation.xml 时需要自己提供每个动作的回调方法(见第十节)。本文 XML 框架示例的完整规则文件与驱动代码在快速入门篇已给出,本篇侧重 API 本身。

💡 技术拓展:单例还是新实例?两类校验器的实例化约定不同,选错一般不报错,但会踩到隐藏的共享状态:

获取方式代表类共享状态并发安全性
getInstance() 单例EmailValidator、InetAddressValidator、DomainValidator、各类数值/日期校验器内部缓存实例,TLD 表进程级固化一次校验方法无状态,多线程可复用
new 构造UrlValidator、CreditCardValidator开关位在构造时固化进实例每次新建或在并发中共享均可

DomainValidator 的 TLD 表是 JVM 级全局的,updateTLDOverride 必须在进程第一次 getInstance() 之前调用(第六节实测)。

三、EmailValidator:邮箱地址校验

3.1 API 形态

  • static EmailValidator getInstance():默认实例,不允许本地域名(如 localhost),不允许裸 TLD 域名(如 user@com);
  • static EmailValidator getInstance(boolean allowLocal):允许本地域名;
  • static EmailValidator getInstance(boolean allowLocal, boolean allowTld):再放开裸 TLD 域名;
  • boolean isValid(String email):唯一核心校验方法,null 安全(null 返回 false)。

校验由本地部分正则 + 域名校验(委托 DomainValidator)+ 域字面量(IPv4)三段组成。引号包裹的本地部分(RFC 5322 quoted-string)与 user+tag 子地址都是合法的。

3.2 默认实例 19 例实测

探针代码(EmailProbe.java,完整可复现):

import org.apache.commons.validator.routines.EmailValidator;

public class EmailProbe {
    public static void main(String[] args) {
        EmailValidator ev = EmailValidator.getInstance();
        String[] emails = {
                "user@example.com",
                "first.last@sub.example.com",
                "user+tag@example.com",
                "user_123@example.co.uk",
                "user@[192.168.1.2]",
                "user@[::1]",
                "user@192.168.1.2",
                "\"john doe\"@example.com",
                "user@localhost",
                "user.name@",
                "@example.com",
                "user@example..com",
                "user@@example.com",
                "user name@example.com",
                "user@.example.com",
                "user@example.com.",
                "user@example.c",
                "user@-example.com",
                "user@com"
        };
        System.out.println("---- getInstance()(默认:不允许本地域名/TLD) ----");
        for (String e : emails) {
            System.out.println(String.format("%-32s -> %s", "\"" + e + "\"", ev.isValid(e)));
        }
        System.out.println("---- getInstance(true)(allowLocal) ----");
        EmailValidator evLocal = EmailValidator.getInstance(true);
        System.out.println("user@localhost    -> " + evLocal.isValid("user@localhost"));
        System.out.println("user@myhost.local -> " + evLocal.isValid("user@myhost.local"));
        System.out.println("user@com          -> " + evLocal.isValid("user@com"));
        System.out.println("---- getInstance(true, true)(allowLocal + allowTld) ----");
        EmailValidator evTT = EmailValidator.getInstance(true, true);
        System.out.println("user@com -> " + evTT.isValid("user@com"));
        System.out.println("user@cn  -> " + evTT.isValid("user@cn"));
        System.out.println("user@localhost -> " + evTT.isValid("user@localhost"));
    }
}

实测回显(完整 25 行):

---- getInstance()(默认:不允许本地域名/TLD) ----
"user@example.com"               -> true
"first.last@sub.example.com"     -> true
"user+tag@example.com"           -> true
"user_123@example.co.uk"         -> true
"user@[192.168.1.2]"             -> true
"user@[::1]"                     -> false
"user@192.168.1.2"               -> false
""john doe"@example.com"         -> true
"user@localhost"                 -> false
"user.name@"                     -> false
"@example.com"                   -> false
"user@example..com"              -> false
"user@@example.com"              -> false
"user name@example.com"          -> false
"user@.example.com"              -> false
"user@example.com."              -> false
"user@example.c"                 -> false
"user@-example.com"              -> false
"user@com"                       -> false
---- getInstance(true)(allowLocal) ----
user@localhost    -> true
user@myhost.local -> false
user@com          -> true
---- getInstance(true, true)(allowLocal + allowTld) ----
user@com -> true
user@cn  -> true
user@localhost -> true

3.3 要点提示

  • user@[192.168.1.2] 默认即通过(IPv4 域字面量内置支持),而 user@[::1] 实测被拒:1.11.0 的 IPv6 字面量分支不可达,需要时应在业务层自行解析;
  • 引号本地部分 "john doe"@example.com 通过——含空格的邮箱在 RFC 里合法,录入系统别按「邮箱不含空格」硬编码;
  • user@example..com(连续点)、user@-example.com(连字符开头)、user@example.c(单字符 TLD 不在表内)都被正确拒绝;
  • allowLocal 放开的是 localhost、localdomain 这类内置本地 TLD;myhost.local 实测仍被拒,因为 .local 不在 1.11.0 的 LOCAL 表里(LOCAL 表只有 localdomain,localhost 两个条目,见 6.2);
  • user@com 需要 getInstance(true, true) 才放行,第二个参数是 allowTld 而不是 allowDomainLiteral。

四、UrlValidator:URL 校验

4.1 构造器与开关位

  • new UrlValidator():默认实例;
  • new UrlValidator(long options):按位或组合开关;
  • new UrlValidator(String[] customSchemes, long options):追加自定义 scheme 白名单;
  • new UrlValidator(String[] schemes, String authorityRegex, long options):再自定义主机名校验。

开关位(public static long 常量)与实测效果:

开关含义实测结论
ALLOW_2_SLASHES路径允许连续双斜杠默认拒 //double,开启后通过
NO_FRAGMENTS禁止 #fragment默认允许 #top,开启后拒绝
ALLOW_ALL_SCHEMES放开 scheme 白名单gopher/ldap 转为通过;含 + 的 scheme 仍被拒
ALLOW_LOCAL_URLS允许本地/内网主机myserver:8080 通过;local.server 仍被拒(server 不是合法 TLD)

4.2 开关组合实测矩阵

探针代码(UrlProbe.java,完整可复现):

import org.apache.commons.validator.routines.UrlValidator;

public class UrlProbe {

    // isValidScheme 等分解方法在 1.11.0 中为 protected,通过子类暴露用于演示
    static class ExposedUrlValidator extends UrlValidator {
        boolean checkScheme(String s)    { return isValidScheme(s); }
        boolean checkAuthority(String s) { return isValidAuthority(s); }
        boolean checkPath(String s)      { return isValidPath(s); }
        boolean checkQuery(String s)     { return isValidQuery(s); }
        boolean checkFragment(String s)  { return isValidFragment(s); }
    }

    static void t(String tag, UrlValidator v, String url) {
        System.out.println(String.format("%-14s %-52s -> %s", tag, url, v.isValid(url)));
    }

    public static void main(String[] args) {
        UrlValidator def = new UrlValidator();
        UrlValidator all = new UrlValidator(UrlValidator.ALLOW_ALL_SCHEMES);
        UrlValidator local = new UrlValidator(UrlValidator.ALLOW_LOCAL_URLS);
        UrlValidator two = new UrlValidator(UrlValidator.ALLOW_2_SLASHES);
        UrlValidator noFrag = new UrlValidator(UrlValidator.NO_FRAGMENTS);
        UrlValidator combo = new UrlValidator(UrlValidator.ALLOW_ALL_SCHEMES
                | UrlValidator.ALLOW_LOCAL_URLS | UrlValidator.ALLOW_2_SLASHES);

        t("default", def, "http://www.apache.org");
        t("default", def, "https://commons.apache.org/proper/commons-validator/");
        t("default", def, "ftp://ftp.example.com/pub/file.txt");
        t("default", def, "http://www.apache.org:8080/index.html?q=validator#top");
        t("default", def, "https://example.com/a/b?x=1&y=2");
        t("default", def, "gopher://example.com");
        t("default", def, "ldap://example.com/dc=example,dc=com");
        t("default", def, "www.apache.org");
        t("default", def, "http://local.server/path");
        t("default", def, "http://www.apache.org//double");
        t("default", def, "http://bad url.com");

        t("ALL_SCHEMES", all, "gopher://example.com");
        t("ALL_SCHEMES", all, "ldap://example.com/dc=example,dc=com");
        t("ALL_SCHEMES", all, "custom+app://host/path");
        t("LOCAL_URLS", local, "http://local.server/path");
        t("LOCAL_URLS", local, "http://myserver:8080/app");
        t("LOCAL_URLS", local, "http://www.apache.org");
        t("2_SLASHES", two, "http://www.apache.org//double");
        t("2_SLASHES", two, "http://www.apache.org/single");
        t("NO_FRAGMENTS", noFrag, "http://www.apache.org/index.html#top");
        t("NO_FRAGMENTS", noFrag, "http://www.apache.org/index.html");
        t("COMBO", combo, "myapp://internal.host:8080//a/b#frag");

        System.out.println("---- 分解校验(子类暴露 protected 方法,default 实例) ----");
        ExposedUrlValidator ex = new ExposedUrlValidator();
        System.out.println("isValidScheme(http)        = " + ex.checkScheme("http"));
        System.out.println("isValidScheme(gopher)      = " + ex.checkScheme("gopher"));
        System.out.println("isValidAuthority(www.apache.org:8080) = " + ex.checkAuthority("www.apache.org:8080"));
        System.out.println("isValidAuthority(bad_host) = " + ex.checkAuthority("bad_host"));
        System.out.println("isValidPath(/proper/commons-validator/) = " + ex.checkPath("/proper/commons-validator/"));
        System.out.println("isValidPath(//double)      = " + ex.checkPath("//double"));
        System.out.println("isValidQuery(?q=validator) = " + ex.checkQuery("?q=validator"));
        System.out.println("isValidFragment(#top)      = " + ex.checkFragment("#top"));
    }
}

实测回显(完整 30 行):

default        http://www.apache.org                                -> true
default        https://commons.apache.org/proper/commons-validator/ -> true
default        ftp://ftp.example.com/pub/file.txt                   -> true
default        http://www.apache.org:8080/index.html?q=validator#top -> true
default        https://example.com/a/b?x=1&y=2                      -> true
default        gopher://example.com                                 -> false
default        ldap://example.com/dc=example,dc=com                 -> false
default        www.apache.org                                       -> false
default        http://local.server/path                             -> false
default        http://www.apache.org//double                        -> false
default        http://bad url.com                                   -> false
ALL_SCHEMES    gopher://example.com                                 -> true
ALL_SCHEMES    ldap://example.com/dc=example,dc=com                 -> true
ALL_SCHEMES    custom+app://host/path                               -> false
LOCAL_URLS     http://local.server/path                             -> false
LOCAL_URLS     http://myserver:8080/app                             -> true
LOCAL_URLS     http://www.apache.org                                -> true
2_SLASHES      http://www.apache.org//double                        -> true
2_SLASHES      http://www.apache.org/single                         -> true
NO_FRAGMENTS   http://www.apache.org/index.html#top                 -> false
NO_FRAGMENTS   http://www.apache.org/index.html                     -> true
COMBO          myapp://internal.host:8080//a/b#frag                 -> true
---- 分解校验(子类暴露 protected 方法,default 实例) ----
isValidScheme(http)        = true
isValidScheme(gopher)      = false
isValidAuthority(www.apache.org:8080) = true
isValidAuthority(bad_host) = false
isValidPath(/proper/commons-validator/) = true
isValidPath(//double)      = false
isValidQuery(?q=validator) = true
isValidFragment(#top)      = true

4.3 要点提示

  • 默认实例不认识 www.apache.org 这种不带 scheme 的写法,也没有「自动补 http://」的能力,需要业务层先拼前缀;
  • ALLOW_ALL_SCHEMES 开了仍拒绝 custom+app://:scheme 合法性正则不包含 + 号,自定义 scheme 请改用 new UrlValidator(new String[]{"custom+app"}, options) 白名单方式;
  • ALLOW_LOCAL_URLS 放行的是无 TLD 的主机名(myserver),主机名末段若恰是无效 TLD(如 local.server)依旧被拒——这两个开关解决的是不同问题;
  • isValidScheme/isValidAuthority/isValidPath 等分解方法都是 protected,对外只暴露整体 isValid;确需分步校验时按上面示例写个子类暴露即可;
  • URL 校验纯字符串操作,不发起网络请求,可以放心放进高并发入口。

五、InetAddressValidator:IP 地址校验

5.1 API 形态

  • static InetAddressValidator getInstance():单例;
  • boolean isValid(String address):IPv4 或 IPv6 任一通过即可;
  • boolean isValidInet4Address(String address):仅 IPv4,四段点分十进制、每段 0 到 255;
  • boolean isValidInet6Address(String address):仅 IPv6,支持 :: 压缩写法与 ::ffff:x.x.x.x IPv4 映射段。

5.2 实测

探针代码(InetProbe.java,完整可复现):

import org.apache.commons.validator.routines.InetAddressValidator;

public class InetProbe {
    static InetAddressValidator v = InetAddressValidator.getInstance();

    static void line(String kind, String addr, boolean r) {
        System.out.println(String.format("%-8s %-36s -> %s", kind, addr, r));
    }

    public static void main(String[] args) {
        line("IPv4", "192.168.1.1", v.isValidInet4Address("192.168.1.1"));
        line("IPv4", "255.255.255.0", v.isValidInet4Address("255.255.255.0"));
        line("IPv4", "0.0.0.0", v.isValidInet4Address("0.0.0.0"));
        line("IPv4", "127.0.0.1", v.isValidInet4Address("127.0.0.1"));
        line("IPv4", "256.1.1.1", v.isValidInet4Address("256.1.1.1"));
        line("IPv4", "1.2.3", v.isValidInet4Address("1.2.3"));
        line("IPv4", "1.2.3.4.5", v.isValidInet4Address("1.2.3.4.5"));
        line("IPv4", "1.2.3.4a", v.isValidInet4Address("1.2.3.4a"));
        line("IPv4", "01.02.03.04", v.isValidInet4Address("01.02.03.04"));
        line("IPv4", "1.2.3.256", v.isValidInet4Address("1.2.3.256"));

        line("IPv6", "::1", v.isValidInet6Address("::1"));
        line("IPv6", "fe80::1", v.isValidInet6Address("fe80::1"));
        line("IPv6", "2001:db8::8a2e:370:7334", v.isValidInet6Address("2001:db8::8a2e:370:7334"));
        line("IPv6", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", v.isValidInet6Address("2001:0db8:85a3:0000:0000:8a2e:0370:7334"));
        line("IPv6", "::ffff:192.168.1.1", v.isValidInet6Address("::ffff:192.168.1.1"));
        line("IPv6", "2001:db8::8a2e:370::7334", v.isValidInet6Address("2001:db8::8a2e:370::7334"));
        line("IPv6", "1:2:3:4:5:6:7:8:9", v.isValidInet6Address("1:2:3:4:5:6:7:8:9"));
        line("IPv6", "12345::", v.isValidInet6Address("12345::"));
        line("IPv6", "::ffff:999.1.1.1", v.isValidInet6Address("::ffff:999.1.1.1"));

        line("合并", "::1", v.isValid("::1"));
        line("合并", "192.168.1.1", v.isValid("192.168.1.1"));
        line("合并", "example.com", v.isValid("example.com"));
    }
}

实测回显(完整 22 行):

IPv4     192.168.1.1                          -> true
IPv4     255.255.255.0                        -> true
IPv4     0.0.0.0                              -> true
IPv4     127.0.0.1                            -> true
IPv4     256.1.1.1                            -> false
IPv4     1.2.3                                -> false
IPv4     1.2.3.4.5                            -> false
IPv4     1.2.3.4a                             -> false
IPv4     01.02.03.04                          -> false
IPv4     1.2.3.256                            -> false
IPv6     ::1                                  -> true
IPv6     fe80::1                              -> true
IPv6     2001:db8::8a2e:370:7334              -> true
IPv6     2001:0db8:85a3:0000:0000:8a2e:0370:7334 -> true
IPv6     ::ffff:192.168.1.1                   -> true
IPv6     2001:db8::8a2e:370::7334             -> false
IPv6     1:2:3:4:5:6:7:8:9                    -> false
IPv6     12345::                              -> false
IPv6     ::ffff:999.1.1.1                     -> false
合并       ::1                                  -> true
合并       192.168.1.1                          -> true
合并       example.com                          -> false

要点提示:01.02.03.04 被拒(前导零是历史上可被部分系统按八进制解释的歧义写法,一律不接受);::ffff:192.168.1.1 通过且内嵌 IPv4 段也参与段值校验(::ffff:999.1.1.1 拒绝);IPv6 只允许出现一个 :: 压缩段。该校验器同样只做格式判断,不做可达性探测。

六、DomainValidator:域名与 TLD 校验

6.1 API 形态与 TLD 分类实测

  • static DomainValidator getInstance() / getInstance(boolean allowLocal);
  • boolean isValidTld(String tld):总入口;
  • boolean isValidGenericTld(String tld) / isValidCountryCodeTld(String tld) / isValidLocalTld(String tld) / isValidInfrastructureTld(String tld):四类细分;
  • static String[] getTLDEntries(ArrayType type):读内置 TLD 表;
  • static void updateTLDOverride(ArrayType type, String[] tlds):进程初始化前增删 TLD 条目。

探针代码(DomainProbe.java,完整可复现):

import org.apache.commons.validator.routines.DomainValidator;

public class DomainProbe {
    public static void main(String[] args) {
        DomainValidator dv = DomainValidator.getInstance();
        String[] tlds = {
                "com", "org", "net", "info", "biz", "xxx",
                "cn", "uk", "de", "jp", "io",
                "localhost", "example", "invalid", "test", "local",
                "mycompany.local", "internal", "corp",
                "COM", "CN"
        };
        for (String t : tlds) {
            System.out.println(String.format("isValidTld(%-16s) = %-5s generic=%-5s cc=%-5s local=%s",
                    t, dv.isValidTld(t), dv.isValidGenericTld(t), dv.isValidCountryCodeTld(t), dv.isValidLocalTld(t)));
        }
        System.out.println("---- getInstance(true) 后 ----");
        DomainValidator dvLocal = DomainValidator.getInstance(true);
        System.out.println("isValidTld(localhost)      = " + dvLocal.isValidTld("localhost"));
        System.out.println("isValidTld(example)        = " + dvLocal.isValidTld("example"));
        System.out.println("isValidTld(mycompany.local)= " + dvLocal.isValidTld("mycompany.local"));
        System.out.println("isValidLocalTld(localhost) = " + dvLocal.isValidLocalTld("localhost"));
        System.out.println("isValidLocalTld(example)   = " + dvLocal.isValidLocalTld("example"));
    }
}

实测回显(完整 25 行):

isValidTld(com             ) = true  generic=true  cc=false local=false
isValidTld(org             ) = true  generic=true  cc=false local=false
isValidTld(net             ) = true  generic=true  cc=false local=false
isValidTld(info            ) = true  generic=true  cc=false local=false
isValidTld(biz             ) = true  generic=true  cc=false local=false
isValidTld(xxx             ) = true  generic=true  cc=false local=false
isValidTld(cn              ) = true  generic=false cc=true  local=false
isValidTld(uk              ) = true  generic=false cc=true  local=false
isValidTld(de              ) = true  generic=false cc=true  local=false
isValidTld(jp              ) = true  generic=false cc=true  local=false
isValidTld(io              ) = true  generic=false cc=true  local=false
isValidTld(localhost       ) = false generic=false cc=false local=true
isValidTld(example         ) = false generic=false cc=false local=false
isValidTld(invalid         ) = false generic=false cc=false local=false
isValidTld(test            ) = false generic=false cc=false local=false
isValidTld(local           ) = false generic=false cc=false local=false
isValidTld(mycompany.local ) = false generic=false cc=false local=false
isValidTld(internal        ) = false generic=false cc=false local=false
isValidTld(corp            ) = false generic=false cc=false local=false
isValidTld(COM             ) = true  generic=true  cc=false local=false
isValidTld(CN              ) = true  generic=false cc=true  local=false
---- getInstance(true) 后 ----
isValidTld(localhost)      = true
isValidTld(example)        = false
isValidTld(mycompany.local)= false
isValidLocalTld(localhost) = true
isValidLocalTld(example)   = false

6.2 内置 TLD 清单规模

探针代码(ExtraProbe.java,完整可复现):

import org.apache.commons.validator.routines.DomainValidator;
import org.apache.commons.validator.routines.DomainValidator.ArrayType;

public class ExtraProbe {
    public static void main(String[] args) {
        DomainValidator dv = DomainValidator.getInstance();
        System.out.println("isValidTld(server) = " + dv.isValidTld("server"));
        System.out.println("isValidTld(app)    = " + dv.isValidTld("app"));
        System.out.println("isValidTld(arpa)   = " + dv.isValidTld("arpa"));
        System.out.println("isValidInfrastructureTld(arpa) = " + dv.isValidInfrastructureTld("arpa"));
        System.out.println("GENERIC_RO 条目数  = " + DomainValidator.getTLDEntries(ArrayType.GENERIC_RO).length);
        System.out.println("COUNTRY_CODE_RO 条目数 = " + DomainValidator.getTLDEntries(ArrayType.COUNTRY_CODE_RO).length);
        System.out.println("INFRASTRUCTURE_RO  = " + String.join(",", DomainValidator.getTLDEntries(ArrayType.INFRASTRUCTURE_RO)));
        System.out.println("LOCAL_RO           = " + String.join(",", DomainValidator.getTLDEntries(ArrayType.LOCAL_RO)));
        System.out.println("GENERIC 最后 5 个   = " + String.join(",",
                java.util.Arrays.copyOfRange(DomainValidator.getTLDEntries(ArrayType.GENERIC_RO),
                        DomainValidator.getTLDEntries(ArrayType.GENERIC_RO).length - 5,
                        DomainValidator.getTLDEntries(ArrayType.GENERIC_RO).length)));
    }
}

实测回显(完整 8 行):

isValidTld(server) = false
isValidTld(app)    = true
isValidTld(arpa)   = true
isValidInfrastructureTld(arpa) = true
GENERIC_RO 条目数  = 1127
COUNTRY_CODE_RO 条目数 = 309
INFRASTRUCTURE_RO  = arpa
LOCAL_RO           = localdomain,localhost
GENERIC 最后 5 个   = zara,zero,zip,zone,zuerich

要点提示:内置表来自 IANA 注册数据快照,截至 1.11.0 共 1127 个通用 TLD、309 个国家代码 TLD;server、local、internal 这类公司内部约定后缀都不在表内,app 则已是真实 TLD。TLD 表是只读数组(RO 后缀即 read-only),运行期想改动只能走 updateTLDOverride。

6.3 自定义 TLD 覆盖

新增内部品牌 TLD(DomainOverrideProbe.java,完整可复现):

import org.apache.commons.validator.routines.DomainValidator;
import org.apache.commons.validator.routines.DomainValidator.ArrayType;

public class DomainOverrideProbe {
    public static void main(String[] args) {
        DomainValidator.updateTLDOverride(ArrayType.GENERIC_PLUS, new String[]{"mybrand"});
        DomainValidator.updateTLDOverride(ArrayType.LOCAL_PLUS, new String[]{"mycorp.internal"});
        DomainValidator dv = DomainValidator.getInstance();
        System.out.println("isValidTld(mybrand)        = " + dv.isValidTld("mybrand"));
        System.out.println("isValidGenericTld(mybrand) = " + dv.isValidGenericTld("mybrand"));
        System.out.println("isValidTld(mycorp.internal)= " + dv.isValidTld("mycorp.internal"));
        System.out.println("isValidLocalTld(mycorp.internal) = " + dv.isValidLocalTld("mycorp.internal"));
        System.out.println("isValidTld(com)            = " + dv.isValidTld("com"));
    }
}

实测回显(完整 5 行):

isValidTld(mybrand)        = true
isValidGenericTld(mybrand) = true
isValidTld(mycorp.internal)= false
isValidLocalTld(mycorp.internal) = true
isValidTld(com)            = true

反向移除已有 TLD(XxxMinusProbe.java,完整可复现):

import org.apache.commons.validator.routines.DomainValidator;
import org.apache.commons.validator.routines.DomainValidator.ArrayType;

public class XxxMinusProbe {
    public static void main(String[] args) {
        DomainValidator.updateTLDOverride(ArrayType.GENERIC_MINUS, new String[]{"xxx"});
        DomainValidator dv = DomainValidator.getInstance();
        System.out.println("移除后 isValidTld(xxx) = " + dv.isValidTld("xxx"));
        System.out.println("对照 isValidTld(com)  = " + dv.isValidTld("com"));
    }
}

实测回显(完整 2 行):

移除后 isValidTld(xxx) = false
对照 isValidTld(com)  = true

要点提示:四种 ArrayType 中 GENERIC_PLUS/GENERIC_MINUS 操作通用 TLD 表,LOCAL_PLUS/LOCAL_MINUS 操作本地表;updateTLDOverride 必须在进程第一次 getInstance() 前调用,且全局只生效一次,放错位置会直接抛 IllegalStateException。这组开关适合「自建内部域名体系」的场景,不适合跟随 IANA 动态更新的场景。

七、GenericValidator 与数值/日期校验器

7.1 GenericValidator:静态方法全家桶

GenericValidator 无需实例,全部静态方法,适合表单层快速判断。实测(RoutinesProbe.java 的 GenericValidator 段):

isBlankOrNull("")        = true
isBlankOrNull("   ")     = true
isBlankOrNull(" x ")     = false
isEmail(user@example.com)= true
isEmail(bad@@x.com)      = false
isUrl(http://example.com/x) = true
isUrl(https://commons.apache.org/proper/commons-validator/) = true
isUrl(ftp://ftp.example.com/file) = true
isUrl(gopher://example.com/x)     = false
isInt(-1)                = true
isInt(12.3)              = false
isLong(9999999999)       = true
isFloat(1.5)             = true
isDouble(1e3)            = true
isInRange(5, 1, 10)      = true
isCreditCard(4111111111111111) = true
isCreditCard(4111111111111112) = false
matchRegexp(abc123, ^abc\d+$) = true
isDate(2026-10-15, yyyy-MM-dd, strict) = true
isDate(2026-13-01, yyyy-MM-dd, strict) = false

要点提示:isEmail/isUrl/isCreditCard 只是 routines 包对应校验器的静态门面;isDate 带 strict 参数可显式控制严格模式;matchRegexp 是整串匹配(等价 Pattern.matches),不是子串查找。

7.2 DateValidator 与 CalendarValidator

实测代码(RoutinesProbe.java 的日期段):

DateValidator dv = DateValidator.getInstance();
Date d = dv.validate("15/10/2026", "dd/MM/yyyy", Locale.US);
System.out.println("validate(15/10/2026, dd/MM/yyyy, US) = " + dv.format(d, "yyyy-MM-dd"));
System.out.println("validate(2026-10-15, yyyy-MM-dd)     = " + dv.format(dv.validate("2026-10-15", "yyyy-MM-dd"), "yyyy/MM/dd"));
System.out.println("validate(15-10-2026, yyyy-MM-dd)     = " + dv.validate("15-10-2026", "yyyy-MM-dd"));
System.out.println("validate(2026-02-30, yyyy-MM-dd)     = " + dv.validate("2026-02-30", "yyyy-MM-dd"));
System.out.println("validate(10/15/2026, dd/MM/yyyy, US) = " + dv.validate("10/15/2026", "dd/MM/yyyy", Locale.US));
System.out.println("validate(15.10.2026, dd.MM.yyyy, DE) = " + dv.format(dv.validate("15.10.2026", "dd.MM.yyyy", Locale.GERMANY), "yyyy-MM-dd"));
DateValidator lenient = new DateValidator(false, DateFormat.SHORT);
Date d30 = lenient.validate("2026-02-30", "yyyy-MM-dd");
System.out.println("lenient 2026-02-30 (回写格式化)      = " + lenient.format(d30, "yyyy-MM-dd"));

CalendarValidator cv = CalendarValidator.getInstance();
Calendar cal = cv.validate("2026-10-15 13:45", "yyyy-MM-dd HH:mm");
System.out.println("parse 类型 = " + cal.getClass().getName());
System.out.println("format 回写 = " + cv.format(cal, "yyyy年MM月dd日 HH:mm"));
System.out.println("format 反转 = " + cv.format(cal, "HH:mm yyyy-MM-dd"));

实测回显(完整 10 行):

---- DateValidator(getInstance 默认 strict=true) ----
validate(15/10/2026, dd/MM/yyyy, US) = 2026-10-15
validate(2026-10-15, yyyy-MM-dd)     = 2026/10/15
validate(15-10-2026, yyyy-MM-dd)     = null
validate(2026-02-30, yyyy-MM-dd)     = null
validate(10/15/2026, dd/MM/yyyy, US) = null
validate(15.10.2026, dd.MM.yyyy, DE) = 2026-10-15
lenient 2026-02-30 (回写格式化)      = null
---- CalendarValidator ----
parse 类型 = java.util.GregorianCalendar
format 回写 = 2026年10月15日 13:45
format 反转 = 13:45 2026-10-15

要点提示:validate 失败返回 null 而不抛异常,配合 null 判断即是「解析即校验」模式;日期模式不匹配(15-10-2026 对 yyyy-MM-dd)与溢出日期(2026-02-30)都拒绝;即便显式构造宽松实例(new DateValidator(false, DateFormat.SHORT)),2026-02-30 仍返回 null——底层 formatter 被硬编码为非宽松模式,这是 1.11.0 的既定行为。CalendarValidator.parse 返回的具体类型是 GregorianCalendar,中文模式串里的字面量「年/月/日」原样透传。

7.3 CurrencyValidator 与 PercentValidator

实测代码(RoutinesProbe.java 的货币/百分比段):

org.apache.commons.validator.routines.BigDecimalValidator currency = CurrencyValidator.getInstance();
BigDecimal c1 = currency.validate("$1,234.56", Locale.US);
System.out.println("validate($1,234.56, US)  = " + c1 + " (" + c1.getClass().getName() + ")");
System.out.println("format(1234.56, US)      = " + currency.format(new BigDecimal("1234.56"), Locale.US));
System.out.println("format(1234.56, DE)      = " + currency.format(new BigDecimal("1234.56"), Locale.GERMANY));
BigDecimal c2 = currency.validate("1.234,56 €", Locale.GERMANY);
System.out.println("validate(ASCII 空格写法, DE) = " + c2);
String deOut = currency.format(new BigDecimal("1234.56"), Locale.GERMANY);
System.out.println("往返 validate(format 产物, DE) = " + currency.validate(deOut, Locale.GERMANY));

org.apache.commons.validator.routines.BigDecimalValidator percent = PercentValidator.getInstance();
BigDecimal p1 = percent.validate("50%", Locale.US);
System.out.println("validate(50%, US)   = " + p1 + " (" + p1.getClass().getName() + ")");
BigDecimal p2 = percent.validate("12.5%", Locale.US);
System.out.println("validate(12.5%, US) = " + p2);
System.out.println("format(0.125, US)   = " + percent.format(new BigDecimal("0.125"), Locale.US));

实测回显(完整 12 行):

---- CurrencyValidator ----
validate($1,234.56, US)  = 1234.56 (java.math.BigDecimal)
format(1234.56, US)      = $1,234.56
format(1234.56, DE)      = 1.234,56 €
validate(ASCII 空格写法, DE) = null
往返 validate(format 产物, DE) = 1234.56
---- PercentValidator ----
validate(50%, US)   = 0.50 (java.math.BigDecimal)
validate(12.5%, US) = 0.12
format(0.125, US)   = 12%

要点提示:两个校验器的 getInstance() 声明类型都是父类 BigDecimalValidator,返回值统一是 BigDecimal;德国 locale 输出中 € 与数字之间是 U+00A0 不换行空格(JDK 17 CLDR 行为),手敲 ASCII 空格的 1.234,56 € 解析为 null,而 format 产物可以往返解析成功——前端传值务必原样透传 formatter 输出;PercentValidator 把 12.5% 解析成 0.12 是默认实例小数位为 0 加 ROUND_DOWN 截断的叠加结果,百分比有小数需求时须自建 NumberFormat 实例。

7.4 LongValidator 与 IntegerValidator

实测代码(RoutinesProbe.java 数值段):

LongValidator lv = LongValidator.getInstance();
Long l1 = lv.validate("123456789012", Locale.US);
System.out.println("validate(123456789012, US) = " + l1 + " (" + l1.getClass().getName() + ")");
System.out.println("isInRange(5, 1, 10)  = " + lv.isInRange(5L, 1L, 10L));
System.out.println("isInRange(11, 1, 10) = " + lv.isInRange(11L, 1L, 10L));
System.out.println("validate(abc, US)    = " + lv.validate("abc", Locale.US));
IntegerValidator intv = IntegerValidator.getInstance();
Integer i1 = intv.validate("42", Locale.US);
System.out.println("validate(42, US)     = " + i1 + " (" + i1.getClass().getName() + ")");

实测回显(完整 6 行):

---- LongValidator / IntegerValidator ----
validate(123456789012, US) = 123456789012 (java.lang.Long)
isInRange(5, 1, 10)  = true
isInRange(11, 1, 10) = false
validate(abc, US)    = null
validate(42, US)     = 42 (java.lang.Integer)

要点提示:validate null 安全,isInRange 直接接收数值本身用于范围复核;各数值校验器方法签名一致(validate/isInRange/minIn/maxIn/format),按返回类型挑选即可,Float/Double 版本多一个 minFractionalDigits 精度参数。

八、CreditCardValidator:信用卡号校验

8.1 构造器与卡种开关

  • new CreditCardValidator(long options):按位或组合卡种开关;
  • boolean isValid(String cardNumber):先做卡号格式与卡种匹配,再交给 Luhn 算法验检查位。

卡种开关常量:VISA、MASTERCARD、AMEX、DISCOVER、DINERS、MASTERCARD_PRE_OCT2016(历史 BIN 段),另有 NONE。构造时组合,实例不可变。

8.2 实测

实测代码(CheckDigitProbe.java 卡种段):

CreditCardValidator cc1 = new CreditCardValidator(CreditCardValidator.VISA + CreditCardValidator.AMEX);
CreditCardValidator cc2 = new CreditCardValidator(CreditCardValidator.VISA + CreditCardValidator.AMEX
        + CreditCardValidator.MASTERCARD + CreditCardValidator.DISCOVER);
System.out.println("VISA+AMEX  6011111111111117 = " + cc1.isValid("6011111111111117"));
System.out.println("全卡种     6011111111111117 = " + cc2.isValid("6011111111111117"));
System.out.println("全卡种     4111111111111111 = " + cc2.isValid("4111111111111111"));
System.out.println("全卡种     378282246310005  = " + cc2.isValid("378282246310005"));
System.out.println("全卡种     4111111111111112 = " + cc2.isValid("4111111111111112"));

实测回显(完整 5 行):

---- CreditCardValidator(卡种开关 + Luhn) ----
VISA+AMEX  6011111111111117 = false
全卡种     6011111111111117 = true
全卡种     4111111111111111 = true
全卡种     378282246310005  = true
全卡种     4111111111111112 = false

要点提示:6011111111111117 是 Luhn 合法的 Discover 卡号——白名单只放 VISA+AMEX 时被拒、放开 DISCOVER 后通过,说明卡种过滤与检查位校验是两级串联;末位换 2 后 Luhn 不再成立,返回 false。注意组件只验证「号码格式像一张有效卡」,不校验发卡行状态与额度,风控环节仍需支付网关。

九、CheckDigit 检查位体系

9.1 接口与实现类

org.apache.commons.validator.routines.checkdigit 包的核心是 CheckDigit 接口,1.11.0 中只有两个方法:

方法行为
boolean isValid(String code)验证带检查位的完整串
String calculate(String code)对不含检查位的部分计算校验位,返回单个字符

实现类清单:LuhnCheckDigit(信用卡/IMEI)、ISBNCheckDigit(ISBN-10/13,含 ISBN10_CHECK_DIGIT/ISBN13_CHECK_DIGIT/ISBN_CHECK_DIGIT 三个单例常量)、IBANCheckDigit、ISINCheckDigit、ModulusTenCheckDigit(权重可配的通用模 10)、ModulusCheckDigit(模 N 抽象基类)。1.11.0 没有 Mod11 现成实现类(历史 1.x 版本同样没有),需要时继承 ModulusCheckDigit 自实现(见 9.3)。

💡 技术拓展:常见检查位算法如何选?

算法典型应用1.11.0 提供方式
Luhn(模 10,权重 1/2 交替)信用卡号、IMEILuhnCheckDigit.LUHN_CHECK_DIGIT 单例
ISBN 校验图书条码ISBNCheckDigit 单例常量,自动区分 10/13
IBAN(97 取模)欧洲银行账号new IBANCheckDigit()
ISIN证券识别码new ISINCheckDigit()
通用模 10(自定义权重)EAN/UPC、企业自定义编码new ModulusTenCheckDigit(weights, useRightPos)
模 11(权重循环)国内组织机构码、部分票据号继承 ModulusCheckDigit 自实现

9.2 LuhnCheckDigit

实测代码(CheckDigitProbe.java Luhn 段):

CheckDigit luhn = LuhnCheckDigit.LUHN_CHECK_DIGIT;
System.out.println("isValid(4417123456789113) = " + luhn.isValid("4417123456789113"));
System.out.println("isValid(4111111111111111) = " + luhn.isValid("4111111111111111"));
System.out.println("isValid(4111111111111112) = " + luhn.isValid("4111111111111112"));
System.out.println("isValid(5500005555555559) = " + luhn.isValid("5500005555555559"));
System.out.println("calculate(441712345678911) = " + luhn.calculate("441712345678911"));
String generated = "441712345678911" + luhn.calculate("441712345678911");
System.out.println("isValid(拼接后 " + generated + ") = " + luhn.isValid(generated));

实测回显:

---- LuhnCheckDigit(信用卡号) ----
isValid(4417123456789113) = true
isValid(4111111111111111) = true
isValid(4111111111111112) = false
isValid(5500005555555559) = true
calculate(441712345678911) = 3
isValid(拼接后 4417123456789113) = true

要点提示:接口没有「生成完整号码」的 generate 方法,生成完整编码的标准写法就是上文 原串 + calculate(原串) 拼接,回验通过。

9.3 模 11 自实现:继承 ModulusCheckDigit

ModulusCheckDigit 唯一必须实现的抽象方法是 weightedValue(int charValue, int leftPos, int rightPos)。关键约定:rightPos=1 是校验位自身的位置(权重为 1),数据位从 rightPos=2 起向左递增。实测代码(CheckDigitProbe.java Mod11 段,经典 Mod11 权重 2 到 10 自右向左循环):

static final class Mod11CheckDigit extends ModulusCheckDigit {
    Mod11CheckDigit() { super(11); }

    @Override
    protected int weightedValue(int charValue, int leftPos, int rightPos) {
        // rightPos=1 是校验位自身位置(权重 1);数据位自右向左权重 2 到 10 循环
        int weight = rightPos;
        if (weight > 10) {
            weight = (weight - 2) % 9 + 2;
        }
        return charValue * weight;
    }
}

Mod11CheckDigit mod11 = new Mod11CheckDigit();
System.out.println("calculate(036532)         = " + mod11.calculate("036532"));
System.out.println("isValid(0365327)          = " + mod11.isValid("0365327"));
System.out.println("isValid(0365328)          = " + mod11.isValid("0365328"));

实测回显:

---- Mod11(ModulusCheckDigit 子类自实现,模 11、权重 2..10 权重 2..10 自右向左) ----
calculate(036532)         = 7
isValid(0365327)          = true
isValid(0365328)          = false

要点提示:初版实现把权重起点算在校验位上,运行即抛 CheckDigitException: Invalid Check Digit Value =10——rightPos=1 的位置不参与加权求和(权重 1 的实现直接乘即可,需要跳过时用 if (rightPos > 1) 分流)。读基类 calculate 源码确认位置约定后修正权重函数,036532 按经典 Mod11 应得校验位 7,回验一致。

9.4 ModulusTenCheckDigit:EAN-13 等通用模 10

构造器 new ModulusTenCheckDigit(int[] positionWeight, boolean useRightPos, boolean sumWeightedDigits):positionWeight 是自指定基准端起循环的权重数组,useRightPos=true 表示权重自右端对齐。EAN-13 条码的权重是自右向左 1、3 交替(校验位权重 1):

ModulusTenCheckDigit ean13 = new ModulusTenCheckDigit(new int[]{1, 3}, true);
System.out.println("isValid(4006381333931)    = " + ean13.isValid("4006381333931"));
System.out.println("isValid(4006381333932)    = " + ean13.isValid("4006381333932"));
System.out.println("calculate(400638133393)   = " + ean13.calculate("400638133393"));

实测回显:

---- ModulusTenCheckDigit(通用模 10,权重可配;EAN-13 等价写法) ----
isValid(4006381333931)    = true
isValid(4006381333932)    = false
calculate(400638133393)   = 1

要点提示:sumWeightedDigits 区分两种求和口径——加权和直接累加(EAN 用这种)与先把各位加权结果按位求和再累加(某些 UPC 变体);权重数组自校验位起算时记得 useRightPos 传 true,否则对不齐整条编码的奇偶位。

9.5 ISBN / IBAN / ISIN

实测代码(CheckDigitProbe.java 对应段):

System.out.println("ISBN10 isValid(0306406152)    = " + ISBNCheckDigit.ISBN10_CHECK_DIGIT.isValid("0306406152"));
System.out.println("ISBN13 isValid(9780306406157) = " + ISBNCheckDigit.ISBN13_CHECK_DIGIT.isValid("9780306406157"));
System.out.println("ISBN   isValid(0306406152)    = " + ISBNCheckDigit.ISBN_CHECK_DIGIT.isValid("0306406152"));
System.out.println("ISBN   isValid(0306406153)    = " + ISBNCheckDigit.ISBN_CHECK_DIGIT.isValid("0306406153"));
System.out.println("ISBN   isValid(9780306406157) = " + ISBNCheckDigit.ISBN_CHECK_DIGIT.isValid("9780306406157"));
System.out.println("ISBN   isValid(9780306406158) = " + ISBNCheckDigit.ISBN_CHECK_DIGIT.isValid("9780306406158"));
CheckDigit iban = new IBANCheckDigit();
System.out.println("isValid(GB82WEST12345698765432) = " + iban.isValid("GB82WEST12345698765432"));
System.out.println("isValid(DE89370400440532013000) = " + iban.isValid("DE89370400440532013000"));
System.out.println("isValid(GB82WEST12345698765433) = " + iban.isValid("GB82WEST12345698765433"));
CheckDigit isin = new ISINCheckDigit();
System.out.println("isValid(US0378331005)     = " + isin.isValid("US0378331005"));
System.out.println("isValid(US5949181045)     = " + isin.isValid("US5949181045"));
System.out.println("isValid(US0378331006)     = " + isin.isValid("US0378331006"));

实测回显:

---- ISBNCheckDigit(ISBN-10 / ISBN-13) ----
ISBN10 isValid(0306406152)    = true
ISBN13 isValid(9780306406157) = true
ISBN   isValid(0306406152)    = true
ISBN   isValid(0306406153)    = false
ISBN   isValid(9780306406157) = true
ISBN   isValid(9780306406158) = false
---- IBANCheckDigit ----
isValid(GB82WEST12345698765432) = true
isValid(DE89370400440532013000) = true
isValid(GB82WEST12345698765433) = false
---- ISINCheckDigit ----
isValid(US0378331005)     = true
isValid(US5949181045)     = true
isValid(US0378331006)     = false

要点提示:ISBN_CHECK_DIGIT 按长度自动分流 10 位(含 X 校验位的旧式 ISBN)与 13 位(EAN-13 口径);IBAN 示例号与 ISIN 示例号(苹果公司 US0378331005)均为各自标准文档的官方测试用例,末位篡改即拒。IBAN 校验只验检查位,不校验各国长度规则(德国 22 位、法国 27 位等需另行核对)。

十、旧式 XML 校验框架

10.1 核心类与运行时对象

XML 方式的一次校验涉及四个运行时对象:ValidatorResources(解析规则文件)、Validator(会话,绑定表单名与 Bean)、ValidatorResults/ValidatorResult(结果)。典型驱动代码(XmlProbe.java 核心段):

try (InputStream forms = XmlProbe.class.getResourceAsStream("/validation.xml")) {
    ValidatorResources resources = new ValidatorResources(forms);
    Validator validator = new Validator(resources, "orderForm");
    validator.setParameter(Validator.BEAN_PARAM, bean);
    ValidatorResults results = validator.validate();
    for (Object pn : results.getPropertyNames()) {
        String prop = (String) pn;
        ValidatorResult vr = results.getValidatorResult(prop);
        for (Iterator<String> it = vr.getActions(); it.hasNext();) {
            String action = it.next();
            System.out.println(prop + " " + action + " = " + vr.isValid(action));
        }
    }
}

💡 技术拓展:XML 框架与 Bean Validation(JSR 380)如何选?

维度commons-validator XML 框架Bean Validation(Hibernate Validator)
规则载体validation.xml + 自定义回调类注解 + 校验器实现
依赖体量digester 等 4 个传递依赖较大(EL、JPA 元模型可选)
与 Struts 1 的关系即其内核,遗留系统兼容首选无历史包袱
新项目建议仅在维护遗留规则文件时使用默认选择

10.2 回调方法约定

每个 <validator> 动作对应一个静态回调方法,签名固定为四参数、返回 Boolean:

public static Boolean validateEmail(Object bean, ValidatorAction va, Field field, List<Object> errors) {
    String value = ValidatorUtils.getValueAsString(bean, field.getProperty());
    if (GenericValidator.isBlankOrNull(value)) {
        return Boolean.TRUE;
    }
    return GenericValidator.isEmail(value);
}

约定要点:ValidatorUtils.getValueAsString 按 field.getProperty() 从 Bean 取值;field.getVarValue("minlength") 取 <var> 参数;返回 Boolean.TRUE 记 PASS、Boolean.FALSE 记 FAIL;方法内抛出的异常会被框架捕获、打 SEVERE 日志并把该动作记为 FAIL,不会中断其余字段。1.11.0 不再内置 Struts 的 FieldChecks 工具类,以上回调必须自行编写。

10.3 端到端实测

配套规则文件 validation.xml(5 个动作:required、minLength、email、intRange、mask,含一个 postCodeMask 常量)与 OrderBean 的完整定义见快速入门篇第五节,此处直接给出两轮实测回显(完整 10 行):

---- 第一轮:全部非法 ----
postCode       required=PASS  mask=FAIL
customerName   required=FAIL
email          required=PASS  email=FAIL
age            intRange=FAIL  required=PASS
---- 第二轮:全部合法 ----
postCode       required=PASS  mask=PASS
customerName   minLength=PASS  required=PASS
email          required=PASS  email=PASS
age            intRange=PASS  required=PASS

验证结果:第一轮 customerName(空串)只有 required=FAIL,说明 required 失败后同字段后续动作短路;email 非空但格式非法时 required=PASS 且 email=FAIL,两个动作独立判定。第二轮四个字段全部 PASS,mask 正则(经常量替换)、intRange 区间(29 到 60)均按配置生效。该套 XML 方式在 JDK 17 下编译运行无兼容性问题。

十一、常见问题(FAQ)

1. CheckDigit 接口为什么没有生成校验位的 generate 方法? 1.11.0 的接口只有 isValid(String) 与 calculate(String) 两个方法。生成完整编码用 原串 + calculate(原串) 拼接,9.2 节已实测拼接结果回验通过。

2. 为什么找不到 Mod11CheckDigit 类? 1.11.0 的 checkdigit 包及历史 1.x 版本源码中均无此类。模 11 需求按 9.3 节继承 ModulusCheckDigit 自实现,权重约定(校验位 rightPos=1)是唯一易错点。

3. UrlValidator 的 isValidScheme 等分解方法为什么编译不过? 它们是 protected,1.11.0 对外只暴露整体 isValid。确需分步校验时写个子类把方法包一层 public(4.2 节示例),不建议反射调用。

4. user@[::1] 为什么不合法而 user@[192.168.1.2] 合法? EmailValidator 的域字面量校验只覆盖 IPv4,IPv6 分支不可达(3.2 节实测)。需要 IPv6 邮箱字面量时在业务层自行解析。

5. 12.5% 为什么解析成 0.12? PercentValidator 默认实例的小数位为 0 且按 ROUND_DOWN 截断(7.3 节实测),并非丢失精度;需要保留小数的百分比请自建 NumberFormat 后构造校验器。

6. XML 方式回调方法签名能否自定义? 不能。固定为 (Object bean, ValidatorAction va, Field field, List errors) 四参数、返回 Boolean,参数个数或类型不符时规则文件加载即报错。抛异常会记 SEVERE 并判 FAIL,不会中断校验。

7. XML 方式需要随包携带 validator-rules.xml 吗? 不需要也拿不到——该文件属于 Struts 1,1.11.0 jar 内没有。自写 validation.xml + 自定义回调即可完整跑通(10.3 节已在 JDK 17 实测)。

十二、参考链接

Apache Commons Validator 1.11.0 常用 API 与校验器详解 - Quzuo