Java Bean Validation 详解(下)

简介: 前言最近维护一个老项目,项目使用最原始的Servlet,项目中充斥着各种类似判空的简单校验,为了减少重复代码,因此需要手动引入 Java 的 Bean Validation。

重新定义默认分组

@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进行介绍,最后解答前面提出的问题。


目录
相关文章
|
6月前
|
搜索推荐 Java 开发者
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException 问题处理
【5月更文挑战第14天】org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException 问题处理
641 1
|
3月前
|
Java Spring 容器
Java SpringBoot 中,动态执行 bean 对象中的方法
Java SpringBoot 中,动态执行 bean 对象中的方法
41 0
|
3月前
|
Java Spring
Java SpringBoot Bean InitializingBean 项目初始化
Java SpringBoot Bean InitializingBean 项目初始化
55 0
|
4月前
|
存储 前端开发 Java
Java中的不同Bean作用域
【7月更文挑战第5天】
59 0
Java中的不同Bean作用域
|
5月前
|
Java 持续交付 Maven
Java报错:Missing ServletWebServerFactory bean,如何解决
Java开发中遇到`Missing ServletWebServerFactory bean`错误?该问题可能由依赖冲突、配置问题或环境不一致引起。解决方法包括:检查依赖版本一致性、修复配置错误、确保环境匹配,以及查看IDE中的JRE配置。预防这类问题,可采用版本管理工具、CI/CD流程、代码审查和社区学习。木头左提醒,记得点赞和分享,下次见!
Java报错:Missing ServletWebServerFactory bean,如何解决
|
5月前
|
Java API 数据处理
Java Bean参数验证:深入探索javax.validation.constraints注解
Java Bean参数验证:深入探索javax.validation.constraints注解
206 0
|
6月前
|
消息中间件 安全 Java
在Spring Bean中,如何通过Java配置类定义Bean?
【4月更文挑战第30天】在Spring Bean中,如何通过Java配置类定义Bean?
99 1
|
6月前
|
Java 测试技术 Spring
|
6月前
|
XML Java 程序员
作为Java程序员还不知道Spring中Bean创建过程和作用?
作为Java程序员还不知道Spring中Bean创建过程和作用?
46 0
|
6月前
|
XML 缓存 Java
Java常见bean 工具类性能对比
Java常见bean 工具类性能对比
99 0
下一篇
无影云桌面