第十三章. 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-core 的 utils.regex, factory 和 constant 包中,精心设计了一套 三层架构 来实现这个校验功能。本章,我们将深入解析这套架构的设计思想与实战应用。

本章学习路径

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)。
    • 适用:用于“断言”,不符合就中断程序(例如在 Controller 或 Service 的入口处)。

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 业务专属 的校验(如 Account 和 Status)。

我们现在来实战这些 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 增强的 isAccount 和 isStatus 为通用业务校验提供了极大的便利。


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.PASSWORD,SysUserBo 这个 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 Validator(RegexValidator 的父类)中的 Pattern 常量 不带缓存。这是一个非常隐蔽的性能陷阱。

我们来对比一下 Hutool Validator 和 RVP 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 Validator 的 Pattern 常量(如 RegexValidator.GENERAL 或 Validator.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 Validator 的 Pattern(如 GENERAL)是 不带缓存 的。因此在“二开”中,我们应 优先使用 RVP 增强的 isAccount() 等方法。