SpringBoot 數據校驗


在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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM