JAVA Spring Boot 参数校验未触发?@Validated 与 @Valid 注解差异剖析

Spring Boot 参数校验未触发?@Validated 与 @Valid 注解差异剖析

各位开发者,大家好。今天我们来聊聊 Spring Boot 中参数校验相关的问题,尤其是“参数校验未触发”的情况,以及 @Validated@Valid 这两个注解的区别和使用场景。参数校验是保证系统健壮性和数据完整性的重要一环。一个完善的参数校验机制能够有效防止脏数据进入系统,减少错误发生的可能性。

为什么参数校验没有生效?

首先,我们来看看导致参数校验没有生效的常见原因:

  1. 忘记添加依赖: Spring Boot 的校验功能依赖于 hibernate-validator,需要确保在 pom.xml 文件中引入了相应的依赖。

    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
  2. 注解位置错误: @Valid@Validated 注解必须添加到需要校验的参数或对象上。

  3. 缺少 @RequestBody 注解: 如果是校验请求体中的参数,务必确保方法参数使用了 @RequestBody 注解,否则校验器无法获取到请求体中的数据。

  4. 未开启校验: 对于 Controller 方法的参数校验,需要在 Controller 类上使用 @Validated 注解。

  5. 分组校验使用错误: 如果使用了分组校验,需要正确指定要使用的分组。

  6. 嵌套对象校验未生效: 如果需要校验嵌套对象,需要在嵌套对象上添加 @Valid 注解。

  7. 自定义校验器的问题: 如果使用了自定义校验器,需要确保校验器的逻辑正确,并且正确地绑定到相应的字段上。

  8. 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;
    }
}

解决校验不生效问题的思路

当遇到参数校验不生效的问题时,可以按照以下步骤进行排查:

  1. 检查依赖: 确认是否引入了 spring-boot-starter-validation 依赖。
  2. 检查注解: 确认 @Valid@Validated 注解是否添加正确,位置是否正确。
  3. 检查 @RequestBody 如果是校验请求体中的参数,确认是否使用了 @RequestBody 注解。
  4. 检查 Controller 上的 @Validated 确认 Controller 类上是否添加了 @Validated 注解。
  5. 检查分组: 如果使用了分组校验,确认分组是否指定正确。
  6. 检查嵌套对象: 如果需要校验嵌套对象,确认嵌套对象上是否添加了 @Valid 注解。
  7. 调试: 使用断点调试,查看校验器是否被调用,以及校验结果是否符合预期。
  8. 日志: 开启 DEBUG 日志,查看 Spring Validation 的执行过程。

数据校验是保证系统稳定性的重要手段

Spring Boot 提供了强大的参数校验功能,通过合理使用 @Validated@Valid 注解,可以有效地防止脏数据进入系统,提高系统的健壮性。理解它们之间的区别,并根据实际需求选择合适的注解,能够帮助我们更好地构建可靠的应用。 同时,也需要注意异常处理,将校验信息友好的返回给调用方。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注