第十三章. common-core 正则校验:Validator 三层架构详解

第十三章. common-core 正则校验:Validator 三层架构详解

摘要:本章我们将深入 RVP 封装的“字段校验器”体系。我们将重点分析 RVP 独创的“Constants (字符串) -> Factory (编译) -> Validator (调用)”三层架构,并掌握 Hutool Validator(父类)和 RVP RegexValidator(子类)在数据校验中的实战应用。

在上一章(第十二章)中,我们深入学习了 RegexUtils (Hutool ReUtil),它是一个 文本处理 工具。我们掌握了它的“痛点”(原生 Pattern/Matcher)和核心功能(get, findAll, replaceAll),并系统学习了正则表达式的语法。

现在,我们转向 common-core另一套 基于正则的工具体系。这套体系 不用于“文本处理”,而是专门用于“数据校验”(判断是非)。

RVP 在 common-coreutils.regex, factoryconstant 包中,精心设计了一套 三层架构 来实现这个校验功能。本章,我们将深入解析这套架构的设计思想与实战应用。

本章学习路径

Validator 三层架构详解


13.1. RVP 校验器架构:Constants -> Factory -> Validator

在 RVP 框架中,RegexUtils (Hutool ReUtil) 用于“文本处理”,而 RegexValidator (Hutool Validator) 用于“数据校验”。

在开始实战 RegexValidator 之前,我们必须先理解 RVP 围绕它构建的这套三层架构,这套架构展示了框架在“性能”与“复用”上的思考。

13.1.1. 架构概览:三层分离的设计哲学

我们知道,Java 原生的 Pattern.compile(regex) 是一个相对耗时的操作(它需要将正则表达式字符串编译成一个状态机)。如果每次调用 isMatch(regex, ...) 都重新编译一次,性能会很差。

RVP 的三层架构正是为了解决这个问题:

  1. RegexConstants (常量层):只负责存储 正则表达式字符串 (String)。
  2. RegexPatternPoolFactory (工厂/编译层):负责将 RegexConstants 中的“字符串”编译Pattern 对象,并 缓存(“池化”)起来。
  3. RegexValidator (应用/门面层):负责调用 Factory已编译好Pattern 对象,去执行最终的 校验 逻辑。

我们来逐层解析这三个文件。

13.1.2. 【层级 1】RegexConstants (字符串池)

文件路径ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java

这个类继承了 Hutool 的 RegexPool(Hutool 的正则字符串常量池),并在其基础上,增强 了 RVP 业务所需的专属常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 位于 RegexConstants.java
public interface RegexConstants extends RegexPool {

/**
* 字典类型必须以字母开头,且只能为(小写字母,数字,下滑线)
*/
String DICTIONARY_TYPE = "^[a-z][a-z0-9_]*$";

// ... RVP 专属的 ACCOUNT, PASSWORD ...

/**
* 通用状态(0表示正常,1表示停用)
*/
String STATUS = "^[01]$";
}

这一层非常纯粹,只定义 String 常量。例如 STATUS = "^[01]$",通过 ^ (开头) 和 $ (结尾) 锚定,确保这个字符串 必须且只能 是 “0” 或 “1”。

13.1.3. 【层级 2】RegexPatternPoolFactory (编译池)

文件路径ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/factory/RegexPatternPoolFactory.java

这一层负责**“编译”“缓存”**。它同样继承了 Hutool 的 PatternPool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 位于 RegexPatternPoolFactory.java
public class RegexPatternPoolFactory extends PatternPool {

/**
* 字典类型
*/
public static final Pattern DICTIONARY_TYPE = get(RegexConstants.DICTIONARY_TYPE);

/**
* 通用状态
*/
public static final Pattern STATUS = get(RegexConstants.STATUS);

// ...
}

分析:此类中的所有字段都是 static final Pattern 类型。它调用了父类 PatternPool.get(String regex) 方法。

get(String regex) 方法(Hutool 提供)内部 自带缓存PATTERN_CACHE)。它会检查这个 regex 字符串是否已经被编译过,如果已在缓存中,直接返回 Pattern 对象,如果不在,才调用 Pattern.compile(),存入缓存后再返回。

通过这一层,RVP 保证了 RegexConstants.DICTIONARY_TYPE 这个字符串 在整个应用生命周期中只被编译一次

13.1.4. 【层级 3】RegexValidator (工具门面)

文件路径ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexValidator.java

这是我们“二开”时 唯一应该调用 的类。它继承了 Hutool 的 Validator,并聚合了 Factory 层的成果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 位于 RegexValidator.java
public class RegexValidator extends Validator {

// 1. 聚合 Factory 的成果
public static final Pattern ACCOUNT = RegexPatternPoolFactory.ACCOUNT;
public static final Pattern STATUS = RegexPatternPoolFactory.STATUS;
// ...

// 2. RVP 增强的校验方法
public static boolean isAccount(CharSequence value) {
// 3. 调用父类 (Validator) 的 isMatchRegex 方法,传入编译好的 Pattern
return isMatchRegex(ACCOUNT, value);
}

public static <T extends CharSequence> T validateAccount(T value, String errorMsg) {
if (!isAccount(value)) {
throw new ValidateException(errorMsg);
}
return value;
}
// ... (isStatus 和 validateStatus 同理)
}

分析
RegexValidator 作为“门面”,它做了两件事:

  1. Factory已编译、已缓存Pattern 对象(如 ACCOUNT, STATUS)声明为 public static final,方便我们 在其他地方(如 RegexUtils)中复用
  2. 提供了 RVP 专属的校验方法,如 isAccount(value),其内部调用了父类 Validator.isMatchRegex(),并传入了 已编译好ACCOUNT Pattern

13.2. Hutool Validator 详解(RegexValidator 的父类)

在上一节中,我们理清了 RVP 的三层架构。我们知道 RVP RegexValidator 继承了 Hutool Validator。这个父类 Validator 已经提供了海量的通用校验方法(isMoney, isEmail, isUrl 等)。

13.2.1. isXxx() vs validateXxx():返回 boolean 与抛出异常的区别

Hutool Validator 的设计遵循一个清晰的命名约定,这对于我们“二开”使用者来说至关重要:

  1. isXxx(value)

    • 功能:判断 value 是否符合规则。
    • 返回true (符合) 或 false (不符合)。
    • 特点不抛出异常。你需要自己写 if 逻辑来处理 false 的情况。
    • 适用:用在业务逻辑的 if 判断中。
  2. validateXxx(value, errorMsg)

    • 功能:校验 value 是否符合规则。
    • 返回value(如果校验通过)。
    • 特点:如果校验 不通过,会 立即抛出 ValidateException(errorMsg)
    • 适用:用于“断言”,不符合就中断程序(例如在 ControllerService 的入口处)。

13.2.2. 测试准备:创建 RegexValidatorTest

RegexValidator 是一个纯 Java 工具类,不依赖 Spring 容器。我们使用 main 方法来测试它。

文件路径ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/utils/test/RegexValidatorTest.java
(我们创建一个新的 main 方法测试类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.dromara.demo.utils.test;

import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
// 导入 RVP 的 RegexValidator
import org.dromara.common.core.utils.regex.RegexValidator;

import java.math.BigDecimal;

/**
* RegexValidator 校验器实战
* (纯 Java 测试,无需 Spring)
*/
public class RegexValidatorTest {

private static final Log console = LogFactory.get();

// psvm
public static void main(String[] args) {
testHutoolValidatorApis();
}

/**
* 测试 1:Hutool Validator 基础 API
*/
public static void testHutoolValidatorApis() {
// 我们将在这里分步测试
}
}

13.2.3. 实战:isMoney, isEmail, isUrl, isBetween

testHutoolValidatorApis() 方法中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 位于 testHutoolValidatorApis()

console.info("--- 1. 测试 isXxx() (返回布尔值) ---");

// 1. 校验货币 (isMoney)
// Hutool 的 money 正则要求必须是数字,可以带两位小数
console.info("【isMoney '123.45'】: {}", RegexValidator.isMoney("123.45")); // -> true
console.info("【isMoney '123.ABC'】: {}", RegexValidator.isMoney("123.ABC")); // -> false
console.info("【isMoney '123.'】: {}", RegexValidator.isMoney("123.")); // -> false

// 2. 校验邮箱 (isEmail)
console.info("【isEmail 'test@qq.com'】: {}", RegexValidator.isEmail("test@qq.com")); // -> true

// 3. 校验URL
console.info("【isUrl 'http://ruoyi.vip'】: {}", RegexValidator.isUrl("http://ruoyi.vip")); // -> true
console.info("【isUrl 'httpx://ruoyi.vip'】: {}", RegexValidator.isUrl("httpx://ruoyi.vip")); // -> false

// 4. 校验数字范围 (isBetween)
console.info("【isBetween(2, 1, 3)】: {}", RegexValidator.isBetween(2, 1, 3)); // -> true
console.info("【isBetween(5, 1, 3)】: {}", RegexValidator.isBetween(5, 1, 3)); // -> false
// 它也支持 BigDecimal
console.info("【isBetween(2.0, 1.0, 3.0)】: {}",
RegexValidator.isBetween(new BigDecimal("2.0"), new BigDecimal("1.0"), new BigDecimal("3.0")) // -> true
);

运行 main 方法,控制台输出(内联在注释中):
我们看到 isXxx 方法提供了大量预设的、开箱即用的校验逻辑。

我们继续在 testHutoolValidatorApis() 方法中添加 validateXxx 的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 位于 testHutoolValidatorApis()
console.info("--- 2. 测试 validateXxx() (校验失败则抛出异常) ---");

// 1. 校验为 True
try {
// 传入 false,会校验失败并抛出异常
RegexValidator.validateTrue(false, "错误信息: 必须为 true");
} catch (Exception e) {
console.error("【validateTrue(false)】捕获异常: {}", e.getMessage()); // -> 错误信息: 必须为 true
}

// 2. 自定义正则校验
try {
// validateMatchRegex 用于自定义正则校验
// 规则是 [a-z]+ (1个或多个小写字母),但我们传入了 "123"
RegexValidator.validateMatchRegex("[a-z]+", "123", "必须是小写字母");
} catch (Exception e) {
console.error("【validateMatchRegex】捕获异常: {}", e.getMessage()); // -> 必须是小写字母
}

运行 main 方法,控制台输出(内联在注释中):
分析validateXxx 方法在校验失败时,会立即抛出 ValidateException 并携带我们指定的错误信息。这在需要中断程序执行的场景下非常有用。


13.3. RVP RegexValidator 增强实战

在 13.1.4 中我们看到,RVP 在 Hutool Validator 的基础上,增加了 RVP 业务专属 的校验(如 AccountStatus)。

我们现在来实战这些 RVP 增强的方法。

13.3.1. 实战 isAccount() / validateAccount() (RVP 独有)

isAccount 检查是否符合 RVP 定义的“账号”规则(以字母开头,5-16 位,包含字母、数字、下划线)。

RegexValidatorTest.java 中创建 testRvpValidatorApis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class RegexValidatorTest {
// ... main 和 testHutoolValidatorApis ...

public static void main(String[] args) {
// testHutoolValidatorApis();
testRvpValidatorApis();
}

/**
* 测试 2:RVP RegexValidator 增强 API
*/
public static void testRvpValidatorApis() {
console.info("--- 3. 测试 isAccount (RVP 增强) ---");

// 1. 合法账号
console.info("【isAccount 'admin'】: {}", RegexValidator.isAccount("admin")); // -> true

// 2. 非法账号
console.info("【isAccount '123admin'】: {}", RegexValidator.isAccount("123admin")); // -> false (非字母开头)
console.info("【isAccount 'adm'】: {}", RegexValidator.isAccount("adm")); // -> false (长度不足5)
console.info("【isAccount 'admin-test'】: {}", RegexValidator.isAccount("admin-test")); // -> false (包含非法字符 '-')

console.info("--- 4. 测试 validateAccount (抛出异常) ---");
try {
// 校验通过
RegexValidator.validateAccount("admin", "账号不符合规则");
console.info("【validateAccount 'admin'】: 校验通过"); // -> 校验通过

// 校验失败
RegexValidator.validateAccount("admin-test", "账号不符合规则");
} catch (Exception e) {
console.error("【validateAccount 'admin-test'】捕获异常: {}", e.getMessage()); // -> 账号不符合规则
}
}
}

运行 main 方法,控制台输出(内联在注释中):
isAccount 精确地执行了 RegexConstants.ACCOUNT 常量中定义的规则。

13.3.2. 实战 isStatus() / validateStatus() (RVP 独有)

isStatus 检查是否符合 RVP 的“通用状态”规则(必须是 “0” 或 “1”)。

testRvpValidatorApis() 方法中继续添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 位于 testRvpValidatorApis()
console.info("--- 5. 测试 isStatus (RVP 增强) ---");

console.info("【isStatus '0'】: {}", RegexValidator.isStatus("0")); // -> true
console.info("【isStatus '1'】: {}", RegexValidator.isStatus("1")); // -> true
console.info("【isStatus '2'】: {}", RegexValidator.isStatus("2")); // -> false
console.info("【isStatus '01'】: {}", RegexValidator.isStatus("01")); // -> false

console.info("--- 6. 测试 validateStatus (抛出异常) ---");
try {
RegexValidator.validateStatus("2", "状态有误");
} catch (Exception e) {
console.error("【validateStatus '2'】捕获异常: {}", e.getMessage()); // -> 状态有误
}

运行 main 方法(接上文输出):
分析:RVP 增强的 isAccountisStatus 为通用业务校验提供了极大的便利。


13.4. 【RVP 真实场景】如何在“注解校验”中使用

在第十一章中,我们学习了 RVP 的参数校验体系,当时我们使用了 @Pattern(regexp = "...") 来执行自定义正则校验。

如果我们直接在注解里“硬编码”正则表达式,会带来两个严重问题:

  1. 难以阅读:复杂的正则(比如密码强度)写在 DTO 里,会让 DTO 变得臃肿不堪。
  2. 难以维护:如果“账号”或“密码”的校验规则需要全局修改,我们就必须去搜索并修改 所有 DTO(UserBo, RegisterBody 等)中的 @Pattern 注解,这极易遗漏。

RVP 的解决方案:这正是 RegexConstants.java(字符串池)存在的 真正价值。它提供了一个“单一事实来源”(Single Source of Truth),让所有校验注解都来引用它。

我们来看一个 RVP system 模块中的真实案例:

1
2
3
4
5
6
// 位于 ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/RegexConstants.java
public interface RegexConstants extends RegexPool {
// ...
// RVP 定义的复杂密码规则
String PASSWORD = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
}

这个正则要求密码必须同时包含“小写字母、大写字母、数字、特殊字符”,且至少 8 位。

现在,我们来看“注册”功能是如何使用它的:
文件路径ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysUserBo.java(或其他相关 DTO)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 位于 SysUserBo.java (或 RegisterBody.java 等)
public class SysUserBo {

// ...

@NotBlank(message = "密码不能为空", groups = { AddGroup.class })
// 【关键】regexp 直接引用了 RegexConstants
@Pattern(
regexp = RegexConstants.PASSWORD,
message = "密码必须包含大小写字母、数字和特殊字符,且长度至少为8位",
groups = { AddGroup.class, EditGroup.class }
)
private String password;

// ...
}

分析
@Pattern 注解的 regexp 属性接收的只是一个 String。通过引用 RegexConstants.PASSWORDSysUserBo 这个 DTO 就和“密码的具体规则”解耦 了。

【二开守则】:当我们“二开”时,如果需要新增一个全局复用的正则校验(例如“工号”employee_id):

  1. 第一步:在 RegexConstants.java 中添加 String EMPLOYEE_ID = "^EMP\\d{8}$";
  2. 第二步:在我们的 DTO(例如 MyEmployeeBo)中,使用 @Pattern(regexp = RegexConstants.EMPLOYEE_ID, ...)

13.5. 性能陷阱分析:Validator vs Factory (缓存差异)

在 13.1.3 节中,我们提到 RVP 的 RegexPatternPoolFactory带缓存 的,但 Hutool ValidatorRegexValidator 的父类)中的 Pattern 常量 不带缓存。这是一个非常隐蔽的性能陷阱。

我们来对比一下 Hutool ValidatorRVP RegexValidator 的设计差异。

Hutool Validator (父类) 源码(简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
// 位于 Hutool 的 Validator.java
public class Validator {

// 【Hutool 陷阱】:Hutool Validator 直接调用 compile,【没有】使用 PatternPool 缓存
public static final Pattern GENERAL = Pattern.compile("^[\\w]+$");
public static final Pattern NUMBERS = Pattern.compile("\\d+");
// ...

// 校验方法
public static boolean isMatchRegex(Pattern pattern, CharSequence value) {
// ...
}
}

RVP RegexValidator (子类) 源码(精简):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 位于 RVP 的 RegexValidator.java
public class RegexValidator extends Validator {

// 【RVP 优化】:RVP Validator 从 Factory 获取【已缓存】的 Pattern
public static final Pattern ACCOUNT = RegexPatternPoolFactory.ACCOUNT;
public static final Pattern STATUS = RegexPatternPoolFactory.STATUS;
// ...

// RVP 增强方法
public static boolean isAccount(CharSequence value) {
// RVP 的方法调用的是 RVP 自己的【缓存 Pattern】
return isMatchRegex(ACCOUNT, value);
}
}

分析

  1. Hutool Validator 自身的 Pattern 常量(如 Validator.GENERAL)是在类加载时直接 Pattern.compile() 的,它 没有 利用 PatternPool 的缓存机制。
  2. RVP RegexValidator 在设计上 更胜一筹。它 没有 直接 compile,而是从 RegexPatternPoolFactory 中获取 已缓存Pattern

【二开守则】为何应优先使用 RegexValidator.ACCOUNT

  • 性能更优:RVP 的 RegexValidator.ACCOUNT (或 RegexValidator.STATUS 等) 100% 来自 PatternPool 缓存。
  • API 统一:RVP RegexValidator 提供了 isAccount() / validateAccount() 这样语义更清晰的方法,我们应优先调用它们。
  • 避免使用:我们“二开”时,应 避免 直接使用 RVP 继承 自 Hutool ValidatorPattern 常量(如 RegexValidator.GENERALValidator.NUMBERS),因为它们没有利用 RVP 的缓存体系。

13.6. 本章总结

在本章中,我们深入剖析了 RVP common-core 模块中的 正则校验 体系,它与上一章的“文本处理”(RegexUtils) 截然不同。

  1. RVP 三层架构:我们首先理解了 RVP 为“校验”设计的精妙分层:

    • RegexConstants (常量层):只存 String 字符串(如 "^[01]$")。
    • RegexPatternPoolFactory (工厂层):调用 PatternPool.get(),将字符串 编译Pattern 对象并 缓存
    • RegexValidator (门面层):继承 Hutool Validator,并聚合 Factory已缓存Pattern,提供给外部使用。
  2. Hutool Validator:我们掌握了 RegexValidator 父类 Validator 的核心使用方法:

    • isXxx():用于 if 判断,返回 boolean不抛异常
    • validateXxx():用于“断言”,校验失败 立即抛出 ValidateException
  3. RVP 真实场景:我们明确了 RegexConstants最主要用途——是在 DTO 中配合 @Pattern(regexp = RegexConstants.PASSWORD) 注解,实现“声明式校验”的统一维护。

  4. 性能与“二开”守则:我们分析了 RVP RegexValidator 提供的 Pattern(如 ACCOUNT)是 带缓存 的,而它继承自 Hutool ValidatorPattern(如 GENERAL)是 不带缓存 的。因此在“二开”中,我们应 优先使用 RVP 增强的 isAccount() 等方法