springboot使用hibernate validator校验方式
一、参数校验
在开发中经常需要写一些字段校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉有两个麻烦:
- 验证代码繁琐,重复劳动
- 方法内代码显得冗长
- 每次要看哪些参数验证是否完整,需要去翻阅验证逻辑代码
hibernatevalidator(官方文档)提供了一套比较完善、便捷的验证实现方式。
spring-boot-starter-web包里面有hibernate-validator包,不需要引用hibernatevalidator依赖。
二、hibernatevalidator校验demo
先来看一个简单的demo,添加了Validator的注解:
importorg.hibernate.validator.constraints.NotBlank; importjavax.validation.constraints.AssertFalse; importjavax.validation.constraints.Pattern;
@Getter @Setter @NoArgsConstructor publicclassDemoModel{ @NotBlank(message="用户名不能为空") privateStringuserName; @NotBlank(message="年龄不能为空") @Pattern(regexp="^[0-9]{1,2}$",message="年龄不正确") privateStringage; @AssertFalse(message="必须为false") privateBooleanisFalse; /** *如果是空,则不校验,如果不为空,则校验 */ @Pattern(regexp="^[0-9]{4}-[0-9]{2}-[0-9]{2}$",message="出生日期格式不正确") privateStringbirthday; }
POST接口验证,BindingResult是验证不通过的结果集合:
@RequestMapping("/demo2") publicvoiddemo2(@RequestBody@ValidDemoModeldemo,BindingResultresult){ if(result.hasErrors()){ for(ObjectErrorerror:result.getAllErrors()){ System.out.println(error.getDefaultMessage()); } } }
POST请求传入的参数:{"userName":"dd","age":120,"isFalse":true,"birthday":"21010-21-12"}
输出结果:
出生日期格式不正确
必须为false
年龄不正确
参数验证非常方便,字段上注解+验证不通过提示信息即可代替手写一大堆的非空和字段限制验证代码。下面深入了解下参数校验的玩法。
三、hibernate的校验模式
细心的读者肯定发现了:上面例子中一次性返回了所有验证不通过的集合,通常按顺序验证到第一个字段不符合验证要求时,就可以直接拒绝请求了。HibernateValidator有以下两种验证模式:
1、普通模式(默认是这个模式)
普通模式(会校验完所有的属性,然后返回所有的验证失败信息)
2、快速失败返回模式
快速失败返回模式(只要有一个验证失败,则返回)
两种验证模式配置方式:(参考官方文档)
failFast:true 快速失败返回模式 false普通模式
ValidatorFactoryvalidatorFactory=Validation.byProvider(HibernateValidator.class) .configure() .failFast(true) .buildValidatorFactory(); Validatorvalidator=validatorFactory.getValidator();
和(hibernate.validator.fail_fast:true 快速失败返回模式 false普通模式)
ValidatorFactoryvalidatorFactory=Validation.byProvider(HibernateValidator.class) .configure() .addProperty("hibernate.validator.fail_fast","true") .buildValidatorFactory(); Validatorvalidator=validatorFactory.getValidator();
四、hibernate的两种校验
配置hibernateValidator为快速失败返回模式:
@Configuration publicclassValidatorConfiguration{ @Bean publicValidatorvalidator(){ ValidatorFactoryvalidatorFactory=Validation.byProvider(HibernateValidator.class) .configure() .addProperty("hibernate.validator.fail_fast","true") .buildValidatorFactory(); Validatorvalidator=validatorFactory.getValidator(); returnvalidator; } }
1、请求参数校验
如demo里示例的,验证请求参数时,在@RequestBodyDemoModeldemo之间加注解@Valid,然后后面加BindindResult即可;多个参数的,可以加多个@Valid和BindingResult,如:
publicvoidtest()(@RequestBody@ValidDemoModeldemo,BindingResultresult) publicvoidtest()(@RequestBody@ValidDemoModeldemo,BindingResultresult,@RequestBody@ValidDemoModeldemo2,BindingResultresult2)
@RequestMapping("/demo2") publicvoiddemo2(@RequestBody@ValidDemoModeldemo,BindingResultresult){ if(result.hasErrors()){ for(ObjectErrorerror:result.getAllErrors()){ System.out.println(error.getDefaultMessage()); } } }
2、GET参数校验(@RequestParam参数校验)
使用校验bean的方式,没有办法校验RequestParam的内容,一般在处理Get请求(或参数比较少)的时候,会使用下面这样的代码:
@RequestMapping(value="/demo3",method=RequestMethod.GET) publicvoiddemo3(@RequestParam(name="grade",required=true)intgrade,@RequestParam(name="classroom",required=true)intclassroom){ System.out.println(grade+","+classroom); }
使用@Valid注解,对RequestParam对应的参数进行注解,是无效的,需要使用@Validated注解来使得验证生效。如下所示:
a.此时需要使用MethodValidationPostProcessor的Bean:
@Bean publicMethodValidationPostProcessormethodValidationPostProcessor(){ /**默认是普通模式,会返回所有的验证不通过信息集合*/ returnnewMethodValidationPostProcessor(); }
或可对MethodValidationPostProcessor进行设置Validator(因为此时不是用的Validator进行验证,Validator的配置不起作用)
@Bean publicMethodValidationPostProcessormethodValidationPostProcessor(){ MethodValidationPostProcessorpostProcessor=newMethodValidationPostProcessor(); /**设置validator模式为快速失败返回*/ postProcessor.setValidator(validator()); returnpostProcessor; } @Bean publicValidatorvalidator(){ ValidatorFactoryvalidatorFactory=Validation.byProvider(HibernateValidator.class) .configure() .addProperty("hibernate.validator.fail_fast","true") .buildValidatorFactory(); Validatorvalidator=validatorFactory.getValidator(); returnvalidator; }
b.方法所在的Controller上加注解@Validated
@RequestMapping("/validation") @RestController @Validated publicclassValidationController{ /**如果只有少数对象,直接把参数写到Controller层,然后在Controller层进行验证就可以了。*/ @RequestMapping(value="/demo3",method=RequestMethod.GET) publicvoiddemo3(@Range(min=1,max=9,message="年级只能从1-9") @RequestParam(name="grade",required=true) intgrade, @Min(value=1,message="班级最小只能1") @Max(value=99,message="班级最大只能99") @RequestParam(name="classroom",required=true) intclassroom){ System.out.println(grade+","+classroom); } }
c.返回验证信息提示
可以看到:验证不通过时,抛出了ConstraintViolationException异常,使用同一捕获异常处理:
@ControllerAdvice @Component publicclassGlobalExceptionHandler{ @ExceptionHandler @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) publicStringhandle(ValidationExceptionexception){ if(exceptioninstanceofConstraintViolationException){ ConstraintViolationExceptionexs=(ConstraintViolationException)exception; Set>violations=exs.getConstraintViolations(); for(ConstraintViolation>item:violations){ /**打印验证不通过的信息*/ System.out.println(item.getMessage()); } } return"badrequest,"; } }
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 publicclassDemo2{ @Length(min=5,max=17,message="length长度在[5,17]之间") privateStringlength; /**@Size不能验证Integer,适用于String,Collection,Mapandarrays*/ @Size(min=1,max=3,message="size在[1,3]之间") privateStringage; @Range(min=150,max=250,message="range在[150,250]之间") privateinthigh; @Size(min=3,max=5,message="list的Size在[3,5]") privateListlist; }
验证model,以下全部验证通过:
@Autowired privateValidatorvalidator; @RequestMapping("/demo3") publicvoiddemo3(){ Demo2demo2=newDemo2(); demo2.setAge("111"); demo2.setHigh(150); demo2.setLength("ABCDE"); demo2.setList(newArrayList(){{add("111");add("222");add("333");}}); Set >violationSet=validator.validate(demo2); for(ConstraintViolation model:violationSet){ System.out.println(model.getMessage()); } }
4、对象级联校验
对象内部包含另一个对象作为属性,属性上加@Valid,可以验证作为属性的对象内部的验证:(验证Demo2示例时,可以验证Demo2的字段)
@Data publicclassDemo2{ @Size(min=3,max=5,message="list的Size在[3,5]") privateListlist; @NotNull @Valid privateDemo3demo3; } @Data publicclassDemo3{ @Length(min=5,max=17,message="length长度在[5,17]之间") privateStringextField; }
级联校验:
/**前面配置了快速失败返回的Bean*/ @Autowired privateValidatorvalidator; @RequestMapping("/demo3") publicvoiddemo3(){ Demo2demo2=newDemo2(); demo2.setList(newArrayList(){{add("111");add("222");add("333");}}); Demo3demo3=newDemo3(); demo3.setExtField("22"); demo2.setDemo3(demo3); Set >violationSet=validator.validate(demo2); for(ConstraintViolation model:violationSet){ System.out.println(model.getMessage()); } }
可以校验Demo3的extField字段。
5、分组校验
结论:分组顺序校验时,按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不行验证。
有这样一种场景,新增用户信息的时候,不需要验证userId(因为系统生成);修改的时候需要验证userId,这时候可用用户到validator的分组验证功能。
设置validator为普通验证模式("hibernate.validator.fail_fast","false"),用到的验证GroupA、GroupB和model:
GroupA、GroupB: publicinterfaceGroupA{ } publicinterfaceGroupB{ }
验证model:Person
@Data publicclassPerson{ @NotBlank @Range(min=1,max=Integer.MAX_VALUE,message="必须大于0",groups={GroupA.class}) /**用户id*/ privateIntegeruserId; @NotBlank @Length(min=4,max=20,message="必须在[4,20]",groups={GroupB.class}) /**用户名*/ privateStringuserName; @NotBlank @Range(min=0,max=100,message="年龄必须在[0,100]",groups={Default.class}) /**年龄*/ privateIntegerage; @Range(min=0,max=2,message="性别必须在[0,2]",groups={GroupB.class}) /**性别0:未知;1:男;2:女*/ privateIntegersex; }
如上Person所示,3个分组分别验证字段如下:
- GroupA验证字段userId;
- GroupB验证字段userName、sex;
- Default验证字段age(Default是Validator自带的默认分组)
a、分组
只验证GroupA、GroupB标记的分组:
@RequestMapping("/demo5") publicvoiddemo5(){ Personp=newPerson(); /**GroupA验证不通过*/ p.setUserId(-12); /**GroupA验证通过*/ //p.setUserId(12); p.setUserName("a"); p.setAge(110); p.setSex(5); Set>validate=validator.validate(p,GroupA.class,GroupB.class); for(ConstraintViolation item:validate){ System.out.println(item); } }
或
@RequestMapping("/demo6") publicvoiddemo6(@Validated({GroupA.class,GroupB.class})Personp,BindingResultresult){ if(result.hasErrors()){ ListallErrors=result.getAllErrors(); for(ObjectErrorerror:allErrors){ System.out.println(error); } } }
GroupA、GroupB、Default都验证不通过的情况:
验证信息如下所示:
ConstraintViolationImpl{interpolatedMessage='必须在[4,20]',propertyPath=userName,rootBeanClass=classvalidator.demo.project.model.Person,messageTemplate='必须在[4,20]'} ConstraintViolationImpl{interpolatedMessage='必须大于0',propertyPath=userId,rootBeanClass=classvalidator.demo.project.model.Person,messageTemplate='必须大于0'} ConstraintViolationImpl{interpolatedMessage='性别必须在[0,2]',propertyPath=sex,rootBeanClass=classvalidator.demo.project.model.Person,messageTemplate='性别必须在[0,2]'}
GroupA验证通过、GroupB、Default验证不通过的情况:
验证信息如下所示:
ConstraintViolationImpl{interpolatedMessage='必须在[4,20]',propertyPath=userName,rootBeanClass=classvalidator.demo.project.model.Person,messageTemplate='必须在[4,20]'} ConstraintViolationImpl{interpolatedMessage='性别必须在[0,2]',propertyPath=sex,rootBeanClass=classvalidator.demo.project.model.Person,messageTemplate='性别必须在[0,2]'}
b、组序列
除了按组指定是否验证之外,还可以指定组的验证顺序,前面组验证不通过的,后面组不进行验证:
指定组的序列(GroupA》GroupB》Default):
@GroupSequence({GroupA.class,GroupB.class,Default.class}) publicinterfaceGroupOrder{ }
测试demo:
@RequestMapping("/demo7") publicvoiddemo7(){ Personp=newPerson(); /**GroupA验证不通过*/ //p.setUserId(-12); /**GroupA验证通过*/ p.setUserId(12); p.setUserName("a"); p.setAge(110); p.setSex(5); Set>validate=validator.validate(p,GroupOrder.class); for(ConstraintViolation item:validate){ System.out.println(item); } }
或
@RequestMapping("/demo8") publicvoiddemo8(@Validated({GroupOrder.class})Personp,BindingResultresult){ if(result.hasErrors()){ ListallErrors=result.getAllErrors(); for(ObjectErrorerror:allErrors){ System.out.println(error); } } }
GroupA、GroupB、Default都验证不通过的情况:
验证信息如下所示:
ConstraintViolationImpl{interpolatedMessage='必须大于0',propertyPath=userId,rootBeanClass=classvalidator.demo.project.model.Person,messageTemplate='必须大于0'}
GroupA验证通过、GroupB、Default验证不通过的情况:
验证信息如下所示:
ConstraintViolationImpl{interpolatedMessage='必须在[4,20]',propertyPath=userName,rootBeanClass=classvalidator.demo.project.model.Person,messageTemplate='必须在[4,20]'} ConstraintViolationImpl{interpolatedMessage='性别必须在[0,2]',propertyPath=sex,rootBeanClass=classvalidator.demo.project.model.Person,messageTemplate='性别必须在[0,2]'}
结论:分组顺序校验时,按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不行验证。
五、自定义验证器
一般情况,自定义验证可以解决很多问题。但也有无法满足情况的时候,此时,我们可以实现validator的接口,自定义自己需要的验证器。
如下所示,实现了一个自定义的大小写验证器:
publicenumCaseMode{ UPPER, LOWER; } @Target({ElementType.METHOD,ElementType.FIELD,ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy=CheckCaseValidator.class) @Documented public@interfaceCheckCase{ Stringmessage()default""; Class>[]groups()default{}; Class[]payload()default{}; CaseModevalue(); } publicclassCheckCaseValidatorimplementsConstraintValidator{ privateCaseModecaseMode; publicvoidinitialize(CheckCasecheckCase){ this.caseMode=checkCase.value(); } publicbooleanisValid(Strings,ConstraintValidatorContextconstraintValidatorContext){ if(s==null){ returntrue; } if(caseMode==CaseMode.UPPER){ returns.equals(s.toUpperCase()); }else{ returns.equals(s.toLowerCase()); } } }
要验证的Model:
publicclassDemo{ @CheckCase(value=CaseMode.LOWER,message="userName必须是小写") privateStringuserName; publicStringgetUserName(){ returnuserName; } publicvoidsetUserName(StringuserName){ this.userName=userName; } }
validator配置:
@Bean publicValidatorvalidator(){ ValidatorFactoryvalidatorFactory=Validation.byProvider(HibernateValidator.class) .configure() .addProperty("hibernate.validator.fail_fast","true") .buildValidatorFactory(); Validatorvalidator=validatorFactory.getValidator(); returnvalidator; }
验证测试:
@RequestMapping("/demo4") publicvoiddemo4(){ Demodemo=newDemo(); demo.setUserName("userName"); Set>validate=validator.validate(demo); for(ConstraintViolation dem:validate){ System.out.println(dem.getMessage()); } }
输出结果:
userName必须是小写
六、常见的注解
BeanValidation中内置的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=)被注释的元素必须符合指定的正则表达式 HibernateValidator附加的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) privateIntegergreaterThan; //大于等于0.01 @NotNull @DecimalMin(value="0.01",inclusive=true) privateBigDecimalgreatOrEqualThan; @Length(min=1,max=20,message="message不能为空") //不能将Length错用成Range //@Range(min=1,max=20,message="message不能为空") privateStringmessage;
七、参考资料
参考资料:
http://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html_single/#validator-gettingstarted