在開發中常常須要寫一些字段校驗的代碼,好比字段非空,字段長度限制,郵箱格式驗證等等,寫這些與業務邏輯關係不大的代碼我的感受有兩個麻煩:html
hibernate validator(官方文檔)提供了一套比較完善、便捷的驗證明現方式。java
spring-boot-starter-web
包裏面有hibernate-validator
包,不須要引用hibernate validator依賴。git
先來看一個簡單的demo,添加了Validator的註解:web
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"}spring
輸出結果:瀏覽器
出生日期格式不正確
必須爲false
年齡不正確app
參數驗證很是方便,字段上註解+驗證不經過提示信息便可代替手寫一大堆的非空和字段限制驗證代碼。下面深刻了解下參數校驗的玩法。ide
本文地址:http://www.cnblogs.com/mr-yang-localhost/p/7812038.htmlspring-boot
細心的讀者確定發現了:上面例子中一次性返回了全部驗證不經過的集合,一般按順序驗證到第一個字段不符合驗證要求時,就能夠直接拒絕請求了。Hibernate Validator有如下兩種驗證模式:
普通模式(會校驗完全部的屬性,而後返回全部的驗證失敗信息)
快速失敗返回模式(只要有一個驗證失敗,則返回)
兩種驗證模式配置方式:(參考官方文檔)
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 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; } }
如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()); } } }
使用校驗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註解來使得驗證生效。以下所示:
@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; }
@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); } }
能夠看到:驗證不經過時,拋出了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, " ; } }
瀏覽器服務請求地址: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
待校驗的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()); } }
對象內部包含另外一個對象做爲屬性,屬性上加@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字段。
結論:分組順序校驗時,按指定的分組前後順序進行驗證,前面的驗證不經過,後面的分組就不行驗證。
有這樣一種場景,新增用戶信息的時候,不須要驗證userId(由於系統生成);修改的時候須要驗證userId,這時候可用用戶到validator的分組驗證功能。
設置validator爲普通驗證模式("hibernate.validator.fail_fast", "false"),用到的驗證GroupA、GroupB和model:
GroupA、GroupB:
public interface GroupA { } public interface GroupB { }
驗證model:Person
@Data public class Person { @NotBlank @Range(min = 1,max = Integer.MAX_VALUE,message = "必須大於0",groups = {GroupA.class}) /**用戶id*/ private Integer userId; @NotBlank @Length(min = 4,max = 20,message = "必須在[4,20]",groups = {GroupB.class}) /**用戶名*/ private String userName; @NotBlank @Range(min = 0,max = 100,message = "年齡必須在[0,100]",groups={Default.class}) /**年齡*/ private Integer age; @Range(min = 0,max = 2,message = "性別必須在[0,2]",groups = {GroupB.class}) /**性別 0:未知;1:男;2:女*/ private Integer sex; }
如上Person所示,3個分組分別驗證字段以下:
只驗證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); } } }
驗證信息以下所示:
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]'}
驗證信息以下所示:
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]'}
除了按組指定是否驗證以外,還能夠指定組的驗證順序,前面組驗證不經過的,後面組不進行驗證:
指定組的序列(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); } } }
驗證信息以下所示:
ConstraintViolationImpl{interpolatedMessage='必須大於0', propertyPath=userId, rootBeanClass=class validator.demo.project.model.Person, messageTemplate='必須大於0'}
驗證信息以下所示:
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