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

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.class2. 支持类级别声明 方法级别校验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✅ 支持需配合ValidController 参数校验✅ 支持✅ 支持更推荐因为支持分组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返回值上加ValidService 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作为服务提供方确保自己返回给调用方的数据符合契约