Springboot HibernateValidator 中自定义检查器ConstraintValidator Autowire注入服务失败的问题原因和解决

问题背景

服务原来自定义了Validation检查器,

自定义的ConstraintValidator可能的写法举例:

public class ProjectAuth implements ConstraintValidator<ProjectAuth, Object> {

    @Autowired
    private ProjectService projectService;

    @Override
    public void initialize(ProjectAuth project) {
        // ...省略若干
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // ...省略若干
        return true;
    }

看上去一切正常,但是后来引入了HibernateValidator,上述代码中Autowired注入的服务是null,也就是说服务没有注入。

@Autowired
private ProjectService projectService;

解决问题

解决问题的第一直觉是,增加@Component,这样spring可能有自动注入机制注入ProjectAuth,然后顺便注入依赖。

@Component
public class ProjectAuth implements ConstraintValidator<ProjectAuth, Object>

因为服务注入失败,所以第一直觉是要加@Component 或者 @Service
,但是试验过之后并没有解决,注入依然是null,这时候问题引起了我的兴趣。

之后搜索了很多中文的博客...(没有找到解决的方法,用法都差不多)
关键是找到stackoverflow上的类似问题
Spring Boot - Hibernate custom constraint doesn't inject Service

无法访问stackOverFlow以下附上了问题的解决:
To have the Spring beans injected into your ConstraintValidator, you need a specific ConstraintValidatorFactory which should be passed at the initialization of the ValidatorFactory
意思是:注入服务到我们自己的ConstraintValidator,需要在ValidatorFactory初始化的时候指定特定的ConstraintValidatorFactory,但是这里没有给出特定的Factory。

为了节省大家的时间,我这里先附上问题的解决方式,就是在配置的时候加上如下配置。原因我在后续分析,有时间的朋友可以继续读完全文。

解决方式是添加以下配置:

validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory)) # 解决问题的关键
                .failFast(true)
                .buildValidatorFactory();

原因分析

看到了问题的答案,接下来分析以下原因。
(了解以下Validation背景,SPI,Springboot Bean加载方式会加深对于原因的理解)

背景 Validation介绍

JSR是Java Specification Requests的缩写,意思是Java 规范提案。关于数据校验这块,最新的是JSR380,也就是我们常说的Bean Validation 2.0。

Bean Validation 2.0 是JSR第380号标准。该标准连接如下:

Bean Validation是一个通过配置注解来验证参数的框架,它包含两部分Bean Validation API(规范)和Hibernate Validator(实现)

参考:深入了解数据校验:Java Bean Validation 2.0 JSR303、JSR349、JSR380 Hibernate-Validation 6.x使用案例

configure()

回到上述问题解决的配置,我们看到前两行

validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()

第一行就是加载了provider
核心的源码是:

// validationProviderClass是入参,就是HibernateValidator.class
this.validationProviderClass = validationProviderClass;

第二行.configure()
核心源码是:

// if no resolver is given, simply instantiate the given provider
            if ( resolver == null ) {
                U provider = run( NewProviderInstance.action( validationProviderClass ) );
                return provider.createSpecializedConfiguration( state );
            }

简化一下理解:
run( NewProviderInstance.action( validationProviderClass ) )
这里结合第一行,等价于 new HibernateValidator();

provider.createSpecializedConfiguration的核心源码如下

@Override
    public HibernateValidatorConfiguration createSpecializedConfiguration(BootstrapState state) {
        return HibernateValidatorConfiguration.class.cast( new ConfigurationImpl( this ) );
    }

返回HibernateValidatorConfiguration, 通过cast做强制类型转换。
这几个Configuration类的继承关系如下:


Configuration类的继承关系

ConfigurationImpl其中支持了Validation的各项配置,
配置的面向接口编程得到实现。

配置中和ConstraintValidator相关的几个关键代码:

this.defaultConstraintValidatorFactory = new ConstraintValidatorFactoryImpl();

public final void setConstraintValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory) {
        this.constraintValidatorFactory = constraintValidatorFactory;
    }

也就是说如果没有定义,就会通过new的方式创建一个。
也可以通过set的方式设置一个。

目前我用的版本中ConstraintValidatorFactory只有两个实现:
Spring 和 hibernate的实现,而new的方式创建的就是hibernate的实现


ConstraintValidatorFactory的实现类

constraintValidatorFactory实现

constraintValidatorFactory是加载自定义约束的工厂,前面已经介绍了,目前的配置只有两个实现Spring 和 hibernate,hibernate的实现是默认的配置。
这两个实现的不同:
先看hibernate的ConstraintValidatorFactoryImpl

public class ConstraintValidatorFactoryImpl implements ConstraintValidatorFactory {

    @Override
    public final <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        return run( NewInstance.action( key, "ConstraintValidator" ) );
    }

    @Override
    public void releaseInstance(ConstraintValidator<?, ?> instance) {
        // noop
    }

    private <T> T run(PrivilegedAction<T> action) {
        return System.getSecurityManager() != null ? AccessController.doPrivileged( action ) : action.run();
    }
}

return run( NewInstance.action( key, "ConstraintValidator" ) );这个代码内部等同于
clazz.newInstance();也就是key.newInstance(),注入的key就是用户自定义的ConstraintValidator,我们这个文章的例子相当于 new ProjectAuth();
到这里大家应该理解了为什么注入的服务会是null。

再看Spring中的实现:

public class SpringConstraintValidatorFactory implements ConstraintValidatorFactory {

    private final AutowireCapableBeanFactory beanFactory;


    /**
     * Create a new SpringConstraintValidatorFactory for the given BeanFactory.
     * @param beanFactory the target BeanFactory
     */
    public SpringConstraintValidatorFactory(AutowireCapableBeanFactory beanFactory) {
        Assert.notNull(beanFactory, "BeanFactory must not be null");
        this.beanFactory = beanFactory;
    }


    @Override
    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        return this.beanFactory.createBean(key);
    }

    // Bean Validation 1.1 releaseInstance method
    public void releaseInstance(ConstraintValidator<?, ?> instance) {
        this.beanFactory.destroyBean(instance);
    }

}

获得实例的方式是:
return this.beanFactory.createBean(key);
通过这个方式获得ProjectAuth的实例是可以注入bean的依赖的(SpringBeanFactory会帮我们完成依赖的注入,原因不在这里介绍了。)

前面所说的,configure()的返回对象支持配置ValidatorFactory,接口是setConstraintValidatorFactory。所以解决这个问题的方式,就是把SpringConstraintValidatorFactory通过配置设置进去即可。
至此,问题解决了,解决问题的原因也从源码上得到了解答。

一些心得

  • 解决问题方式网上有很多教程,但是读到了源码才知道所以然,否则就需要一遍遍按照网上的教程试错,非常浪费时间,积累读源码的能力是技术的必修课。
  • 另外读这个源码中,ValidationProvider的提供由META-INF/services/javax.validation.spi.ValidationProvider这个配置文件来表示。这是Java内置的SPI机制,java的服务注入常见的两个方式一个是ClassLoader(java/lang这个包下,系统接口通用性强),一个是ServiceLoader,spi的实现(java/util中的类,使用友好,但是注入的server要符合一定规范),更多细节这里不展开了。
  • 领域建模的模型,应该包含数据的校验,领域建模的背景知识可以参考//www.greatytc.com/p/712a49baf468
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345