新版本hibernate-validator7.x不生效问题

如果使用spring-boot-stater-validation,默认会集成hibernate-validator 6.x版本,如果想使用hibernate-validtor 7.x而java EE 在2018年命名空间全面迁移,由javax--> jakarta 则validation的api也由 javax-validation 迁移到了jakarta.validation,导致了失效。

可以看最新版本hibernate-validator 7.x的集成环境
7.0 series - Hibernate Validator

降低版本方式解决

直接使用spring-boot-stater-validation,默认hibernate-validator 6.x版本和
javax.validation-api 的版本 或者显示的使用下面maven的版本
jakarta.validation-api 2.x版本(也可以使用javax.validation-api因为2.x版本做了横向迁移)
如下图。spring-boot 新增独立依赖spring-boot-starter-parent或者 spring-boot-dependencies 的版本所以无需添加版本

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

如果不使用spring-boot-starter如下图

        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.2.3.Final</version>
        </dependency>

自定义初始化spring的Validator适配器

如果就想使用hibernate-validator7.x版本,可以使用自定义初始化逻辑,下面我们看看源码,可以先参考之前的源码解读javax.validation.constraints 及spring的整合,hibernate-validator的实现 - 简书 (jianshu.com)
前面的源码可以看到 spring和validator的集成通过一个适配器,首先通过参数解析器然后通过 dataBinder来调用校验,spring自己定义了org.springframework.validation.validator作为适配器接口,由SpringValidatorAdapter同时实现spring的适配器和被适配的javax.validation.Validator(新版本变成了jakarta.validation.Validator)那么我们进入源码也能看到spring的适配器还是实现的旧的javax.validation.Validator

image.png

可以看到全部使用的旧的命名空间,那么我们可不可以自己初始化这个适配器呢。继续看看源码。我们找到了最下面的一个子类,这个子类被spring注入了适配器接口 org.springframework.validation.validator

public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean {

    @Override
    public void afterPropertiesSet() {
        try {
            super.afterPropertiesSet();
        }
        catch (ValidationException ex) {
            LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
        }
    }

}

WebMvcConfigurationSupport 类进行注入适配器作为springmvc的controller参数校验

    @Bean
    public Validator mvcValidator() {
// 这里是一个模版方法,可以由子类覆盖,但是下面又做了类命名限定检查,只能使用旧版本api,所以这个方法覆盖并不能实现我们想要的
        Validator validator = getValidator();
        if (validator == null) {
// 这里看到指向的还是旧版本的命名空间
            if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
                Class<?> clazz;
                try {
                    String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
                    clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
                }
                catch (ClassNotFoundException | LinkageError ex) {
                    throw new BeanInitializationException("Failed to resolve default validator class", ex);
                }
                validator = (Validator) BeanUtils.instantiateClass(clazz);
            }
            else {
// 如果没有找到旧的命名空间就返回空适配器的实现,这个适配器并没有实现javax.validation.Validator来做适配
                validator = new NoOpValidator();
            }
        }
        return validator;
    }

我们可以在上面代码来自己初始化适配器,然后看到SpringValidatorAdapter的代码全部是引用的旧命名空间,new的话一定会NoClassDefFoundError的,spring为了支持多语言实际初始化的是OptionalValidatorFactoryBean,那么我们可以直将LocalValidatorFactoryBean和SpringValidatorAdapter和OptionalValidatorFactoryBean的代码复制出来,然后将所有命名空间替换即可.
要复制的类及功能如下
1.LocaleContextMessageInterpolator 实现MessageInterpolator接口对校验不通过的提示信息的实现

  1. SpringValidatorAdaptor 实现spring的适配器接口,及真正做校验的jakarta.validation.Validator接口,适配器的具体实现,做校验工作
  2. LocalValidatorFactoryBean 作为SpringValidatorAdaptor 适配器的子类用来整合spring的上下文,对当前适配器bean的管理及提示信息解析
  3. OptionalValidatorFactoryBean 适配器的真正实例化的子类
  4. SpringConstraintValidatorFactory 校验器工厂,例如@NotNull注解对应一个专门的非空判断的校验器,我们也可以自定义校验注解,实现的就是这个工厂实例化出来的。具体可以看代码ConstraintValidator<A extends Annotation, T>

我们将上面所有的类复制一份出来修改命名空间然后注入即可

    @Override
    @Bean
    public Validator mvcValidator() {
        return new CustomOptionalValidatorFactoryBean();
    }

启动之后还是不好用,我们通过debug初始化Validator的过程,发现报错

public class CustomOptionalValidatorFactoryBean extends CustomLocalValidatorFactoryBean {

    @Override
    public void afterPropertiesSet() {
        try {
            super.afterPropertiesSet();
        }
        catch (ValidationException ex) {
// 这里报错打印日志,发现
            ex.printStackTrace();
            LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
        }
    }

}

但是启动程序还是不好用我们debug发现还是没有注入Validator,这里debug方式就不写了,我们直接在上面代码初始化时发现有报错

jakarta.validation.ValidationException: HV000183: Unable to initialize 'jakarta.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead
    at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.buildExpressionFactory(ResourceBundleMessageInterpolator.java:211)
    at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.<init>(ResourceBundleMessageInterpolator.java:97)
    at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.getDefaultMessageInterpolator(AbstractConfigurationImpl.java:574)
    at io.g.erha.test.CustomLocalValidatorFactoryBean.afterPropertiesSet(CustomLocalValidatorFactoryBean.java:265)

发现是jakarta.el 包没发现,突然想起来最新的整合还需要依赖新得jakarta.el版本,新得版本el包抽出来了我们添加maven配置

      <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>jakarta.el</artifactId>
            <version>4.0.2</version>
        </dependency>

然后重新编译启动 。看见可以了

以上套路最好不要使用,就当是提供了另一种角度看spring和validator的整合逻辑,首先hibernate-validator是javaEE的javax.validation标准的实现,那么javaEE在2018年更改了命名空间后hibernate-validtor随后推出了7.x版本支持,但是spring-boot-starter一直使用旧的版本,我们可以从hibernate-validator的发布信息看到基本做了很少的改动,修复了少量bug,如果您就是遇到了7.x版本修复的bug的使用场景可以考虑使用上述方式提升版本,而通过上述的代码复制我们也知道了spring通过适配器方式整合validator结合springmvc进行参数校验,并提供了Spring上下文支持的javax.validation(jakarta.validation)的自定义校验器的工厂实现,因为我们默认注入的Validator都是spring的适配器,那么这个spring上下文的支持就是我们在自定义实现了ConstraintValidator<A extends Annotation, T> 接口通过自定义校验注解校验时,虽然这个校验器子类是每次实例化但是spring也会帮你@Autowired 和@Resource注入

最后我们看看spring官方也解释了为什么不集成新的命名空间版本,及未来会在今年的spring-boot 3.x 和 spring 6.x版本全面支持! 大家尽量等等喽

支持 Jakarta EE 9(jakarta.* 命名空间中的注释和接口) ·问题 #25354 ·弹簧项目/弹簧框架 (github.com)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350