Apache Commons Validator 1.11.0 常用 API 与校验器详解
系统梳理 Apache Commons Validator 1.11.0 的 routines 校验器、CheckDigit 检查位算法与旧式 XML 校验框架 API,每个方法均附可复现代码与 JDK 17 实测回显
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() |
| UrlValidator | URL | new UrlValidator(long options) |
| InetAddressValidator | IPv4/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 取参 |
| GenericTypeValidator | formatInt/formatLong 等类型转换 |
| util.ValidatorUtils | getValueAsString 等 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.xIPv4 映射段。
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 交替) | 信用卡号、IMEI | LuhnCheckDigit.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 实测)。
十二、参考链接
- EmailValidator API 文档:https://commons.apache.org/proper/commons-validator/apidocs/org/apache/commons/validator/routines/EmailValidator.html
- UrlValidator API 文档:https://commons.apache.org/proper/commons-validator/apidocs/org/apache/commons/validator/routines/UrlValidator.html
- checkdigit 包 API 文档:https://commons.apache.org/proper/commons-validator/apidocs/org/apache/commons/validator/routines/checkdigit/package-summary.html
- 用户指南(XML 配置章节):https://commons.apache.org/proper/commons-validator/userguide/index.html
- 本专栏配套文档:《Apache Commons Validator 1.11.0 快速入门与集成示例》《Apache Commons Validator 1.11.0 配置参数详解》