当前位置: 首页 > news >正文

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:作为服务提供方,确保自己返回给调用方的数据符合契约

http://www.jsqmd.com/news/1222601/

相关文章:

  • 2026年 苏州手表回收推荐榜:奢侈品名表诚信回收,二手手表高价变现专业渠道解析 - 甄选服务推荐
  • EllipticCurveKeyPair完全解析:从密钥生成到安全存储的完整实践
  • Windows字体自定义终极指南:noMeiryoUI让系统界面焕然一新
  • 为什么Nitro和Nuxt都在用unenv?探索Node.js兼容性层的终极解决方案
  • SegmenTron与PyTorch生态集成:模型导出与部署最佳实践
  • 2026年7月最新泰格豪雅烟台芝罘吾悦广场维修保养服务电话 - 亨得利钟表维修中心
  • 赢标核心逻辑:如何打造一份既有竞争力又控风险的投标方案
  • 3个颠覆性技巧:如何将在线课程变成你的个人知识库
  • 济南亨得利钟表店怎么样?售后维修保养服务专业吗权威公示(2026年7月最新) - 亨得利官方
  • 光学微电子净化工程、微电子洁净室解决方案 - 华川洁净
  • 深圳院子火锅怎么选?避坑指南+选购标准+品牌推荐,新手不踩雷 - 品牌2026推荐
  • OmenSuperHub:解锁惠普暗影精灵笔记本全部性能的终极控制工具
  • 帕累托分布 密度函数 和 分布函数
  • 2026年上海全铝家居行业消费指南暨头部源头工厂权威评测报告指南
  • 欧米茄官方售后服务中心电话和网点地址实地考察报告多信源验证(2026年7月最新) - 欧米茄服务中心
  • 【无人机控制】十字型四旋翼无人机的非线性六自由度动力学模型附simulink仿真和Matlab代码
  • Flask-PyMongo源码解析:理解包装器模式与扩展机制
  • 厦门思明区厦港街道亨得利官方钟表服务中心电话公示(2026年7月最新) - 亨得利官方
  • LangGraph 和 LangChain 有什么区别?
  • GPU内存检测实战秘籍:用memtest_vulkan快速揪出显卡隐患
  • 2026香港EMBA深度测评:最好的香港EMBA怎么选?民企老板避坑榜单 - 品牌2026推荐
  • 2026 相城防水补漏哪家好?全城正规漏水维修商家综合排名 TOP3 - 苏易房屋修缮
  • 2026年7月最新劳力士无锡宝龙城市广场维修保养服务电话 - 劳力士官方服务中心
  • 欧米茄中古表值得买吗保养维修成本分析权威公示(2026年7月最新) - 欧米茄服务中心
  • 2026 年现阶段,柞水热门的餐厅水泥塑石假山生产商怎么联系,揭秘:餐厅水泥塑石假山如何提升空间格调 - 企业官方推荐【认证】
  • DeepSeek估值740亿美元启动IPO:一家杭州公司如何让硅谷VC集体反思
  • 期货交易中的Level-2数据处理实战:从CTP原生接口到订单薄重建
  • 2026 吴中工程防水排名 TOP3,厂房、车库、小区公共区域漏水维修哪家好 - 苏易房屋修缮
  • 3大算法合一:Python光学仿真神器如何破解周期性结构电磁分析难题?
  • Windows NTQQ数据库解密:一键获取聊天记录密钥的完整实战指南