Spring Validation校验框架原理
·
Spring Validation 校验框架原理深度解析
目录
- 引言
- 核心接口体系
- 约束注解解析与元数据
- 约束验证器执行机制
- 级联校验与对象图遍历
- 分组校验与组序列
- 消息国际化与模板机制
- @Validated 与 @Valid 深度对比
- Spring Boot 自动配置原理
- 控制器参数校验源码流程
- 自定义校验器高级用法
- 实战最佳实践
- 总结
1. 引言
Spring Validation 基于 JSR-380(Bean Validation 2.0)规范,构建了一套完整的声明式校验体系。但大多数开发者只停留在 @NotNull、@NotBlank 的表层使用,对框架内部的执行机制知之甚少。
本文将深入剖析 Validation 框架从注解解析到约束执行的完整链路,揭示那些隐藏在 ConstraintValidator 背后的设计哲学。
2. 核心接口体系
2.1 接口继承关系图
2.2 核心接口职责
3. 约束注解解析与元数据
3.1 约束注解结构
每个校验注解必须包含以下四个属性:
3.2 常用校验注解分类
4. 约束验证器执行机制
4.1 ConstraintValidator 生命周期
4.2 内置约束验证器继承层次
4.3 组合约束与合成约束
组合约束示例:
@NotBlank(message = "邮箱不能为空")
@Pattern(regexp = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
message = "邮箱格式不正确")
@ReportAsSingleViolation // 多个失败只报告一个
@Constraint(validatedBy = {})
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface CompositeEmail {
String message() default "邮箱格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
5. 级联校验与对象图遍历
5.1 级联校验执行流程
5.2 对象图遍历策略
5.3 @Valid vs @Cascade
6. 分组校验与组序列
6.1 分组校验执行流程
6.2 组序列与强制顺序
6.3 组序列定义
// 定义组序列
@GroupSequence({BasicInfo.class, ContactInfo.class, CompleteValidation.class})
public interface UserValidationSequence {
}
// 使用组序列
@Validated(UserValidationSequence.class)
public class User {}
// 分组接口定义
public interface BasicInfo {
// 基本信息组
}
public interface ContactInfo {
// 联系信息组
}
public class User {
@NotNull(groups = BasicInfo.class)
private Long id;
@NotBlank(groups = {BasicInfo.class, ContactInfo.class})
private String username;
@Email(groups = ContactInfo.class)
private String email;
@Pattern(groups = CompleteValidation.class, regexp = "^1[3-9]\\d{9}$")
private String phone;
}
7. 消息国际化与模板机制
7.1 消息插值流程
7.2 消息模板语法
7.3 自定义消息资源
# ValidationMessages.properties
javax.validation.constraints.NotNull.message=不能为 null
javax.validation.constraints.NotBlank.message=不能为空
javax.validation.constraints.NotEmpty.message=集合不能为空
javax.validation.constraints.Size.message=长度必须在 {min} 到 {max} 之间
javax.validation.constraints.Email.message=邮箱格式不正确
javax.validation.constraints.Pattern.message=格式不匹配: {regexp}
javax.validation.constraints.Min.message=值不能小于 {value}
javax.validation.constraints.Max.message=值不能大于 {value}
javax.validation.constraints.DecimalMin.message=值不能小于 {value}
javax.validation.constraints.DecimalMax.message=值不能大于 {value}
javax.validation.constraints.AssertTrue.message=必须为 true
javax.validation.constraints.AssertFalse.message=必须为 false
javax.validation.constraints.Past.message=必须是过去的时间
javax.validation.constraints.Future.message=必须是未来的时间
# 自定义消息(包级别)
com.example.constraints.Phone.message=手机号格式不正确
com.example.constraints.IdCard.message=身份证号格式不正确
8. @Validated 与 @Valid 深度对比
8.1 功能对比图
8.2 使用场景决策树
9. Spring Boot 自动配置原理
9.1 自动配置类加载链路
9.2 Validator 初始化流程
9.3 依赖关系
10. 控制器参数校验源码流程
10.1 请求处理完整链路
10.2 校验触发的关键类
10.3 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return Result.error(400, "参数校验失败", errors);
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<?> handleConstraintViolation(ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String field = violation.getPropertyPath().toString();
errors.put(field, violation.getMessage());
});
return Result.error(400, "参数校验失败", errors);
}
@ExceptionHandler(MethodValidationException.class)
public Result<?> handleMethodValidation(MethodValidationException ex) {
// @Validated 方法参数校验异常
return Result.error(400, "方法参数校验失败", ex.getViolations());
}
}
11. 自定义校验器高级用法
11.1 依赖注入的校验器
@Component
public class UserServiceAwareValidator implements ConstraintValidator<UniqueUsername, String>,
ApplicationContextAware {
private ApplicationContext context;
private UserMapper userMapper;
@Override
public void setApplicationContext(ApplicationContext context) {
this.context = context;
}
@Override
public void initialize(UniqueUsername constraintAnnotation) {
this.userMapper = context.getBean(UserMapper.class);
}
@Override
public boolean isValid(String username, ConstraintValidatorContext context) {
if (username == null || username.isBlank()) {
return true; // @NotBlank 处理
}
return !userMapper.existsByUsername(username);
}
}
11.2 多字段联合校验
@Constraint(validatedBy = FieldMatchValidator.class)
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
public @interface FieldMatch {
String message() default "字段不匹配";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String first();
String second();
Class<? extends Comparator> matchType() default EqualMatch.class;
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
FieldMatch[] value();
}
}
// 使用示例
@FieldMatch(first = "password", second = "confirmPassword",
message = "两次密码输入不一致")
public class PasswordResetRequest {
private String password;
private String confirmPassword;
}
11.3 动态分组校验
public class DynamicGroupValidator implements ConstraintValidator<DynamicGroup, Object> {
@Override
public void initialize(DynamicGroup constraintAnnotation) {}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) return true;
// 动态决定分组
Class<?>[] dynamicGroups = determineGroups(value);
// 手动触发分组校验
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Object>> violations = validator.validate(value, dynamicGroups);
// 如果有违规,复制到当前上下文
if (!violations.isEmpty()) {
context.disableDefaultConstraintViolation();
violations.forEach(v -> {
context.buildConstraintViolationWithTemplate(v.getMessage())
.addConstraintViolation();
});
return false;
}
return true;
}
private Class<?>[] determineGroups(Object value) {
if (value instanceof User user) {
return user.isAdmin() ?
new Class[]{AdminGroup.class, UserGroup.class} :
new Class[]{UserGroup.class};
}
return new Class[]{Default.class};
}
}
12. 实战最佳实践
12.1 分层校验策略
12.2 性能优化策略
12.3 统一响应封装
@Data
public class ValidationResult {
private boolean valid;
private Map<String, String> errors;
public static ValidationResult from(Set<ConstraintViolation<?>> violations) {
ValidationResult result = new ValidationResult();
result.setValid(violations.isEmpty());
result.setErrors(violations.stream()
.collect(Collectors.toMap(
v -> v.getPropertyPath().toString(),
ConstraintViolation::getMessage,
(e1, e2) -> e1 + "; " + e2
)));
return result;
}
}
// 全局异常处理
@RestControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler({MethodArgumentNotValidException.class,
BindException.class})
public Result<ValidationResult> handleValidationException(Exception ex) {
BindingResult bindingResult = ex instanceof MethodArgumentNotValidException ?
((MethodArgumentNotValidException) ex).getBindingResult() :
((BindException) ex).getBindingResult();
Set<ConstraintViolation<Object>> violations =
bindingResult.getAllErrors().stream()
.map(error -> (ConstraintViolation<Object>) error)
.collect(Collectors.toSet());
return Result.error(400, "参数校验失败",
ValidationResult.from(violations));
}
}
13. 总结
13.1 核心组件关系图
13.2 关键知识点
| 概念 | 说明 |
|---|---|
| ConstraintValidator | 校验逻辑执行器,包含初始化和校验两个阶段 |
| ConstraintViolation | 保存校验失败信息,包含消息、路径、属性值 |
| ConstraintDescriptor | 约束的元数据描述器 |
| 级联校验 | 通过 @Valid 触发嵌套对象的递归校验 |
| 分组校验 | 通过 groups 属性实现不同场景的差异化校验 |
| 组序列 | 通过 @GroupSequence 定义校验顺序,支持短路 |
| 消息插值 | 支持占位符替换和国际化消息解析 |
13.3 最佳实践总结
- 分层校验:Controller 做基本校验,Service 做业务校验,DAO 做唯一性校验
- 分组校验:使用 groups 区分创建、更新、删除等不同场景
- 组序列:使用 @GroupSequence 实现短路校验,避免无效校验
- 国际化:统一使用消息模板,通过 ValidationMessages.properties 管理
- 性能:避免在校验器中进行数据库查询,使用预校验机制
- 异常处理:统一封装 ValidationException,返回结构化的错误信息
更多推荐




所有评论(0)