第十一章. common-core 校验框架:Validation 与自定义注解
第十一章. common-core 校验框架:Validation 与自定义注解
Prorise实现“二开”中复杂的校验需求的。
第十一章. common-core 校验框架:Validation 与自定义注解
摘要:本章我们将深入 RVP 的参数校验体系。我们将从 ValidatorConfig 配置(如“快速失败”)开始,掌握 @Validated 和 @NotBlank 等基础用法。重点是深入 RVP 的“二开”实践:如何利用 分组校验(AddGroup, EditGroup)实现不同场景(如“新增”与“修改”)的校验,以及如何使用 自定义注解(@EnumPattern, @DictPattern)实现动态业务校验。
在前面的章节中,我们已经深入分析了 StringUtils, StreamUtils, ReflectUtils 和 TreeBuildUtils 等核心工具。现在,我们转向 common-core 模块中一个至关重要的组成部分——参数校验(Validation)。
在任何 Web 应用中,对前端传入的数据进行校验,是保证数据安全和业务逻辑正确性的第一道防线。RVP 框架基于 spring-boot-starter-validation(它封装了 hibernate-validator)构建了一套强大且可扩展的校验体系。
本章,我们将深入 common-core 中的 validate 包,不仅要学会 如何使用 @NotBlank, @Email 等标准注解,更要重点掌握 RVP 是如何通过 自定义注解(如 @DictPattern, @EnumPattern)和 分组校验(AddGroup, EditGroup)来构建这一套完整的验证体系的
本章学习路径

11.1. RVP 校验体系概览:ValidatorConfig
RVP 的校验能力首先来源于 pom.xml 中的依赖和 config 包下的配置类。
11.1.1. 依赖引入:spring-boot-starter-validation
RVP 框架通过在 ruoyi-common 模块(具体在 common-core 根目录的 pom.xml 中)引入 spring-boot-starter-validation 依赖,从而自动获得了 hibernate-validator 这一 JSR 303/JSR 380 规范的实现。
1 | <dependency> |
引入这个依赖后,我们就可以在 Spring Boot 环境中使用 @NotBlank、@Email 等注解了。
11.1.2. 源码解析 ValidatorConfig:为何要配置 MessageSource?(国际化 i18n)
仅仅引入依赖是不够的。默认情况下,hibernate-validator 的校验失败信息是英文的(例如 must not be blank)。RVP 作为一个支持国际化(i18n)的框架,需要让这些提示信息变成中文(或根据请求头动态切换语言)。
这就是 ValidatorConfig 存在的核心价值。
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/config/ValidatorConfig.java
1 | // 这是一个自动配置类 |
分析:ValidatorConfig 向 Spring 容器注册了一个 Validator Bean。在注册时,它通过 factoryBean.setValidationMessageSource(messageSource),将 Spring 的 MessageSource(国际化资源)注入给了 hibernate-validator。
这样,当校验失败时,@NotBlank(message = "{user.name.not.blank}") 这样的注解就能自动去 ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties 文件中查找对应的中文提示:“用户名不能为空”。
11.1.3. 源码解析 快速失败 vs 全量校验
ValidatorConfig 中还有一段至关重要的配置:
1 | // 位于 ValidatorConfig.java 的 @Bean 方法中 |
fail_fast (快速失败) 模式是什么意思?
false(默认值):全量校验。校验器会检查 DTO 上的 所有 字段,然后返回一个 包含所有错误 的列表。例如,如果name和email都为空,前端会一次性收到两条错误信息:“姓名不能为空”、“邮箱不能为空”。true(RVP 的选择):快速失败。校验器在检查字段时,只要遇到 第一个 校验失败的字段(例如name为空),就会 立即停止 后续所有校验,并马上返回这一个错误。
RVP 为什么选择 true?
对于 API 接口而言,“快速失败”是更高效、更简洁的选择。它能更快地给前端一个明确的反馈,减少了不必要的计算开销,也简化了前端对错误信息的处理逻辑(一次只处理一个)。
11.2. 【基础】@Validated 与标准注解 (JSR 303)
配置完成后,我们就可以开始在“二开”中实战了。@Validated 是 Spring 提供的注解,用于“触发”对某个 Bean 的校验。
11.2.1. 测试准备:创建 ValidateController 与 Student DTO
和前面的章节一样,Validator 的测试必须在 Spring 环境中进行,所以我们不能使用 main 方法。我们将在 ruoyi-demo 模块中创建一个 Controller。
文件路径:ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/ValidateController.java
1 | package org.dromara.demo.controller; |
11.2.2. 【场景一】校验请求体
这是最常见的场景:校验前端 POST 或 PUT 请求发来的 JSON 数据。
步骤 1:为 DTO 添加注解
我们修改 Student 类,添加标准校验注解:
1 | // ... 位于 ValidateController 内部 ... |
步骤 2:在 Controller 方法参数上添加 @Validated
1 | // ... 位于 ValidateController ... |
步骤 3:实战验证(使用 ApiFox/Postman)
- 重启
DromaraApplication。 - 使用 ApiFox 向
POST http://localhost:8080/demo/validate/valid4发送请求。 - 测试“快速失败”:
- Body (JSON):
{}(空对象) - 返回 (HTTP 400):
{"code": 400, "msg": "姓名不能为空"} - 分析:校验器按 DTO 字段顺序(
name,age,email)检查,name首先失败,触发“快速失败”,程序 立即返回,不再检查age和email。
- Body (JSON):
- 测试通过:
- Body (JSON):
{"name": "张三", "age": 20, "email": "test@qq.com", "enName": "zhangsan"} - 返回 (HTTP 200):
{"code": 200, "msg": "操作成功", "data": ...}
- Body (JSON):
11.2.3. 【场景二】校验 Query 参数
【二开痛点】:如果我们只是想校验 GET 请求的一个 Query 参数(.../valid2?name=xxx),总不能也创建一个 DTO 吧?
解决方案:
- 在
ValidateController类 上添加@Validated注解。 - 在方法 参数 上直接添加校验注解。
步骤 1:修改 ValidateController
1 | // ... |
步骤 2:添加 valid2 接口
1 | // ... |
步骤 3:实战验证(使用 ApiFox/Postman)
- 重启
DromaraApplication。 - 测试失败:
- URL:
GET http://localhost:8080/demo/validate/valid2(不传name) - 返回 (HTTP 400):
{"code": 400, "msg": "valid2.name: 姓名不能为空"}
- URL:
- 测试成功:
- URL:
GET http://localhost:8080/demo/validate/valid2?name=zhangsan - 返回 (HTTP 200):
{"code": 200, "msg": "操作成功", "data": "zhangsan"}
- URL:
11.2.4. 国际化(i18n)实战:message 键与 Content-Language
在 11.1.2 中我们知道 RVP 支持国际化。如何使用?
步骤 1:修改 DTO(使用 message 键)
我们修改 Student DTO,将 message 从“硬编码”改为“i18n 键”:
1 | // 位于 ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties |
步骤 2:实战验证(使用 ApiFox/Postman)
- 重启
DromaraApplication。 - 向
POST http://localhost:8080/demo/validate/valid4发送请求。 - 测试中文(默认):
- Body (JSON):
{"name": "张三", "age": 20, "email": "bad-email"} - Headers: (不加
Content-Language) - 返回 (HTTP 400):
{"code": 400, "msg": "邮箱格式不正确"}(中文)
- Body (JSON):
- 测试英文:
- Body (JSON):
{"name": "Zhangsan", "age": 20, "email": "bad-email"} - Headers:
Content-Language: en-US - 返回 (HTTP 400):
{"code": 400, "msg": "Mailbox format error"}(英文)
- Body (JSON):
结论:ValidatorConfig 自动关联了 MessageSource,RVP 的校验体系已 原生支持国际化。
11.3. 【二开核心】分组校验:AddGroup 与 EditGroup
我们已经掌握了基础校验,但现在遇到了一个 最常见 的“二开”痛点。
11.3.1. 【二开痛点】“新增”时 name 必填,但“修改”时 name 选填
我们通常会为“新增”和“修改”复用同一个 DTO(Student)。但它们的校验规则是不同的:
- 新增 (Add):
name,age,email都必须提供。 - 修改 (Edit):
id必须提供,name,age,email允许 部分 更新(即选填)。
问题:@NotBlank(message = "姓名不能为空") 会同时应用于“新增”和“修改”。这导致“修改”时,即使用户只想改 email,也必须强制传入 name,这非常不合理。
解决方案:分组校验(Validation Groups)。
11.3.2. 源码解析:AddGroup, EditGroup 标记接口
RVP 已经在 common-core 中为我们定义好了最常用的分组。
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/
1 | // AddGroup.java |
它们是 空 的 标记接口。它们唯一的价值就是充当一个“标签”或“分组 Key”,让我们可以给注解“分类”。
11.3.3. DTO 改造:@NotBlank(groups = AddGroup.class)
我们重构 Student DTO,使用 groups 属性来为校验规则“打标签”:
1 | // ... 位于 ValidateController 内部 ... |
11.3.4. 激活分组
DTO 改造完成后,我们在 Controller 中通过 @Validated(...) 注解来 指定 本次调用激活哪个“分组”。
1 | // ... 位于 ValidateController ... |
结论:分组校验(Groups)是 RVP “二开”中实现 DTO 复用 和 场景化校验 的 标准解决方案,但并不是我们最优雅的方案
11.4. 【二开核心】ValidatorUtils 工具类
@Validated 注解非常方便,但它的作用范围仅限于“方法入口”。如果我们想在方法的“内部”进行编程方式的校验,@Validated 就无能为力了。
ValidatorUtils 就是 RVP 提供的 手动校验 工具。
11.4.1. 源码解析:ValidatorUtils.validate(object, groups...)
我们打开 ValidatorUtils 的源码:
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/ValidatorUtils.java
这个工具类非常简洁,其核心逻辑如下:
1 |
|
分析:
ValidatorUtils通过SpringUtils.getBean(Validator.class),静态持有 了我们在 11.1 节中配置的那个“支持 i18n”和“快速失败”的Validator实例。validate方法允许我们传入一个 对象(obj)和一个或多个分组(groups...)。- 它手动调用
VALIDATOR.validate()。 - 如果校验失败(
violations不为空),它会 立即抛出ConstraintViolationException,这个异常会被GlobalExceptionHandler捕获,并以 400 错误返回给前端。
11.4.2. 为何需要手动校验?
@Validated 注解只能在 Controller 方法上自动触发,但在 Service 层是无效的。
【二开痛点】:假设我们有一个复杂的业务逻辑,Controller 接收了一个 Student DTO,但这个 DTO 只是部分数据。我们还需要在 Service 层调用 otherService 填充 另外几个字段,然后才 完整地 得到了一个需要入库的对象。此时,我们希望在 调用 mapper.insert() 之前,对这个“完整对象”进行一次 最终校验。
1 | // 在 MyServiceImpl.java 中 |
ValidatorUtils.validate() 提供了在任何业务层(Service、Component…)内,手动触发特定分组校验的能力。
11.4.3. 实战:ValidatorUtils.validate(student, AddGroup.class, ...)
我们来模拟这个“手动调用”的场景。我们在 ValidateController 中添加一个 valid7 接口。
关键:这次,我们在 Controller 方法参数的 @RequestBody 前 不加 @Validated,模拟数据直接“裸奔”进入方法体,然后我们 手动 调用 ValidatorUtils。
1 | // ... 位于 ValidateController.java ... |
实战验证(使用 ApiFox/Postman):
(提醒:我们在 11.3.3 中定义的 Student DTO,AddGroup 组要求 name, age, email 必填)
- 重启
DromaraApplication。 - 测试
/valid7(手动校验AddGroup):- URL:
POST http://localhost:8080/validate/valid7 - Body:
{"email": "test@qq.com"}(不传name和age) - 返回 (HTTP 400):
{"code": 400, "msg": "姓名不能为空"} - 分析:请求成功进入了
valid7方法体 (log.info("--- 1. ...")会打印)。但ValidatorUtils.validate(student, AddGroup.class)捕获了name为空,抛出异常,程序中断。
- URL:
- 测试
/valid7(绕过AddGroup):- Body:
{"id": 1}(不传name,age,email) - 分析:
id字段只在EditGroup中校验。我们 只 校验了AddGroup,因此id上的@NotNull(groups = EditGroup.class)未被激活。 - 返回 (HTTP 400):
{"code": 400, "msg": "姓名不能为空"}(依然是AddGroup的name校验失败)
- Body:
- 测试
/valid7(通过AddGroup):- Body:
{"name": "张三", "age": 20, "email": "test@qq.com"} - 返回 (HTTP 200):
{"code": 200, "msg": "操作成功"} - 分析:
ValidatorUtils.validate校验通过,程序继续执行,返回成功。
- Body:
11.5. 【RVP 增强】自定义注解(一):@EnumPattern
11.5.1. 【二开痛点】如何校验 gender 字段必须是 0 或 1?
标准注解有局限性。假设 DTO 中有一个 String gender 字段,我们只接受 “0”(男)或 “1”(女)。
@NotBlank?不行,它会放行 “2”、“3” 或 “abc”。@Pattern(regexp = "[01]")?可行,但可读性差,且如果值是 “male”, “female” 呢?@Min(0) @Max(1)?这只对Integer类型有效,对String无效。
更复杂的场景:String userType 必须是 UserTypeEnum 枚举中定义的 code 值(例如 “00” “01” “02”)。
RVP 框架提供了 @EnumPattern 注解来完美解决“值必须在指定枚举的某个字段中”这一类校验需求。
“二开”使用示例:假设我们有一个 UserTypeEnum:
1 | public enum UserTypeEnum { |
我们可以在 Student DTO 中这样使用:
1 |
|
11.5.2. 源码解析 @EnumPattern
@EnumPattern 是一个元注解(注解的注解),它定义了校验的“契约”。
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPattern.java
1 |
|
分析:@EnumPattern 的核心是 @Constraint(validatedBy = {EnumPatternValidator.class})。它告诉 hibernate-validator:当看到 @EnumPattern 时,请使用 EnumPatternValidator.class 这个类去执行真正的校验逻辑。
11.5.3. 源码解析 EnumPatternValidator
EnumPatternValidator 负责实现“遍历枚举”的逻辑。
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/enumd/EnumPatternValidator.java
1 | public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> { |
分析:@EnumPattern 是一个绝佳的实战案例,它 完美地结合了我们之前所学的知识:
EnumPatternValidator实现了ConstraintValidator接口,这是hibernate-validator的标准扩展方式。isValid方法 是核心逻辑所在。ReflectUtils.invokeGetter(e, fieldName):这是关键!它利用了我们在 第八章 学习的ReflectUtils,实现了 动态 调用枚举实例的getter方法(如getCode()或getValue())。
11.6. 【RVP 增强】自定义注解(二):@DictPattern
@EnumPattern 解决了硬编码(Enum)的校验。但 RVP 中更多的数据(如“用户状态”、“通知类型”)是存在 数据库字典表(sys_dict_data)中的。
11.6.1. 【二开痛点】如何校验 userStatus 必须是 sys_user_status 字典中的有效值?
当“新增用户”时,前端传入 status: "0"。我们如何校验 "0" 确实是 sys_user_status 字典中定义的有效值(“0”=“正常”)?如果前端恶意传入 status: "99",我们必须拦截。
@EnumPattern 无法访问数据库。因此,RVP 提供了 @DictPattern。
“二开”使用示例:
1 | // 在 DTO 中使用 |
11.6.2. 源码解析 @DictPattern
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/dicts/DictPattern.java
1 | // 【核心】指定实现类 |
11.6.3. 源码解析 DictPatternValidator
DictPatternValidator 的实现比 EnumPatternValidator 更进一步:它 需要访问 Spring 容器和数据库。
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/validate/dicts/DictPatternValidator.java
1 | public class DictPatternValidator implements ConstraintValidator<DictPattern, String> { |
分析:@DictPattern 是另一个综合了前面知识的完美案例:
ConstraintValidator:实现了标准校验扩展。SpringUtils.getBean(DictService.class):这是关键!我们知道,ConstraintValidator(校验器)是由hibernate-validator管理的,它不一定被 Spring 完全接管,因此@Autowired注入DictService可能会失败- RVP 使用了我们在 第四章 学习的
SpringUtils(上下文“后门”),100% 可靠地 从 Spring 容器中“抓取”DictService实例。
- RVP 使用了我们在 第四章 学习的
DictService:它封装了对sys_dict_data的查询(并内置了 Redis 缓存)。getDictLabel返回非空,即证明value是一个合法的字典值。
11.7. 本章总结
在本章中,我们完整地剖析了 RVP common-core 模块中的 三层校验体系。作为“二开”开发者,我们必须熟练掌握这三层武器:
第一层:标准注解 (JSR 303)
- 内容:
@NotBlank,@NotNull,@Size,@Min,@Max,@Pattern,@Email。 - 触发:通过在 Controller 方法上使用
@Validated自动触发。 - 配置:RVP
ValidatorConfig默认开启了**快速失败(fail_fast)和 国际化(i18n)**支持。
- 内容:
第二层:分组校验 (Groups)
- 内容:
AddGroup.class,EditGroup.class,QueryGroup.class(空标记接口)。 - 痛点:解决 DTO 在“新增”和“修改”等不同场景下,校验规则不一致的问题。
- 触发:
@Validated(AddGroup.class)和@NotBlank(groups = AddGroup.class)。
- 内容:
第三层:自定义注解 (RVP 增强)
- 内容:
@EnumPattern和@DictPattern。 - 痛点:解决标准注解无法覆盖的 动态业务逻辑 校验。
- 原理:
@EnumPattern->EnumPatternValidator->ReflectUtils.invokeGetter(反射校验)@DictPattern->DictPatternValidator->SpringUtils.getBean->DictService.getDictLabel(数据库/缓存校验)
- 内容:









