官网
- docs.jboss.org/hibernate/v…
- 测试:github.com/hibernate/h…
- JSR-303
- 早在JavaEE6规范中就定义了参数校验的规范,它就是JSR-303,它定义了Bean Validation,即对bean属性进行校验。
- SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底层使用Hibernate Validator,Hibernate Validator是Bean Validation 的参考实现。
为什么使用?
- 优点
- 可方便修改校验(对于值的特定校验,比如email,对于业务的校验,比如存在userId,唯一email)
- 对于性能来说,我不知道有没有损失,但是应该差不多
依赖
- spring-boot-starter-validation就是直接使用hibernate-validator的
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
@Valid和@Validated
- 相同使用:校验bean,校验方法参数
- 在Spring开启方法AOP校验:
@Validated注解在类上,@Validated或者@Valid让方法启用校验
- 区别
@Validated有分组参数
- 当在Spring把
@Validated注解在类上,即使用方法参数校验时,最好用@Validated来级联bean属性校验
- 原因看
MethodValidationInterceptor的bean校验部分
spring的validation实现
- 方法参数和方法返回值
MethodValidationInterceptor
- AOP:因为是AOP,因此会对类代理,而想要让类被代理,就必须注解
@Validated
- 因此要支持方法参数和方法返回值就必须在类上注解
@Validated
- bean校验
@Valid + 有body的比如JSON
- 导致进行了2次bean校验
- 因为
HandlerMethodArgumentResolver(如RequestResponseBodyMethodProcessor)也会自动进行校验
- 解决方法:使用
@Validated而不是@Valid
- 因为hibernate只认得自己的@Valid注解,使得方法校验不会进行bean校验
- 而
RequestResponseBodyMethodProcessor认得@Validated,直接让其进行bean校验
MethodArgumentResolver会自动进行校验
- 目前有:
ModelAttributeMethodProcessor``RequestPartMethodArgumentResolver``RequestResponseBodyMethodProcessor
- 都使用:
org.springframework.validation.DataBinder#validate
- 都有一个差不多的validateIfApplicable方法进行遍历参数注解进行调用
- 分组:根据方法参数上的注解
javax.validation.Valid无分组
@Validated或者其他Valid注解的value属性
- 异常
- BindException:ModelAttributeMethodProcessor抛出,这个是最后一个ArgumentResolver,有@ModelAttribute或者无@RequestBody的bean对象
- ConstraintViolationException:MethodValidationInterceptor抛出,即普通参数,基本类/集合
- MethodArgumentNotValidException:RequestResponseBodyMethodProcessor/RequestPartMethodArgumentResolver抛出
- 为什么path/其他的不用检查
- 比如path, 如果参数类型是Long,传字符串,convertService就会抛出MethodArgumentTypeMismatchException
- 都是使用javax.validation.Validator
- 用的是org.hibernate.validator.internal.engine.ValidatorImpl
contraint注解
位置
- field constraints:就是字段
- property constraints:就是属性(getter、setter方法)
- container element constraints:容器元素,但更像是泛型参数
- 如
List<@NotEmtpy String>
A<@NotEmtpy String>需要一个类实现ValueExtractor<A<@ExtractedValue ?>>取出元素值
- class constraints
- object graph validation(Cascaded validation级联校验):对象图校验,hibernate确保了不会循环校验,不会校验NULL值
- object-graph-validation(cascaded validation)
- 如校验User,User有字段Pet,给Pet加
@Valid,这样就会对Pet进行级联校验
- 原本只是校验User的字段,现在还会校验User的Pet的字段
- 容器也可以,如
List<@NotNull @Valid Person>
- 原本只是校验元素,现在还会校验元素字段
- 6.0支持
@Valid List<Person>等同于List<@Valid Person>
- method or constructor的parameters:
- 顺序:
- method cross-parameter > method parameter(如果有cascaded就执行)
- bean类 > bean字段(如果有cascaded就执行)
- 至于在一个地方的顺序==注解的顺序:通常是从上到下(但不一定,不能依赖这个)
继承问题
自定义
- customconstraints
- 实现ConstraintValidator,会在IOC中创建(不需要手动注入),因此可以注入属性
- initialize:只在创建时调用
- 只有相同的注解值,才会使用同一个ConstraintValidator对象
- constraint-composition
- 使用一个注解,该注解注解了constraint注解的集合
- 加
@ReportAsSingleViolation就只报告一个ConstraintViolation,而是各自都有ConstraintViolation
构建消息
context.disableDefaultConstraintViolation();
context
.buildConstraintViolationWithTemplate("TestLog-dynamic-message")
.addConstraintViolation();
消息模板
- chapter-message-interpolation
ValidationMessages.properties
- 默认
ResourceBundleMessageInterpolator
resolveMessage(从properties资源中获取{KEY}的value)
- 遍历先解析{},再解析${}(EL表达式),如果没有则直接跳过。
- EL表达式需要配置
constraintExpressionLanguageFeatureLevel
- 值:
{}: 注解属性、context.messageParameters
${}: org.hibernate.validator.internal.engine.messageinterpolation.ElTermResolver.bindContextValues
ElTermResolver.VALIDATED_VALUE_NAME=validatedValue(当前验证的值)
RootResolver.FORMATTER: formatter.format('%s %d', xxx,xxx), 如String.format一样(java.util.Formatter)
- 注解属性、
context.expressionVariables
- 添加messageParameter、expressionVariable:
((HibernateConstraintValidatorContext)context).addXXXX
Validator实现
org.hibernate.validator.internal.engine.ValidatorImplimplementsjavax.validation.Validator**, **javax.validation.executable.ExecutableValidator
- 创建
Validation.byDefaultProvider().configure().buildValidatorFactory().getValidator()
Validation.byProvider(HibernateValidator.class).configure().buildValidatorFactory().getValidator()
- 主要方法:都返回
Set<ConstraintViolation>即约束违规集合
- 检验bean
validate(T object, Class<?>... groups)
- 检验bean或者说一个类的属性,共用一个ConstraintValidator实例
validateProperty(T object,String propertyName,Class<?>... groups)
validateValue(Class<T> beanType, String propertyName, Object value, Class<?>... groups)
- 校验可执行的(比如方法,构造函数)
ExecutableValidator forExecutables()hibernate就是返回这个ValidatorImpl即this
ExecutableValidator:主要校验方法参数、返回值
validateParameters(T object,Method method,Object[] parameterValues,Class<?>... groups)
validateReturnValue(T object,Method method,Object returnValue,Class<?>... groups)
- 对构造函数:
validateConstructorParameters``validateConstructorReturnValue
ConstraintViolation
BeanMetaData:bean类/方法所属类,约束的缓存
- 在第一次时,从类解析出constraintMetaDatas
ConstraintMetaData:某个位置的约束数据(多个)
- 校验顺序
- 同位置有多个时,从靠近位置的开始
- 比如方法参数是注解在右边的,那就从最左边一个约束开始
- 方法参数校验:注解在方法上 > 注解在参数上(按顺序)
- 注解在方法上的ConstraintValidtor需要额外注解
@SupportedValidationTarget(ValidationTarget.PARAMETERS),value是所有参数
- bean校验:bean类 > bean属性
Validtor#validate和ExecutableValidator#validateParameters(其他的校验也差不多)
- 一开使用ValidationOrder计算Groups/Sequences(后面校验就是从这获取的Groups/Sequences进行校验)
- Groups和Sequences只会执行一种方式,默认Groups,有Sequences就不用Groups
- 对于Sequence, 默认failFast, 可以在 Validation.byProvider(HibernateValidator.class).configure().failFast(false)配置
- Group/Sequence校验
- 第一步:校验本身
- 对于bean,就是先校验bean字段/属性上的(值是字段值),再校验bean类上的(值是该bean)
- 对方法,就是先校验cross-parameter(值是所有参数即
Object[]),再校验单个参数上的(值是参数值)
- 统一校验目标上有多个,不确定顺序
- 第二步:再校验validateCascadedConstraints
- 即校验有Valid的方法参数的属性/有Valid的bean的属性的属性
正常需求例子
package com.liruo.learn.spring.mvc.controller.valid;
import com.liruo.learn.spring.mvc.validate.TestIO;
import com.liruo.learn.spring.mvc.validate.TestLog;
import lombok.Data;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.groups.Default;
@Validated
@RequestMapping("/normal")
@RestController
public class NormalController {
@Data
public static class Bean {
@TestLog("校验name")
private String name;
@TestLog(value = "校验name2-Create", groups = Groups.Create.class)
private String name2;
@TestLog(value = "校验name3-Update", groups = Groups.Update.class)
private String name3;
}
@PostMapping("/Valid/{id}")
public String post(@Valid @NotNull @TestLog("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
return "id=" + id + " " + bean.toString();
}
@PostMapping("/Validated/{id}")
public String get(@Validated @NotNull @TestLog("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
return "id=" + id + " " + bean.toString();
}
interface Groups{
interface Create {}
interface Update {}
}
@PostMapping("/Validated/group/Update/{id}")
public String postUpdateGroup(@Validated({Default.class, Groups.Update.class}) @NotNull @TestIO("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
return "id=" + id + " " + bean.toString();
}
@PostMapping("/Validated/group/Create/{id}")
public String postCreateGroup(@Validated({Default.class, Groups.Create.class}) @NotNull @TestIO("io1") @RequestBody Bean bean, @Min(2) @PathVariable("id") Integer id) {
return "id=" + id + " " + bean.toString();
}
}
spring的配置
- 在autocinfigure项目中有
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration
- 注册的
LocalValidatorFactoryBean是Validator的关键
- 还注册了
MethodValidationPostProcessor
org.springframework.validation.beanvalidation.LocalValidatorFactoryBean#afterPropertiesSet
- 使用
javax.validation.Configuration进行配置javax.validation.ValidatorFactory创建Validator
ConstraintValidatorFactory:SpringConstraintValidatorFactory
- 因此创建Validator对象时会使用spring进行创建,可以注入依赖
- 先注入依赖,再调用initialize
组
- docs.jboss.org/hibernate/v…
- 默认都在javax.validation.groups.Default,即默认校验Default组
- 可以认为组就是一个类(一般是一个接口)
- 功能
- 给约束分配组:如
@NotEmpty(groups = Default.class)
- 让多组约束按指定组顺序执行校验:
- 方法就是合并为一个组,在这个组指定这多组的顺序
- 使用javax.validation.GroupSequence指定组顺序
- 防止循环:即合并组是多组中某个组的父级
- @GroupSequence注解在bean类上:定义默认组,默认组顺序
- 注意:必须附带上默认组,但不是Default类而是bean类
- @ConvertGroup:定义级联校验的默认组
- 最外层校验需要手动传入组(但是像Spring要使用Spring的@Validated才能获取组),但是可以指定级联校验的默认组
- 比如Bean1的属性Bean2,给Bean2注解@ConvertGroup就可以指定Bean2要校验什么组
- 因为是默认组,因此如果validator有传入参数组,还是会使用传入的
- 方法参数取巧指定默认组:在参数bean上注解@Valid+@ConvertGroup也可以传入指定的组(解决了像Spring的@Validated才能传入组的不便)