问题背景
服务原来自定义了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类的继承关系如下:
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是加载自定义约束的工厂,前面已经介绍了,目前的配置只有两个实现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