在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
如果不使用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