# 数据校验

在 Java 体系中,Bean Validation 2.0(JSR380)是当前的数据校验规范,Hibernate Validator 是 JSR380 的参考实现,也是事实标准。SpringBoot 整合了 Hibernator Validator 作为数据校验的实现。

# 引入依赖

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

spring-boot-starter-web已经包含了 hibernate-validator 依赖,如果不做 web 项目,也可以使用spring-boot-starter-validation引入依赖。

# 校验 Controller

在 Web 开发中,最常用的就是对前端传入的数据进行校验。

# PathVariables 和 RequestParameters

PathVariables 指的是请求路径中的变量,比如/book/{id}, id 即为 PathVariables;

RequestParameters 指的是请求参数,比如/book/1?name=diana, name 即为 RequestParameters。

对于专业术语,英文表述更为准确,文章中更多使用英文术语。

对于这两类数据,可以直接对 Controller 的方法参数进行校验。

@RestController
@Validated
public class BookController {
  @GetMapping("/book/{id}")
  public String getById(@PathVariable @Max(50) Integer id) {
    return "bookById";
  }

  @GetMapping("/bookByName")
  public String getByName(@RequestParam @Length(min = 2, max = 20) String name) {
    return "bookByName";
  }
}

在方法参数前面加上相应的注解(annotations)即可添加相关约束(constraint)。

另外需要在 Controller 上添加@Validated注解告诉 Spring 需要校验参数

Hibernate Validator 自带了很多基础注解,见后文。

# RequestBody

RequestBody 指定是客户端通过 POST 或 PUT 方法在请求体中传递过来的 JSON 格式的数据。

对于 RequestBody 数据的校验,我们首先需要定义一个 DTO 对象作为容器来接收数据。

SpringBoot 会自动将 RequestBody 映射到 DTO 对象,我们需要校验 DTO 对象是否符合约束条件。

DTO: 数据传输对象

# 定义 DTO 对象

@Getter
@Setter
public class BookDTO {
  @Max(50)
  private Integer id;
  @Length(min = 2, max = 20)
  private String name;
}

校验 DTO 对象即校验对象的成员变量是否满足条件,所以我们需要在 DTO 对象的成员变量上加上相应注解。

注意 DTO 需要添加 Getter 和 Setter 方法用于序列化和反序列化,此处使用 Lombok 添加

# 添加校验

  @PostMapping("/book/add")
  public BookDTO addBook(@RequestBody @Validated BookDTO bookDTO) {
    return bookDTO;
  }

需要将 DTO 对象添加到 Controller 方法参数中,同时添加@Validated注解

BookDTO 前面的@Validated 也可以换成@Valid,@Validated 是 Spring 定义的对标准@Valid 的扩展,此处为了方便统一使用@Validated 注解

# 关于嵌套的 RequestBody

很多时候,RequestBody 具有多层嵌套结构,相应的 DTO 对象也要有多层嵌套。

@Getter
@Setter
public class BookDTO {
  @Max(50)
  private Integer id;
  @Length(min = 2, max = 20)
  private String name;
  @Valid
  private PublisherDTO publisher;
}

@Getter
@Setter
public class PublisherDTO {
  @Length(min = 2, max = 20)
  private String name;
}

比如 PublisherDTO 是嵌套在 BookDTO 内的一层对象,我们首先需要做两件事:

  • 在 PublisherDTO 的成员变量上添加校验注解
  • 在 BookDTO 的 publisher 成员变量上添加@Valid注解

# 校验 Service 和 Entity

在 Service 和 Entity 中也可以使用校验,Service 的校验和 Controller 类似

@Service
@Validated
public class BookService {
  public String getById(@Max(20) Integer id) {
    return "book";
  }
}

在 Entity 中添加了@Entity注解后不需要再添加@Validate,因为校验过程由 JPA 调用 Validator 完成

@Entity
public class Book {
  @Id
  @Max(20)
  private Integer id;
  @Length(min = 2, max = 20)
  private String name;
}

通常来说,Bean Validation 只需要在 Controller 层完成即可,不需要每一层都进行校验。

# 内置校验注解

# 标准 JSR 注解

@NotBlank
@NotEmpty
@NotNull
@Max(value=)
@Min(value=)
@AssertFalse
@AssertTrue
@Size(min=, max=)
@Positive
@Negative
@Past
@Email
@Future
@Pattern(regex=, flags=)
@DecimalMax(value=, inclusive=)
@DecimalMin(value=, inclusive=)
@Digits(integer=, fraction=)
@FutureOrPresent
@NegativeOrZero
@Null
@PastOrPresent
@PositiveOrZero

# Hibernate 扩展注解

@Range(min=, max=)
@Length(min=, max=)
@URL(protocol=, host=, port=, regexp=, flags=)

@CreditCardNumber(ignoreNonDigitCharacters=)
@Currency(value=)
@DurationMax(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=)
@DurationMin(days=, hours=, minutes=, seconds=, millis=, nanos=, inclusive=)
@EAN
@ISBN
@CodePointLength(min=, max=, normalizationStrategy=)
@LuhnCheck(startIndex= , endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=)
@Mod10Check(multiplier=, weight=, startIndex=, endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=)
@Mod11Check(threshold=, startIndex=, endIndex=, checkDigitIndex=, ignoreNonDigitCharacters=, treatCheck10As=, treatCheck11As=)
@SafeHtml(whitelistType= , additionalTags=, additionalTagsWithAttributes=, baseURI=)
@ScriptAssert(lang=, script=, alias=, reportOn=)
@UniqueElements

官方文档:https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-defineconstraints-spec

# 如果不使用 SpringBoot,如何进行校验?

Bean Validation 分为 Annotation 和 Validator 两部分,前者用于添加约束条件,后者用于校验。

SpringBoot 扫描到@Validated之后就会帮我们调用 Validator 进行参数校验,如果没有 SpringBoot,我们也可以自己调用 Validator 进行校验。

public static void main(String[] args) {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();
    PublisherDTO publisher = new PublisherDTO();
    publisher.setName("a");
    Set<ConstraintViolation<PublisherDTO>> violations = validator.validate(publisher);
    // 长度需要在2和20之间
    violations.forEach(violation -> System.out.println(violation.getMessage()));
  }

当然,SpringBoot 也可以帮我们注入 validator 实例

@Component
public class InvokeValidator {

  private Validator validator;

  public InvokeValidator(Validator validator) {
    this.validator = validator;
  }

  public String validate() {
    PublisherDTO publisher = new PublisherDTO();
    publisher.setName("a");
    Set<ConstraintViolation<PublisherDTO>> violations = this.validator.validate(publisher);
    // 长度需要在2和20之间
    StringBuilder message = new StringBuilder();
    violations.forEach(violation -> message.append(violation.getMessage()).append(";"));
    return message.toString();
  }

# 如何自定义注解和校验器?

如果内置校验注解无法满足需要,我们也可以自定义校验器。自定义校验器需要分别定义 Annotation 和 Validator 两部分,然后将两者加以关联。

比如我们需要校验“两次密码输入是否相同”,就可以使用自定义校验器。

# 自定义注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Constraint(validatedBy = PasswordEqualValidator.class)
public @interface PasswordEqual {
	int min() default 5;
  String message() default "两次密码不一致";

  Class<?>[] groups() default { };

  Class<? extends Payload>[] payload() default { };
}
  • 首先定义注解@PasswordEqual,注解必须包含 message,groups,payload,此外我们添加了参数 min 要求密码不少于 5 个字符。
  • 注解定义完成后需要使用@Constraint(validatedBy=)和校验器相关联

# 自定义校验器

public class PasswordEqualValidator implements ConstraintValidator<PasswordEqual, UserDTO> {

  private int min;

  @Override
  public void initialize(PasswordEqual constraintAnnotation) {
    this.min = constraintAnnotation.min();
  }

  @Override
  public boolean isValid(UserDTO userDTO, ConstraintValidatorContext constraintValidatorContext) {
    String password1 = userDTO.getPassword1();
    String password2 = userDTO.getPassword2();
    return password1 != null && password1.length() >= this.min && password1.equals(password2);
  }
}
  • 首先定义一个类 PasswordEqualValidator,要求实现ConstraintValidator<PasswordEqual, UserDTO>接口,该接口是一个泛型接口,类型分别为“关联的注解类型”和“注解标注的对象类型”,由于我们的注解标注在UserDTO这个类上,所以此处填写 UserDTO。
  • 校验器需要实现两个方法initialize()isValid(),前者用于关联注解,从注解中获取参数值;后者用于关联“被标注的对象”同时完成校验逻辑,最终返回 boolean 值。

# 测试自定义注解

@Getter
@Setter
@PasswordEqual(min = 10)
public class UserDTO {
  private String name;
  private String password1;
  private String password2;
}

@RestController
@Validated
public class UserController {
  @PostMapping("/login")
  public String login(@RequestBody @Validated UserDTO userDTO) {
    return "login success";
  }
}

我们将@PasswordEqual(min = 10)标注在 UserDTO 上即可添加约束,和内置注解使用方式相同。

# 返回校验失败信息

对于 RequestBody,校验失败会抛出MethodArgumentNotValidException异常

对于 PathVariables 和 RequestParameters,校验失败会抛出ConstraintViolationException异常

我们可以使用@ControllerAdvice@ExceptionHandler捕获和处理特定的 Controller 异常并进行结构化返回。

具体内容见我的另一篇文章:”SpringBoot 全局异常处理”。

源代码:https://github.com/PeterWangYong/blog-code/tree/master/validation

上次更新: 2/13/2025, 3:29:47 AM