重新定义默认分组
@GroupSequence注解除了可以标注接口,还可以标注在类上,这样@GroupSequence指定的组就构成了约束所属的默认分组。此时如果想校验未指定分组的约束,需要添加当前类到@GroupSequence.value中。自定义默认分组仅在当前类生效,不会传递到级联的其他类中。
示例如下:
public class BeanValidatorTest { @GroupSequence({CustomGroup.class, Bean.class}) static class Bean { @NotNull(message = "约束属于Default分组") private String name; @NotNull(groups = CustomGroup.class, message = "约束属于CustomGroup分组") private Integer sex; } interface CustomGroup { } public static void main(String[] args) { Bean bean = new Bean(); Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); Set<ConstraintViolation<Bean>> violationSet = validator.validate(bean); violationSet.forEach(System.out::println); } }
打印结果如下:
ConstraintViolationImpl{interpolatedMessage='约束属于CustomGroup分组', propertyPath=sex, rootBeanClass=class com.zzuhkp.validator.BeanValidatorTest$Bean, messageTemplate='约束属于CustomGroup分组'}
校验时未指定分组,因为CustomGroup在@GroupSequence.value值的前面,因此先校验分组CustomGroup。
隐式分组
接口中Default分组的约束属于该接口分组。
示例如下:
interface Super { @NotNull(message = "约束属于Default/Super分组") String getName(); } static class Bean implements Super { @NotNull(message = "约束属于Default分组") private Integer sex; @Override public String getName() { return null; } }
校验时分组为默认分组会同时校验接口中的getName方法和实现类中的sex成员变量,如果分组指定为Super.class,则仅校验接口中的getName方法。
约束校验顺序
对于给定的分组列表,前面的分组校验全部通过才会校验后面的分组。
约束校验顺序整体上并不固定,首先通过@Constraint.validateBy确定ConstraintValidator,然后调用isValid方法,如果校验通过则继续后面的约束校验,如果校验不通过则会把校验错误信息添加到ConstraintViolation对象。
能够确定的是按照不同约束类型的顺序进行校验,具体如下:
除非给定的约束已经在前面的分组中被处理,否则对匹配目标分组的可达字段(包括父类)执行字段级别的校验。
除非给定的约束已经在前面的分组中被处理,否则对匹配目标分组的可达get方法(包括接口和父类)执行get方法级别的校验。
除非给定的约束已经在前面的分组中被处理,否则执行类级别(包含父类和接口)的校验。
对于所有可达并且可级联的约束(包含接口和父类,如使用@Valid注解字段)进行校验。对于可级联的约束的校验,通过递归进行实现,为了避免无限循环,如果之前已经对关联的对象进行校验,则验证程序会忽略级联操作。
校验API
Validator
Validator是校验bean实例主要的API,实现必须是线程安全的,推荐ValidatorFactory对Validator进行缓存。
Validator的源码如下:
public interface Validator { /** * 校验对象上的所有约束 */ <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups); /** * 校验给定对象的属性的所有约束,属性是JavaBean的属性名称,忽略@Valid */ <T> Set<ConstraintViolation<T>> validateProperty(T object,String propertyName,Class<?>... groups); /** * 校验给定类型的属性的值,属性是JavaBean的属性名称,属性可以属于给定的bean类型或者其父类,忽略@Valid */ <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType,String propertyName,Object value,Class<?>... groups); /** * 获取给定java bean的约束描述对象,注意返回值是不可变的 */ BeanDescriptor getConstraintsForClass(Class<?> clazz); /** * 返回可以通过provider特定API访问的给定类型的实例 */ <T> T unwrap(Class<T> type); /** * 返回校验方法和构造函数的参数和返回值的对象 */ ExecutableValidator forExecutables(); }
ConstraintViolation
ConstraintViolation是描述单个约束错误的类,使用Validator进行校验会返回ConstraintViolation的集合。
ConstraintViolation源码如下:
public interface ConstraintViolation<T> { /** * 约束的错误信息 */ String getMessage(); /** * 约束的错误信息模板,即约束注解message属性指定的值 */ String getMessageTemplate(); /** * 正在进行校验的根对象 */ T getRootBean(); /** * 正在校验的根对象的类,对于方法是正在校验的对象的类,对于构造器是定义所在的类 */ Class<T> getRootBeanClass(); /** * 校验对象时返回校验的对象 * 校属性时返回属性所属的对象 * 校验给定的属性值时返回null * 校验方法参数或者返回值时返回执行方法的对象 * 校验构造器参数时返回null * 校验构造器返回值时返回构造器创建的对象 */ Object getLeafBean(); /** * 如果当前对象是验证构造器或方法的参数被返回的,则返回构造器或者方法的参数数组 */ Object[] getExecutableParameters(); /** * 如果当前对象是验证构造器或方法的参数被返回的,则返回构造器或者方法的返回值 */ Object getExecutableReturnValue(); /** * 返回从根对象到当前校验值的属性路径 */ Path getPropertyPath(); /** * 返回未通过校验的值 */ Object getInvalidValue(); /** * 返回约束描述信息 */ ConstraintDescriptor<?> getConstraintDescriptor(); /** * 返回可以通过provider特定API访问的给定类型的实例 */ <U> U unwrap(Class<U> type); }
MessageInterpolator
MessageInterpolator可以将约束中的错误信息模板转换为可读的信息,直接接触它的场景应该并不多,简单了解即可。
信息参数是用{ }包起来的字符串,可用\进行转义。
默认MessageInterpolator
默认的MessageInterpolator转换步骤如下:
从信息模板中提取参数,从给定语言环境中获取名称为ValidationMessages的ResourceBundle,如果找到参数对应的值则替换参数,直到无法替换。
从默认的ResourceBundle中查找参数对应的值替换参数,和步骤1不同的是不会进行递归操作。
如果步骤2触发替换,则再次执行步骤1的流程,否则执行步骤4。
将参数对应的值替换参数。
自定义MessageInterpolator
自定义MessageInterpolator实现接口MessageInterpolator即可,实现必须是线程安全的,推荐将实现委托为默认的MessageInterpolator。
MessageInterpolator源码如下:
public interface MessageInterpolator { /** * 根据实现指定的默认语言信息转换信息模板 */ String interpolate(String messageTemplate, Context context); /** * 使用指定的语音信息转换信息模板 * @param messageTemplate 约束上给定的信息模板 * @param context 上下文信息 * @param locale 语言环境 */ String interpolate(String messageTemplate, Context context, Locale locale); /** * 上下文信息 */ interface Context { /** * 获取约束描述信息 */ ConstraintDescriptor<?> getConstraintDescriptor(); /** * 获取被校验的对象 */ Object getValidatedValue(); /** * 返回可以通过provider特定API访问的给定类型的实例 */ <T> T unwrap(Class<T> type); } }
设置MessageInterpolator的方式如下:
通过javax.validation.Configuration.messageInterpolator指定MessageInterpolator。
通过javax.validation.ValidatorContext.messageInterpolator指定MessageInterpolator。
示例源码如下:
public class BeanValidatorTest { static class CustomMessageInterpolator implements MessageInterpolator { private MessageInterpolator delegate; public CustomMessageInterpolator(MessageInterpolator delegate) { this.delegate = delegate; } @Override public String interpolate(String messageTemplate, Context context) { return delegate.interpolate(messageTemplate, context); } @Override public String interpolate(String messageTemplate, Context context, Locale locale) { return delegate.interpolate(messageTemplate, context, locale); } } public static void main(String[] args) { Configuration<?> configuration = Validation.byDefaultProvider().configure(); CustomMessageInterpolator interpolator = new CustomMessageInterpolator( configuration.getDefaultMessageInterpolator()); //1、通过javax.validation.Configuration.messageInterpolator指定MessageInterpolator ValidatorFactory factory = configuration.messageInterpolator(interpolator).buildValidatorFactory(); //2、通过javax.validation.ValidatorContext.messageInterpolator指定MessageInterpolator Validator validator = factory.usingContext().messageInterpolator(interpolator).getValidator(); } }
Bootstrapping API 概览
Validator:用于校验bean,实现必须是线程安全的。
ValidatorFactory: 创建并初始化Validator,使用者可缓存ValidatorFactory,ValidatorFactory可以缓存Validator。
ValidatorContext:创建Validator的上下文,可通过ValidatorFactory#usingContext方法获取,覆盖ValidatorFactory中的配置。
Configuration:接收配置信息,创建ValidatorFactory。
配置信息:
MessageInterpolator:将约束中的模板信息解析为实际的错误信息。
TraversableResolver:确定要校验的对象是否可达或可级联,实现必须是线程安全的。
ConstraintValidatorFactory:用于获取ConstraintValidator。
ParameterNameProvider:提供构造器或普通方法的参数名称列表。
ClockProvider:校验约束@Future和@Past时获取Clock判断当前时间。
ValidationProvider:SPI接口,创建通用的和特定的Configuration,构建ValidatorFactory。
ValidationProviderResolver:用于获取ValidationProviderResolver列表,默认实现是通过SPI机制获取。
Validation:bean validation的入口,用于获取ValidatorFactory。
GenericBootstrap:创建和ValidationProvider无关的Configuration。
ProviderSpecificBootstrap:创建和ValidationProvider自定义的Configuration。
各API之间的关系可参考如下示例代码:
// GenericBootstrap bootstrap = Validation.byDefaultProvider(); ProviderSpecificBootstrap<CustomConfiguration> bootstrap = Validation .byProvider(CustomValidationProvider.class); Configuration<?> configuration = bootstrap.providerResolver(validationProviderResolver).configure(); ValidatorFactory factory = configuration .messageInterpolator(messageInterpolator) .constraintValidatorFactory(constraintValidatorFactory) .traversableResolver(traversableResolver) .buildValidatorFactory(); Validator validator = factory.usingContext() .messageInterpolator(messageInterpolator) .constraintValidatorFactory(constraintValidatorFactory) .traversableResolver(traversableResolver) .getValidator(); validator = factory.getValidator();
问题解答
1、ValidatorFactory是否缓存了Validator?
开篇提出了Validator是否缓存的问题,在此加以解答。
通过前面章节的了解,我们知道Java Bean Validation 是一种规范,它只是描述了ValidatorFactory可以缓存Validator,事实上是否缓存还是依赖于具体的实现。
跟踪 Hibernate Validator 的源码,ValidatorFactory#getValidator方法最终会调用org.hibernate.validator.internal.engine.ValidatorFactoryImpl#createValidator,该方法并未缓存Validator,但是缓存了创建Validator实现所需的元数据,因此创建Validator是相对轻量级的。如果不需要对ValidatorFactory做特殊的配置,还是建议在应用中缓存ValidatorFactory创建好的Validator。
2、为什么只需要引入Hibernate Validator,没有明确使用Hibernate Validator 也可以进行校验?
java中存在一种称为SPI的机制,可以从类路径中获取特定接口的实现,具体可参见前面的文章Java基础知识之SPI。
Validation.byDefaultProvider().configure()返回Configuration对象,#configure实现先获取ValidationProvider,然后通过ValidationProvider#createGenericConfiguration方法获取Configuration,获取ValidationProvider的过程便调用了方法GetValidationProviderListAction#loadProviders,该方法使用SPI机制加载类路径中/META-INF/javax.validation.spi.ValidationProvider文件中定义的ValidationProvider实现。Hibernate Validator 中定义了该文件,因此最终可以获取到Hibernate Validator 的实现。
3、踩坑记录
我们的项目之前未使用maven管理依赖,改为maven管理依赖使用hibernate validator 后服务无响应,无异常日志打印。通过排查发现产生依赖冲突,导致hibernate validator使用了一个较低版本的log4j,缺少log4j类中的一个字段,只有捕获Throwable才会打印日志。最终手动引入了一个较高版本的log4j解决。
总结
本篇先引入使用 bean validation 的问题,然后对 bean validation 中的约束、分组及相关API进行介绍,最后解答前面提出的问题。