Spring Boot 参数校验未触发?@Validated 与 @Valid 注解差异剖析
各位开发者,大家好。今天我们来聊聊 Spring Boot 中参数校验相关的问题,尤其是“参数校验未触发”的情况,以及 @Validated 和 @Valid 这两个注解的区别和使用场景。参数校验是保证系统健壮性和数据完整性的重要一环。一个完善的参数校验机制能够有效防止脏数据进入系统,减少错误发生的可能性。
为什么参数校验没有生效?
首先,我们来看看导致参数校验没有生效的常见原因:
-
忘记添加依赖: Spring Boot 的校验功能依赖于
hibernate-validator,需要确保在pom.xml文件中引入了相应的依赖。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> -
注解位置错误:
@Valid或@Validated注解必须添加到需要校验的参数或对象上。 -
缺少
@RequestBody注解: 如果是校验请求体中的参数,务必确保方法参数使用了@RequestBody注解,否则校验器无法获取到请求体中的数据。 -
未开启校验: 对于 Controller 方法的参数校验,需要在 Controller 类上使用
@Validated注解。 -
分组校验使用错误: 如果使用了分组校验,需要正确指定要使用的分组。
-
嵌套对象校验未生效: 如果需要校验嵌套对象,需要在嵌套对象上添加
@Valid注解。 -
自定义校验器的问题: 如果使用了自定义校验器,需要确保校验器的逻辑正确,并且正确地绑定到相应的字段上。
-
AOP 拦截失效: 某些 AOP 配置可能会干扰 Spring Validation 的执行。 检查是否存在类似拦截器导致无法进入controller。
接下来,我们将通过代码示例,详细讲解这些原因以及对应的解决方案。
基础示例:Controller 方法参数校验
我们创建一个简单的 User 类:
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class User {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2到20之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
// Getters and setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
然后创建一个 Controller:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@Validated // 必须添加此注解才能开启 Controller 方法参数校验
public class UserController {
@PostMapping("/users")
public ResponseEntity<String> createUser(@Valid @RequestBody User user) {
// 处理用户创建逻辑
return new ResponseEntity<>("User created successfully", HttpStatus.CREATED);
}
}
注意:
- Controller 类上必须添加
@Validated注解,才能使方法参数校验生效。 - 方法参数上需要同时使用
@Valid和@RequestBody注解。@RequestBody注解告诉 Spring 将请求体转换为 User 对象,而@Valid注解告诉 Spring 对该 User 对象进行校验。
如果校验失败,会抛出 MethodArgumentNotValidException 异常。我们需要全局异常处理来捕获这个异常并返回友好的错误信息。
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
这段代码定义了一个全局异常处理器,用于捕获 MethodArgumentNotValidException 异常,并将错误信息以键值对的形式返回。
@Validated 和 @Valid 的区别
@Validated 和 @Valid 都是用于参数校验的注解,但它们之间存在一些重要的区别:
| 特性 | @Valid |
@Validated |
|---|---|---|
| 出身 | JSR-303 规范(Bean Validation API) | Spring Validation API |
| 主要用途 | 标注在方法参数或者成员变量上 | 标注在类级别上,开启分组校验功能。同时也可以标注在方法参数上,效果和@Valid类似 |
| 是否支持分组 | 不支持 | 支持 |
| 嵌套校验 | 需要配合 @Valid 才能进行嵌套校验 |
需要配合 @Valid 才能进行嵌套校验 |
| 触发时机 | 运行时 | 运行时 |
1. 分组校验:
@Validated 最大的特点是支持分组校验。我们可以定义多个校验分组,并在不同的场景下使用不同的分组进行校验。
首先,定义一个分组接口:
public interface CreateGroup {
}
然后,在 User 类中,将不同的校验规则分配给不同的分组:
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import javax.validation.groups.Default;
public class User {
@NotBlank(message = "用户名不能为空", groups = {CreateGroup.class})
@Size(min = 2, max = 20, message = "用户名长度必须在2到20之间", groups = {Default.class})
private String username;
@NotBlank(message = "邮箱不能为空", groups = {CreateGroup.class, Default.class})
@Email(message = "邮箱格式不正确", groups = {Default.class})
private String email;
// Getters and setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
在这个例子中,username 字段的 @NotBlank 校验只会在 CreateGroup 分组下生效,而 @Size 校验会在默认分组下生效。 email 的@NotBlank 会在 CreateGroup 和默认分组生效。
在 Controller 中,使用 @Validated 注解指定要使用的分组:
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@Validated
public class UserController {
@PostMapping("/users")
public ResponseEntity<String> createUser(@Validated({CreateGroup.class}) @RequestBody User user) {
// 处理用户创建逻辑
return new ResponseEntity<>("User created successfully", HttpStatus.CREATED);
}
}
注意: @Validated({CreateGroup.class}) 指定了只使用 CreateGroup 分组的校验规则。 如果不指定分组,则默认使用 javax.validation.groups.Default 分组。
2. 嵌套校验:
如果 User 类中包含另一个对象 Address:
import javax.validation.constraints.NotBlank;
public class Address {
@NotBlank(message = "城市不能为空")
private String city;
// Getters and setters
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
}
需要在 User 类中,在 Address 字段上添加 @Valid 注解,才能对 Address 对象进行校验:
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class User {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2到20之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Valid // 必须添加此注解才能进行嵌套校验
private Address address;
// Getters and setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.setUsername(username);
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.setEmail(email);
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
如果没有添加 @Valid 注解,即使 Address 对象的 city 字段为空,也不会触发校验。
总结:关键点再次强调
- 依赖: 确保引入了
spring-boot-starter-validation依赖。 - 注解位置:
@Valid和@Validated注解的位置要正确。 @RequestBody: 使用@RequestBody注解接收请求体数据。- 开启校验: Controller 类上使用
@Validated注解开启校验。 - 分组校验: 使用
@Validated注解指定分组。 - 嵌套校验: 使用
@Valid注解进行嵌套校验。 - 异常处理: 配置全局异常处理,捕获
MethodArgumentNotValidException异常。
实际项目中的应用建议
在实际项目中,参数校验应该贯穿整个系统,而不仅仅局限于 Controller 层。我们可以在 Service 层、DAO 层也进行参数校验,以确保数据的完整性。
另外,可以使用自定义校验器来满足一些特殊的校验需求。例如,校验手机号码格式、身份证号码格式等。
两种注解的选择
- 如果仅仅是简单的参数校验,没有分组需求,使用
@Valid即可。 - 如果需要进行分组校验,或者需要在类级别上开启校验,使用
@Validated。
在大多数情况下,@Valid 已经足够满足需求。只有在需要分组校验时,才需要使用 @Validated。 但在Controller上,@Validated 是必须的,它负责开启校验功能。
示例:自定义校验器
假设我们需要校验一个字符串是否只包含字母和数字。我们可以创建一个自定义校验器:
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
import java.lang.annotation.*;
import java.util.regex.Pattern;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = AlphanumericValidator.class)
@Documented
public @interface Alphanumeric {
String message() default "只能包含字母和数字";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
class AlphanumericValidator implements ConstraintValidator<Alphanumeric, String> {
private static final Pattern ALPHANUMERIC_PATTERN = Pattern.compile("^[a-zA-Z0-9]+$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 允许为空
}
return ALPHANUMERIC_PATTERN.matcher(value).matches();
}
}
然后,在 User 类中使用这个自定义校验器:
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class User {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2到20之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Alphanumeric(message = "密码只能包含字母和数字")
private String password;
// Getters and setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.setUsername(username);
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.setEmail(email);
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
解决校验不生效问题的思路
当遇到参数校验不生效的问题时,可以按照以下步骤进行排查:
- 检查依赖: 确认是否引入了
spring-boot-starter-validation依赖。 - 检查注解: 确认
@Valid和@Validated注解是否添加正确,位置是否正确。 - 检查
@RequestBody: 如果是校验请求体中的参数,确认是否使用了@RequestBody注解。 - 检查 Controller 上的
@Validated: 确认 Controller 类上是否添加了@Validated注解。 - 检查分组: 如果使用了分组校验,确认分组是否指定正确。
- 检查嵌套对象: 如果需要校验嵌套对象,确认嵌套对象上是否添加了
@Valid注解。 - 调试: 使用断点调试,查看校验器是否被调用,以及校验结果是否符合预期。
- 日志: 开启 DEBUG 日志,查看 Spring Validation 的执行过程。
数据校验是保证系统稳定性的重要手段
Spring Boot 提供了强大的参数校验功能,通过合理使用 @Validated 和 @Valid 注解,可以有效地防止脏数据进入系统,提高系统的健壮性。理解它们之间的区别,并根据实际需求选择合适的注解,能够帮助我们更好地构建可靠的应用。 同时,也需要注意异常处理,将校验信息友好的返回给调用方。