Spring11-校验框架:@Valid VS @Validated

📅 2026/7/19 23:15:18 👁️ 阅读次数 📝 编程学习
Spring11-校验框架:@Valid VS @Validated

一、@Valid VS @Validated

第一步:没有任何校验框架的时候

假设你写了一个用户注册的接口:

@RestController public class UserController { @PostMapping("/register") public String register(@RequestBody UserDTO user) { // 手动校验,一堆 if-else if (user.getUsername() == null || user.getUsername().isEmpty()) { return "用户名不能为空"; } if (user.getUsername().length() < 3 || user.getUsername().length() > 20) { return "用户名长度必须在3-20之间"; } if (user.getPassword() == null || user.getPassword().length() < 6) { return "密码不能少于6位"; } if (user.getEmail() == null || !user.getEmail().matches("...正则...")) { return "邮箱格式不正确"; } if (user.getAge() == null || user.getAge() < 0 || user.getAge() > 150) { return "年龄不合法"; } // 终于能写业务代码了 userService.register(user); return "注册成功"; } }

这有什么问题?

问题说明
校验逻辑与业务逻辑混杂一个方法里 80% 的代码是校验,真正业务只有最后一行
重复劳动每个接口都要重复写类似的if (xxx == null)
错误信息不统一有的返回字符串,有的抛异常,前端对接痛苦
复用性差另一个接口也需要校验 UserDTO,你得把这段 if-else 复制过去
维护困难规则变了(比如用户名最大长度从 20 改成 30),要全局搜索替换

第二步:JSR-303/380 标准注解(@NotNull 等)

Java 社区意识到这个问题,制定了Bean Validation规范(JSR-303,后来升级到 JSR-380),提供了一组注解:

public class UserDTO { @NotBlank(message = "用户名不能为空") @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, message = "密码不能少于6位") private String password; @Email(message = "邮箱格式不正确") private String email; @Min(value = 0, message = "年龄不能小于0") @Max(value = 150, message = "年龄不能大于150") private Integer age; // getter / setter }

这样校验规则从业务代码里抽离出来,集中到了 DTO 上。

但是,这些注解只是"标记",它们不会自动生效。

你必须告诉框架:"请帮我检查这个对象上的注解,并执行校验。"


第三步:@Valid—— 标准的触发器

JSR-380 提供了@Valid注解,用来触发校验

@PostMapping("/register") public String register(@Valid @RequestBody UserDTO user) { // 如果校验失败,Spring 会自动抛 MethodArgumentNotValidException userService.register(user); return "注册成功"; }

@Valid放在方法参数上,Spring 在处理请求时,会自动调用 Validator 去检查UserDTO里的注解。


@Valid有两个明显的局限:

局限 1:不支持"分组校验"

实际开发中,同一个 DTO 在不同场景下规则不同。

比如UserDTO

  • 注册时:用户名、密码、邮箱都要校验

  • 登录时:只需要校验用户名和密码,不需要校验邮箱

  • 更新时:id 不能为空,但密码可以不填

@Valid你没法区分场景,它会把所有注解全部校验一遍。


局限 2:只能用在特定位置

@Valid标准定义里,主要用于:

  • 方法参数(Spring MVC 支持)

  • 方法返回值

  • 嵌套对象(DTO 里的字段是另一个对象,需要递归校验)

但它不能直接在类级别声明,也不支持对普通 Service 层方法的参数进行校验(需要额外配置)。


第四步:@Validated—— Spring 的增强版

Spring 发现@Valid不够用,于是提供了自己的注解@Validatedorg.springframework.validation.annotation.Validated)。

它解决了@Valid的核心痛点:

1. 支持分组校验

你先定义分组接口(空接口即可,只是标记):

public interface ValidGroup { interface Register {} interface Login {} interface Update {} }

然后在 DTO 的注解上指定分组:

public class UserDTO { @NotBlank(message = "用户名不能为空", groups = {ValidGroup.Register.class, ValidGroup.Login.class}) @Size(min = 3, max = 20, message = "用户名长度必须在3-20之间", groups = ValidGroup.Register.class) private String username; @NotBlank(message = "密码不能为空", groups = {ValidGroup.Register.class, ValidGroup.Login.class}) @Size(min = 6, message = "密码不能少于6位", groups = ValidGroup.Register.class) private String password; @Email(message = "邮箱格式不正确", groups = ValidGroup.Register.class) private String email; @NotNull(message = "id不能为空", groups = ValidGroup.Update.class) private Long id; // getter / setter }

Controller 里用@Validated指定分组:

@PostMapping("/register") public String register(@Validated(ValidGroup.Register.class) @RequestBody UserDTO user) { userService.register(user); return "注册成功"; } @PostMapping("/login") public String login(@Validated(ValidGroup.Login.class) @RequestBody UserDTO user) { return userService.login(user); }

逻辑:

  • 注册时,只校验标记了Register分组的字段

  • 登录时,只校验标记了Login分组的字段

  • 没有指定groups的字段,默认不会被校验(除非用Default.class


2. 支持类级别声明 + 方法级别校验

@Validated可以加在类上,让整个类的所有方法参数都参与校验:

@Service @Validated // 加在类上 public class UserService { // 这个方法的两个参数都会被校验 public String updateUser( @NotNull(message = "id不能为空") Long id, @NotBlank(message = "用户名不能为空") String username) { // 业务逻辑 return "更新成功"; } }

注意:这种用法底层是 Spring AOP 代理实现的,需要配合MethodValidationPostProcessor这个 Bean。Spring Boot 自动配置已经帮你注册好了,你不需要手动写。


第五步:@Valid@Validated对比

特性@Valid(JSR-380)@Validated(Spring)
来源标准规范 (javax/jakarta.validation)Spring 框架 (org.springframework.validation)
分组校验❌ 不支持✅ 支持
类级别声明❌ 不支持✅ 支持(触发方法参数校验)
嵌套对象校验✅ 支持(字段上加@Valid✅ 支持(需配合@Valid
Controller 参数校验✅ 支持✅ 支持(更推荐,因为支持分组)
Service 方法参数校验❌ 不直接支持✅ 支持(类上加@Validated

第六步:一个完整的代码示例

// ========== 分组定义 ========== public interface ValidGroup { interface Register {} interface Login {} } // ========== DTO ========== @Data public class UserDTO { @NotBlank(groups = {ValidGroup.Register.class, ValidGroup.Login.class}) @Size(min = 3, max = 20, groups = ValidGroup.Register.class) private String username; @NotBlank(groups = {ValidGroup.Register.class, ValidGroup.Login.class}) @Size(min = 6, groups = ValidGroup.Register.class) private String password; @Email(groups = ValidGroup.Register.class) private String email; } // ========== Controller ========== @RestController public class UserController { @Autowired private UserService userService; @PostMapping("/register") public String register(@Validated(ValidGroup.Register.class) @RequestBody UserDTO user) { userService.register(user); return "注册成功"; } @PostMapping("/login") public String login(@Validated(ValidGroup.Login.class) @RequestBody UserDTO user) { return userService.login(user); } } // ========== 全局异常处理(统一返回错误信息) ========== @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public Result handleValidation(MethodArgumentNotValidException e) { String message = e.getBindingResult().getFieldErrors().stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.joining(", ")); return Result.fail(message); } @ExceptionHandler(ConstraintViolationException.class) public Result handleConstraintViolation(ConstraintViolationException e) { // 处理 @Validated 在 Service 层抛出的异常 String message = e.getConstraintViolations().stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining(", ")); return Result.fail(message); } }

逻辑链条总结

手动 if-else 校验 → 代码臃肿、难维护 ↓ Bean Validation 规范定义了 @NotNull 等注解,把校验规则抽离到 DTO ↓ 但注解只是标记,需要触发器才能执行 ↓ @Valid 能触发校验,但不支持分组,也无法在类级别统一启用 ↓ Spring 提供 @Validated,支持分组校验和类级别方法参数校验 ↓ 最终:校验逻辑彻底从业务代码中剥离,按场景精确控制,统一异常处理

一句话记住

@Valid是标准规范的"触发器",能干活但功能有限;@Validated是 Spring 的增强版,核心增加了分组校验类级别方法校验,让你能在不同场景下复用同一个 DTO,且把校验从 Controller 下沉到 Service 层。


二、@Validated防止的位置说明

@Validated可以放在的位置

1.Controller 方法参数上最常用

@PostMapping("/register") public String register( @Validated(ValidGroup.Register.class) @RequestBody UserDTO user ) { // ... }

效果:校验 HTTP 请求体(@RequestBody)或表单参数。校验失败抛MethodArgumentNotValidException


2.类上(Service / Controller / Component 等)

@Service @Validated public class UserService { public String updateUser( @NotNull Long id, @NotBlank String name) { // ... } }

效果:这个类里所有方法的参数都会被校验。底层靠Spring AOP 代理实现。

注意:

  • 校验失败抛ConstraintViolationException

  • 类内部方法互相调用时,不走代理,校验不生效(这是 AOP 的固有特性)

@Service @Validated public class UserService { public void methodA() { methodB(null); // ❌ 这里调用 methodB,校验不会触发! } public void methodB(@NotNull String name) { // ... } }

3.接口上

@Validated public interface UserService { String updateUser(@NotNull Long id, @NotBlank String name); } @Service public class UserServiceImpl implements UserService { @Override public String updateUser(Long id, String name) { // ... } }

效果:所有实现类的方法参数都会触发校验。

适合契约式编程,把校验规则定义在接口层面。


4.不能放的位置(常见误区)

位置是否支持说明
字段上字段校验用@NotNull等 JSR-380 注解,@Validated本身不是约束注解
普通 POJO 类上❌ 无意义加在普通类上不会自动触发校验,必须配合 Spring 容器管理
方法返回值上⚠️ 语法支持但极少用@Validated设计目的是校验输入返回值校验用@Valid更标准

一句话总结

@Validated放在类/接口上,控制整个类的方法参数校验;放在方法参数上,控制单次调用的入参校验。它不能放在字段上——字段的校验规则用@NotNull@Size等约束注解,@Validated是触发器,不是规则本身。


三、@Valid 检验方法返回值

返回值校验的核心逻辑是:

上放@Validated启用方法级校验,方法返回值上放@Valid标记"这个返回值需要被校验"。


完整示例

1. DTO(带约束规则)

@Data public class UserDTO { @NotBlank(message = "用户名不能为空") private String username; @Email(message = "邮箱格式不正确") private String email; @Min(value = 0, message = "年龄不能小于0") @Max(value = 150, message = "年龄不能大于150") private Integer age; }

2. Service(类上加@Validated,返回值上加@Valid

@Service @Validated // 必须加!否则方法级别的校验(包括返回值)不会生效 public class UserService { /** * 返回值会被校验: * 如果返回的 UserDTO 违反了任何约束注解,会抛 ConstraintViolationException */ @Valid public UserDTO getUserById(Long id) { // 模拟从数据库查询,结果数据不完整 UserDTO user = new UserDTO(); user.setUsername(null); // 违反 @NotBlank user.setEmail("not-an-email"); // 违反 @Email user.setAge(-5); // 违反 @Min(0) return user; } }

3. Controller 调用

@RestController public class UserController { @Autowired private UserService userService; @GetMapping("/user/{id}") public UserDTO getUser(@PathVariable Long id) { return userService.getUserById(id); // 这里会触发返回值校验 } }

4. 全局异常处理(捕获返回值校验失败)

@RestControllerAdvice public class GlobalExceptionHandler { /** * 处理 @Validated 在类上时,方法参数/返回值校验失败的异常 */ @ExceptionHandler(ConstraintViolationException.class) public Result handleConstraintViolation(ConstraintViolationException e) { String message = e.getConstraintViolations().stream() .map(violation -> violation.getPropertyPath() + ": " + violation.getMessage()) .collect(Collectors.joining(", ")); // 输出示例:getUserById.<return value>: 用户名不能为空, getUserById.<return value>: 邮箱格式不正确... return Result.fail(message); } }

关键点

要点说明
@Validated必须在类/接口上没有它,Spring 不会为这个方法创建校验代理
@Valid放在方法上标记返回值需要被递归校验(包括嵌套对象)
异常类型ConstraintViolationException,不是MethodArgumentNotValidException
错误信息里的路径会包含<return value>,告诉你这是返回值校验失败

为什么返回值也要校验?

通常用于:

  • 防御性编程:下游接口返回的数据你不信任,确保流出你这一层的数据是干净的

  • 数据转换后校验:比如从数据库查出来的原始数据经过组装后,确认组装逻辑没产生脏数据

  • 对外暴露的 API:作为服务提供方,确保自己返回给调用方的数据符合契约