微信技術團隊的又一力作,WCDB 簡單易用的資料庫框架
一、引數校驗
在開發中經常需要寫一些欄位校驗的程式碼,比如欄位非空,欄位長度限制,郵箱格式驗證等等,寫這些與業務邏輯關係不大的程式碼個人感覺有兩個麻煩:
- 驗證程式碼繁瑣,重複勞動
- 方法內程式碼顯得冗長
- 每次要看哪些引數驗證是否完整,需要去翻閱驗證邏輯程式碼
hibernate validator(官方文件)提供了一套比較完善、便捷的驗證實現方式。
spring-boot-starter-web
包裡面有hibernate-validator
包,不需要引用hibernate validator依賴。
二、hibernate validator校驗demo
先來看一個簡單的demo,添加了Validator的註解:
import org.hibernate.validator.constraints.NotBlank; import javax.validation.constraints.AssertFalse; import javax.validation.constraints.Pattern;
@Getter @Setter @NoArgsConstructor public class DemoModel { @NotBlank(message="使用者名稱不能為空") private String userName; @NotBlank(message="年齡不能為空") @Pattern(regexp="^[0-9]{1,2}$",message="年齡不正確") private String age; @AssertFalse(message = "必須為false") private Boolean isFalse; /** * 如果是空,則不校驗,如果不為空,則校驗 */ @Pattern(regexp="^[0-9]{4}-[0-9]{2}-[0-9]{2}$",message="出生日期格式不正確") private String birthday; }
POST介面驗證,BindingResult是驗證不通過的結果集合:
@RequestMapping("/demo2") public void demo2(@RequestBody @Valid DemoModel demo, BindingResult result){ if(result.hasErrors()){ for (ObjectError error : result.getAllErrors()) { System.out.println(error.getDefaultMessage()); } } }
POST請求傳入的引數:{"userName":"dd","age":120,"isFalse":true,"birthday":"21010-21-12"}
輸出結果:
出生日期格式不正確 必須為false 年齡不正確
引數驗證非常方便,欄位上註解+驗證不通過提示資訊即可代替手寫一大堆的非空和欄位限制驗證程式碼。下面深入瞭解下引數校驗的玩法。
本文地址:http://www.cnblogs.com/mr-yang-localhost/p/7812038.html
三、hibernate的校驗模式
細心的讀者肯定發現了:上面例子中一次性返回了所有驗證不通過的集合,通常按順序驗證到第一個欄位不符合驗證要求時,就可以直接拒絕請求了。Hibernate Validator有以下兩種驗證模式:
1、普通模式(預設是這個模式)
普通模式(會校驗完所有的屬性,然後返回所有的驗證失敗資訊)
2、快速失敗返回模式
快速失敗返回模式(只要有一個驗證失敗,則返回)
兩種驗證模式配置方式:(參考官方文件)
failFast:true 快速失敗返回模式 false 普通模式
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class ) .configure() .failFast( true ) .buildValidatorFactory(); Validator validator = validatorFactory.getValidator();
和 (hibernate.validator.fail_fast:true 快速失敗返回模式 false 普通模式)
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class ) .configure() .addProperty( "hibernate.validator.fail_fast", "true" ) .buildValidatorFactory(); Validator validator = validatorFactory.getValidator();
四、hibernate的兩種校驗
配置hibernate Validator為快速失敗返回模式:
@Configuration
public class ValidatorConfiguration {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
1、請求引數校驗
如demo裡示例的,驗證請求引數時,在@RequestBody DemoModel demo之間加註解 @Valid,然後後面加BindindResult即可;多個引數的,可以加多個@Valid和BindingResult,如:
public void test()(@RequestBody @Valid DemoModel demo, BindingResult result)
public void test()(@RequestBody @Valid DemoModel demo, BindingResult result,@RequestBody @Valid DemoModel demo2, BindingResult result2)
@RequestMapping("/demo2")
public void demo2(@RequestBody @Valid DemoModel demo, BindingResult result){
if(result.hasErrors()){
for (ObjectError error : result.getAllErrors()) {
System.out.println(error.getDefaultMessage());
}
}
}
2、GET引數校驗(@RequestParam引數校驗)
使用校驗bean的方式,沒有辦法校驗RequestParam的內容,一般在處理Get請求(或引數比較少)的時候,會使用下面這樣的程式碼:
@RequestMapping(value = "/demo3", method = RequestMethod.GET)
public void demo3(@RequestParam(name = "grade", required = true) int grade,@RequestParam(name = "classroom", required = true) int classroom) {
System.out.println(grade + "," + classroom);
}
使用@Valid註解,對RequestParam對應的引數進行註解,是無效的,需要使用@Validated註解來使得驗證生效。如下所示:
a.此時需要使用MethodValidationPostProcessor 的Bean:
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
/**預設是普通模式,會返回所有的驗證不通過資訊集合*/
return new MethodValidationPostProcessor();
}
或 可對MethodValidationPostProcessor 進行設定Validator(因為此時不是用的Validator進行驗證,Validator的配置不起作用)
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
/**設定validator模式為快速失敗返回*/
postProcessor.setValidator(validator());
return postProcessor;
}
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
b.方法所在的Controller上加註解@Validated
@RequestMapping("/validation")
@RestController
@Validated
public class ValidationController {
/**如果只有少數物件,直接把引數寫到Controller層,然後在Controller層進行驗證就可以了。*/
@RequestMapping(value = "/demo3", method = RequestMethod.GET)
public void demo3(@Range(min = 1, max = 9, message = "年級只能從1-9")
@RequestParam(name = "grade", required = true)
int grade,
@Min(value = 1, message = "班級最小隻能1")
@Max(value = 99, message = "班級最大隻能99")
@RequestParam(name = "classroom", required = true)
int classroom) {
System.out.println(grade + "," + classroom);
}
}
c.返回驗證資訊提示
可以看到:驗證不通過時,丟擲了ConstraintViolationException異常,使用同一捕獲異常處理:
@ControllerAdvice
@Component
public class GlobalExceptionHandler {
@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handle(ValidationException exception) {
if(exception instanceof ConstraintViolationException){
ConstraintViolationException exs = (ConstraintViolationException) exception;
Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
/**列印驗證不通過的資訊*/
System.out.println(item.getMessage());
}
}
return "bad request, " ;
}
}
d.驗證
瀏覽器服務請求地址:http://localhost:8080/validation/demo3?grade=18&classroom=888
沒有配置快速失敗返回的MethodValidationPostProcessor 時輸出資訊如下:
年級只能從1-9 班級最大隻能99
配置了快速失敗返回的MethodValidationPostProcessor 時輸出資訊如下:
年級只能從1-9
瀏覽器服務請求地址:http://localhost:8080/validation/demo3?grade=0&classroom=0
沒有配置快速失敗返回的MethodValidationPostProcessor 時輸出資訊如下:
年級只能從1-9 班級最小隻能1
配置了快速失敗返回的MethodValidationPostProcessor 時輸出資訊如下:
年級只能從1-9
3、model校驗
待校驗的model:
@Data
public class Demo2 {
@Length(min = 5, max = 17, message = "length長度在[5,17]之間")
private String length;
/**@Size不能驗證Integer,適用於String, Collection, Map and arrays*/
@Size(min = 1, max = 3, message = "size在[1,3]之間")
private String age;
@Range(min = 150, max = 250, message = "range在[150,250]之間")
private int high;
@Size(min = 3,max = 5,message = "list的Size在[3,5]")
private List<String> list;
}
驗證model,以下全部驗證通過:
@Autowired
private Validator validator;
@RequestMapping("/demo3")
public void demo3(){
Demo2 demo2 = new Demo2();
demo2.setAge("111");
demo2.setHigh(150);
demo2.setLength("ABCDE");
demo2.setList(new ArrayList<String>(){{add("111");add("222");add("333");}});
Set<ConstraintViolation<Demo2>> violationSet = validator.validate(demo2);
for (ConstraintViolation<Demo2> model : violationSet) {
System.out.println(model.getMessage());
}
}
4、物件級聯校驗
物件內部包含另一個物件作為屬性,屬性上加@Valid,可以驗證作為屬性的物件內部的驗證:(驗證Demo2示例時,可以驗證Demo2的欄位)
@Data
public class Demo2 {
@Size(min = 3,max = 5,message = "list的Size在[3,5]")
private List<String> list;
@NotNull
@Valid
private Demo3 demo3;
}
@Data
public class Demo3 {
@Length(min = 5, max = 17, message = "length長度在[5,17]之間")
private String extField;
}
級聯校驗:
/**前面配置了快速失敗返回的Bean*/
@Autowired
private Validator validator;
@RequestMapping("/demo3")
public void demo3(){
Demo2 demo2 = new Demo2();
demo2.setList(new ArrayList<String>(){{add("111");add("222");add("333");}});
Demo3 demo3 = new Demo3();
demo3.setExtField("22");
demo2.setDemo3(demo3);
Set<ConstraintViolation<Demo2>> violationSet = validator.validate(demo2);
for (ConstraintViolation<Demo2> model : violationSet) {
System.out.println(model.getMessage());
}
}
可以校驗Demo3的extField欄位。
5、分組校驗
結論:分組順序校驗時,按指定的分組先後順序進行驗證,前面的驗證不通過,後面的分組就不行驗證。
有這樣一種場景,新增使用者資訊的時候,不需要驗證userId(因為系統生成);修改的時候需要驗證userId,這時候可用使用者到validator的分組驗證功能。
設定validator為普通驗證模式("hibernate.validator.fail_fast", "false"),用到的驗證GroupA、GroupB和model:
GroupA、GroupB:
public interface GroupA {
}
public interface GroupB {
}
驗證model:Person
View Code
如上Person所示,3個分組分別驗證欄位如下:
- GroupA驗證欄位userId;
- GroupB驗證欄位userName、sex;
- Default驗證欄位age(Default是Validator自帶的預設分組)
a、分組
只驗證GroupA、GroupB標記的分組:
@RequestMapping("/demo5")
public void demo5(){
Person p = new Person();
/**GroupA驗證不通過*/
p.setUserId(-12);
/**GroupA驗證通過*/
//p.setUserId(12);
p.setUserName("a");
p.setAge(110);
p.setSex(5);
Set<ConstraintViolation<Person>> validate = validator.validate(p, GroupA.class, GroupB.class);
for (ConstraintViolation<Person> item : validate) {
System.out.println(item);
}
}
或
@RequestMapping("/demo6")
public void demo6(@Validated({GroupA.class, GroupB.class}) Person p, BindingResult result){
if(result.hasErrors()){
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError error : allErrors) {
System.out.println(error);
}
}
}
GroupA、GroupB、Default都驗證不通過的情況:
驗證資訊如下所示:
ConstraintViolationImpl{interpolatedMessage='必須在[4,20]', propertyPath=userName, rootBeanClass=class validator.demo.project.model.Person, messageTemplate='必須在[4,20]'} ConstraintViolationImpl{interpolatedMessage='必須大於0', propertyPath=userId, rootBeanClass=class validator.demo.project.model.Person, messageTemplate='必須大於0'} ConstraintViolationImpl{interpolatedMessage='性別必須在[0,2]', propertyPath=sex, rootBeanClass=class validator.demo.project.model.Person, messageTemplate='性別必須在[0,2]'}
GroupA驗證通過、GroupB、Default驗證不通過的情況:
驗證資訊如下所示:
ConstraintViolationImpl{interpolatedMessage='必須在[4,20]', propertyPath=userName, rootBeanClass=class validator.demo.project.model.Person, messageTemplate='必須在[4,20]'} ConstraintViolationImpl{interpolatedMessage='性別必須在[0,2]', propertyPath=sex, rootBeanClass=class validator.demo.project.model.Person, messageTemplate='性別必須在[0,2]'}
b、組序列
除了按組指定是否驗證之外,還可以指定組的驗證順序,前面組驗證不通過的,後面組不進行驗證:
指定組的序列(GroupA》GroupB》Default):
@GroupSequence({GroupA.class, GroupB.class, Default.class})
public interface GroupOrder {
}
測試demo:
@RequestMapping("/demo7")
public void demo7(){
Person p = new Person();
/**GroupA驗證不通過*/
//p.setUserId(-12);
/**GroupA驗證通過*/
p.setUserId(12);
p.setUserName("a");
p.setAge(110);
p.setSex(5);
Set<ConstraintViolation<Person>> validate = validator.validate(p, GroupOrder.class);
for (ConstraintViolation<Person> item : validate) {
System.out.println(item);
}
}
或
@RequestMapping("/demo8")
public void demo8(@Validated({GroupOrder.class}) Person p, BindingResult result){
if(result.hasErrors()){
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError error : allErrors) {
System.out.println(error);
}
}
}
GroupA、GroupB、Default都驗證不通過的情況:
驗證資訊如下所示:
ConstraintViolationImpl{interpolatedMessage='必須大於0', propertyPath=userId, rootBeanClass=class validator.demo.project.model.Person, messageTemplate='必須大於0'}
GroupA驗證通過、GroupB、Default驗證不通過的情況:
驗證資訊如下所示:
ConstraintViolationImpl{interpolatedMessage='必須在[4,20]', propertyPath=userName, rootBeanClass=class validator.demo.project.model.Person, messageTemplate='必須在[4,20]'} ConstraintViolationImpl{interpolatedMessage='性別必須在[0,2]', propertyPath=sex, rootBeanClass=class validator.demo.project.model.Person, messageTemplate='性別必須在[0,2]'}
結論:分組順序校驗時,按指定的分組先後順序進行驗證,前面的驗證不通過,後面的分組就不行驗證。
五、自定義驗證器
一般情況,自定義驗證可以解決很多問題。但也有無法滿足情況的時候,此時,我們可以實現validator的介面,自定義自己需要的驗證器。
如下所示,實現了一個自定義的大小寫驗證器:
public enum CaseMode {
UPPER,
LOWER;
}
@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class)
@Documented
public @interface CheckCase {
String message() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
CaseMode value();
}
public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
private CaseMode caseMode;
public void initialize(CheckCase checkCase) {
this.caseMode = checkCase.value();
}
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (s == null) {
return true;
}
if (caseMode == CaseMode.UPPER) {
return s.equals(s.toUpperCase());
} else {
return s.equals(s.toLowerCase());
}
}
}
要驗證的Model:
public class Demo{
@CheckCase(value = CaseMode.LOWER,message = "userName必須是小寫")
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
validator配置:
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
驗證測試:
@RequestMapping("/demo4")
public void demo4(){
Demo demo = new Demo();
demo.setUserName("userName");
Set<ConstraintViolation<Demo>> validate = validator.validate(demo);
for (ConstraintViolation<Demo> dem : validate) {
System.out.println(dem.getMessage());
}
}
輸出結果:
userName必須是小寫
六、常見的註解
Bean Validation 中內建的 constraint @Null 被註釋的元素必須為 null @NotNull 被註釋的元素必須不為 null @AssertTrue 被註釋的元素必須為 true @AssertFalse 被註釋的元素必須為 false @Min(value) 被註釋的元素必須是一個數字,其值必須大於等於指定的最小值 @Max(value) 被註釋的元素必須是一個數字,其值必須小於等於指定的最大值 @DecimalMin(value) 被註釋的元素必須是一個數字,其值必須大於等於指定的最小值 @DecimalMax(value) 被註釋的元素必須是一個數字,其值必須小於等於指定的最大值 @Size(max=, min=) 被註釋的元素的大小必須在指定的範圍內 @Digits (integer, fraction) 被註釋的元素必須是一個數字,其值必須在可接受的範圍內 @Past 被註釋的元素必須是一個過去的日期 @Future 被註釋的元素必須是一個將來的日期 @Pattern(regex=,flag=) 被註釋的元素必須符合指定的正則表示式 Hibernate Validator 附加的 constraint @NotBlank(message =) 驗證字串非null,且長度必須大於0 @Email 被註釋的元素必須是電子郵箱地址 @Length(min=,max=) 被註釋的字串的大小必須在指定的範圍內 @NotEmpty 被註釋的字串的必須非空 @Range(min=,max=,message=) 被註釋的元素必須在合適的範圍內
//大於0.01,不包含0.01 @NotNull @DecimalMin(value = "0.01", inclusive = false) private Integer greaterThan; //大於等於0.01 @NotNull @DecimalMin(value = "0.01", inclusive = true) private BigDecimal greatOrEqualThan; @Length(min = 1, max = 20, message = "message不能為空") //不能將Length錯用成Range //@Range(min = 1, max = 20, message = "message不能為空") private String message;
七、參考資料
參考資料:
- http://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html_single/#validator-gettingstarted