在 Java 开发中,数据校验是我们绕不开的话题。每天接触无数请求参数,你是否遇到过因缺少校验而导致的线上故障?深夜被叫醒排查问题的经历,让我明白了参数校验的重要性。Spring 框架提供的@Validated 和@Valid 两个注解,你真的用对了吗?
一、两个注解的基本介绍
1.1 @Valid 注解
@Valid
注解来源于 JSR-303 规范(Bean Validation),位于javax.validation.Valid
包下,是 Java 标准的一部分。它主要用于验证 Bean 对象的属性是否符合约束条件。
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@PostMapping("/users")
public String createUser(@Valid @RequestBody User user) {
// 业务逻辑
return "用户创建成功";
}
}
class User {
@NotNull(message = "用户名不能为空")
private String username;
// 无参构造函数
public User() {}
// getter和setter
}
1.2 @Validated 注解
@Validated
注解是 Spring Framework 提供的,位于org.springframework.validation.annotation
包下,是 Spring 对 JSR-303 规范的扩展,提供了分组验证、类级验证等增强功能。
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Validated // 类级别的验证
public class ProductController {
@PostMapping("/products")
public String createProduct(@Validated @RequestBody Product product) {
// 业务逻辑
return "产品创建成功";
}
}
class Product {
@NotNull(message = "产品名称不能为空")
private String name;
// 无参构造函数
public Product() {}
// getter和setter
}
二、两者的核心区别
2.1 规范来源不同
- @Valid: JSR-303(Bean Validation)规范的产物,是 Java EE 标准的一部分
- @Validated: Spring 自家的产物,对 JSR-303 的扩展实现
2.2 分组验证能力
@Valid 属于 JSR-303 规范,其关联的约束注解支持 groups 参数,但 JSR-303 原生未提供类级分组验证的声明方式。而@Validated 作为 Spring 扩展,允许在类或方法上统一指定分组,简化分组验证的使用。
在实际开发中,同一个对象在不同的业务场景下可能有不同的验证规则。比如,创建用户时密码必填,而更新用户信息时密码可以为空。
下面是一个使用@Validated 实现分组验证的例子:
// 定义分组接口
public interface Create {}
public interface Update {}
// 分组继承示例
public interface SuperAdmin extends Create, Update {} // 超级管理员继承创建和更新权限
// 实体类
public class User {
private Long id;
@NotBlank(groups = {Create.class, Update.class}, message = "用户名不能为空")
private String username;
@NotBlank(groups = Create.class, message = "创建时密码不能为空")
@Size(min = 6, max = 20, groups = Create.class, message = "密码长度必须在6-20之间")
private String password;
// 无参构造函数
public User() {}
// getter和setter
}
// 控制器
@RestController
public class UserController {
@PostMapping("/users")
public String createUser(@Validated(Create.class) @RequestBody User user) {
// 创建用户逻辑
return "用户创建成功";
}
@PutMapping("/users/{id}")
public String updateUser(@PathVariable Long id,
@Validated(Update.class) @RequestBody User user) {
// 更新用户逻辑
return "用户更新成功";
}
@PutMapping("/users/admin/{id}")
public String adminUpdateUser(@PathVariable Long id,
@Validated(SuperAdmin.class) @RequestBody User user) {
// SuperAdmin组会触发Create和Update组的所有验证
return "管理员更新用户成功";
}
}
2.3 使用位置的区别
@Valid:
- 可用在方法参数、字段、构造函数参数、方法返回值上
- 主要用于对象属性的验证
@Validated:
- 可用在类型、方法参数、方法上,不能用在字段上
- 可用在类上为整个类的所有方法开启验证功能
- 支持对方法参数和返回值的验证,并允许在类或方法上声明分组规则
2.4 嵌套验证的支持
无论是@Valid 还是@Validated,嵌套对象的验证必须通过在父对象的字段上添加@Valid
注解(JSR-303 规范要求)。@Validated 本身不影响嵌套验证,只是它作为 Spring 扩展,需依赖 JSR-303 的@Valid
来触发级联验证。
多层嵌套验证示例:
public class Order {
@NotNull(message = "订单号不能为空")
private String orderNo;
@Valid // 第一层嵌套验证
@NotNull(message = "用户信息不能为空")
private User user;
@Valid // 第一层嵌套验证
private List<OrderItem> items;
// 无参构造函数
public Order() {}
// getter和setter
}
public class User {
@NotBlank(message = "用户名不能为空")
private String username;
@Valid // 第二层嵌套验证
private Address address;
// 无参构造函数
public User() {}
// getter和setter
}
public class Address {
@NotBlank(message = "省份不能为空")
private String province;
@NotBlank(message = "城市不能为空")
private String city;
// 无参构造函数
public Address() {}
// getter和setter
}
public class OrderItem {
@NotBlank(message = "商品名称不能为空")
private String productName;
@Min(value = 1, message = "商品数量必须大于0")
private Integer quantity;
// 无参构造函数
public OrderItem() {}
// getter和setter
}
@RestController
public class OrderController {
@PostMapping("/orders")
public String createOrder(@Validated @RequestBody Order order) {
// 所有嵌套对象都会被验证,包括:
// 1. Order->User->Address(多层嵌套)
// 2. Order->List<OrderItem>(集合嵌套)
return "订单创建成功";
}
}
三、原理剖析
3.1 实现机制差异
@Valid 的实现原理:
- 依赖 JSR-303 的
ValidatorFactory
和Validator
接口 - 实际验证由 Hibernate Validator 等第三方实现提供
- 在 Spring MVC 中,通过
LocalValidatorFactoryBean
集成到请求处理流程
- 依赖 JSR-303 的
@Validated 的实现原理:
- 是 Spring 的
@AspectJ
切面实现 - 通过
MethodValidationInterceptor
拦截方法调用 - 继承了
@Valid
的验证能力,并添加了分组验证的支持 - 用于控制器时,行为与
@Valid
类似;用于 Service 层时,通过 AOP 拦截实现验证
- 是 Spring 的
3.2 异常处理差异
不同验证方式触发的异常类型:
- @Valid 结合 Spring MVC:参数验证失败会抛出
MethodArgumentNotValidException
- 直接使用 Validator 工具类:验证对象时,抛出
ConstraintViolationException
- @Validated 在 Service 层:方法参数验证失败抛出
ConstraintViolationException
- @Validated 在控制器参数:与
@Valid
行为一致,抛出MethodArgumentNotValidException
3.3 验证框架配置定制
Spring Boot 默认使用 Hibernate Validator 作为 JSR-303 实现。如果你使用的是普通的 Spring 项目,需要添加以下依赖:
<!-- Maven依赖 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
自定义验证器配置示例:
@Configuration
public class ValidationConfig {
@Bean
public LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
// 自定义消息源,用于国际化
validator.setValidationMessageSource(messageSource());
// 自定义参数
validator.getValidationPropertyMap().put("hibernate.validator.fail_fast", "true");
return validator;
}
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
}
四、请求处理与验证流程
下面的图展示了 Spring MVC 中请求处理和验证的流程:
五、实战案例:分组验证详解
分组验证是@Validated 的强大功能,让我们看一个完整的例子,展示如何在不同场景下应用不同的验证规则:
// 分组接口定义
public interface Insert {}
public interface Update {}
public interface Query {}
// 分组顺序定义示例
@GroupSequence({Insert.class, Default.class})
public interface OrderedInsert {} // 先验证Insert组,再验证Default组
// 实体类
public class Product {
@Null(groups = Insert.class, message = "新增产品时ID必须为空")
@NotNull(groups = {Update.class, Query.class}, message = "ID不能为空")
private Long id;
@NotBlank(groups = {Insert.class, Update.class}, message = "产品名称不能为空")
private String name;
@NotNull(groups = {Insert.class, Update.class}, message = "价格不能为空")
@DecimalMin(value = "0.01", groups = {Insert.class, Update.class}, message = "价格必须大于0")
private BigDecimal price;
@NotBlank(groups = Insert.class, message = "新增时必须指定分类")
private String category;
// 无参构造函数
public Product() {}
// getter和setter
}
// 控制器
@RestController
@RequestMapping("/products")
public class ProductController {
@PostMapping
public String createProduct(@Validated(Insert.class) @RequestBody Product product) {
// 创建产品逻辑
return "产品创建成功";
}
@PostMapping("/ordered")
public String createProductWithOrder(@Validated(OrderedInsert.class) @RequestBody Product product) {
// 按顺序验证:先Insert组再Default组
return "产品创建成功";
}
@PutMapping("/{id}")
public String updateProduct(@PathVariable Long id,
@Validated(Update.class) @RequestBody Product product) {
if (!id.equals(product.getId())) {
throw new IllegalArgumentException("路径ID与请求体ID不匹配");
}
// 更新产品逻辑
return "产品更新成功";
}
@GetMapping("/{id}")
public Product getProduct(@PathVariable @Validated(Query.class) Long id) {
// 路径参数验证需显式使用@Validated,@Valid对路径参数支持较弱
// 查询产品逻辑
return new Product(); // 示例返回
}
}
分组验证的流程如下:
六、实战案例:嵌套验证和方法级验证
6.1 嵌套验证
public class Department {
@NotBlank(message = "部门名称不能为空")
private String name;
@Valid // 注意这里必须使用@Valid,与外层是否使用@Validated无关
@NotEmpty(message = "部门必须至少有一名员工")
private List<Employee> employees;
// 无参构造函数
public Department() {}
// getter和setter
}
public class Employee {
@NotBlank(message = "员工姓名不能为空")
private String name;
@NotNull(message = "员工年龄不能为空")
@Min(value = 18, message = "员工年龄必须大于等于18岁")
private Integer age;
// 无参构造函数
public Employee() {}
// getter和setter
}
@RestController
public class DepartmentController {
@PostMapping("/departments")
public String createDepartment(@Validated @RequestBody Department department) {
// 即使使用@Validated,嵌套的List<Employee>验证也会生效,因为Department类中使用了@Valid
return "部门创建成功";
}
}
6.2 方法级验证
方法级验证需要在类上添加@Validated 注解,并在配置类中启用方法验证:
// 配置类启用方法级验证
@Configuration
@EnableMethodValidation // 开启方法级验证
@EnableAspectJAutoProxy(exposeProxy = true) // 关键:暴露AOP代理对象,解决自我调用问题
public class AppConfig {}
// Service类使用验证
@Service
@Validated
public class UserService {
public User findById(@NotNull(message = "ID不能为空") Long id) {
// 查询用户逻辑
return new User();
}
public void updateUsername(
@NotNull(message = "用户ID不能为空") Long userId,
@NotBlank(message = "用户名不能为空") String username) {
// 更新用户名逻辑
}
@NotNull(message = "返回结果不能为空")
public List<User> findAll() {
// 返回值验证由Hibernate Validator等JSR-303实现支持
// Spring通过AOP拦截方法调用,检查返回值
return new ArrayList<>();
}
// 注意:以下情况AOP验证会失效
public void processUser(Long userId) {
// ⚠️ 自我调用问题:此处直接调用同类方法,AOP不会拦截
updateUsername(userId, "newName"); // 验证会被跳过!
// ✅ 正确方式:通过代理对象调用
((UserService) AopContext.currentProxy()).updateUsername(userId, "newName");
}
}
七、常见问题与解决方案
7.1 验证失败如何优雅处理?
使用全局异常处理器捕获验证异常并返回友好信息,始终使用 HTTP 400 状态码表示客户端请求问题:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
// 统一返回400状态码,表示客户端请求有误
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handleConstraintViolation(
ConstraintViolationException ex) {
Map<String, String> errors = new HashMap<>();
ex.getConstraintViolations().forEach(violation -> {
String propertyPath = violation.getPropertyPath().toString();
String message = violation.getMessage();
errors.put(propertyPath, message);
});
// 统一返回400状态码
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
7.2 分组验证与默认组
当使用@Valid
时,验证会包含所有分组(包括 Default 组)。而使用@Validated
时,需注意:
- 使用@Validated 但不指定分组:等价于
@Validated(Default.class)
,只验证 Default 组的约束 - 使用@Validated 并指定分组:仅验证指定分组及其父分组的约束,默认不包含 Default 组
public class User {
// 属于Default分组,不指定分组时会被验证
@NotBlank(message = "用户名不能为空")
private String username;
// 同时属于Default分组和Create分组
@NotBlank(message = "密码不能为空", groups = {Default.class, Create.class})
private String password;
// 只属于Update分组,不指定分组时不会被验证
@NotNull(message = "更新时ID不能为空", groups = Update.class)
private Long id;
// 无参构造函数
public User() {}
}
@RestController
@Validated // 类上标注@Validated,等价于@Validated(Default.class)
public class UserController {
// 验证所有属于Default组的约束
}
7.3 自定义校验注解
有时内置的验证注解不能满足业务需求,我们可以自定义验证注解,并通过 ConstraintValidatorContext 自定义错误信息:
// 自定义手机号码验证注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
String message() default "手机号码格式不正确";
Class<?>[] groups() default {};
// payload参数用于附加验证元数据,通常默认即可
Class<? extends Payload>[] payload() default {};
String region() default "CN"; // 支持不同国家/地区手机格式
}
// 实现验证逻辑
public class MobileValidator implements ConstraintValidator<Mobile, String> {
private String region;
private Map<String, Pattern> PATTERNS = new HashMap<>();
{
// 中国手机号规则
PATTERNS.put("CN", Pattern.compile("^1[3-9]\\d{9}$"));
// 美国手机号规则(简化)
PATTERNS.put("US", Pattern.compile("^\\d{10}$"));
}
@Override
public void initialize(Mobile annotation) {
this.region = annotation.region();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 空值校验由其他注解处理,如需非空校验,需结合@NotBlank/@NotNull使用
if (value == null || value.isEmpty()) {
return true;
}
Pattern pattern = PATTERNS.getOrDefault(region, PATTERNS.get("CN"));
boolean isValid = pattern.matcher(value).matches();
if (!isValid) {
// 禁用默认消息
context.disableDefaultConstraintViolation();
// 自定义错误消息,包含更多信息
context.buildConstraintViolationWithTemplate(
String.format("手机号[%s]格式不符合%s地区规范", value, region))
.addConstraintViolation();
}
return isValid;
}
}
// 使用自定义注解
public class User {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "手机号不能为空")
@Mobile(message = "手机号码格式不正确", region = "CN") // 指定区域验证规则
private String mobile;
// getter和setter
}
八、最佳实践
根据不同场景选择合适的验证方式:
8.1 控制器参数校验
- 简单场景:使用
@Valid
标注参数即可 - 需要分组验证:使用
@Validated
并指定分组,如@Validated(Create.class)
- 接收多个参数时:在控制器类上添加
@Validated
,并在每个参数上添加约束注解 - 路径参数验证:优先使用
@Validated
,对@PathVariable
参数的支持更好
8.2 嵌套对象验证
- 无论外层使用
@Valid
还是@Validated
,嵌套字段必须添加@Valid
注解 - 复杂嵌套(多层嵌套或集合嵌套)中,每一层的嵌套字段都需要添加
@Valid
8.3 Service 层验证
- 在 Service 类上添加
@Validated
注解 - 在方法参数上直接添加验证约束,如
@NotNull
、@Size
等 - 在配置类中启用方法验证:
@EnableMethodValidation
- 使用
@Validated(Group.class)
在方法上指定验证分组 - 避免方法内部自我调用(会导致 AOP 验证失效),必须通过
@EnableAspectJAutoProxy(exposeProxy = true)
配置并使用AopContext.currentProxy()
获取代理对象
8.4 典型应用场景对比
场景 | @Valid | @Validated |
---|---|---|
简单参数校验 | 推荐(直接标注在参数上) | 也可使用,但略冗余 |
分组验证(如增删改不同规则) | 需手动在每个参数的约束中指定分组 | 推荐(类/方法级分组声明更简洁) |
嵌套对象校验 | 必须在嵌套字段加@Valid | 同样依赖@Valid 触发嵌套 |
Service 层方法参数校验 | 不支持类级声明 | 推荐(类上@Validated +方法参数约束) |
路径参数(@PathVariable)校验 | 支持较弱 | 推荐(支持更完善) |
九、总结
下表总结了@Valid 和@Validated 的主要区别:
特性 | @Valid | @Validated |
---|---|---|
来源 | JSR-303 Bean Validation | Spring Framework |
分组验证 | 原生不支持类级分组声明 | 支持类级/方法级分组声明 |
分组顺序 | 不支持顺序控制 | 支持@GroupSequence 控制验证顺序 |
使用位置 | 方法参数、字段、构造函数参数、返回值 | 类、方法参数、方法 |
嵌套验证 | 原生支持(字段上添加@Valid) | 需结合@Valid 使用 |
方法级验证 | 支持参数和返回值验证(JSR-303) | 支持类级验证、分组验证(Spring 扩展) |
默认组处理 | 验证所有约束,包括 Default 组 | 不指定分组时仅验证 Default 组 |
路径参数校验 | 支持有限 | 支持更完善 |
类级分组声明 | 不支持 | 支持 |
适用场景 | 简单参数验证、嵌套对象验证 | 分组验证、方法级验证、复杂业务场景 |
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。