基于Spring框架由ConditionalOnMissingBean注解引发的问题
问题描述
最新负责的工程在做dubbo配置disconf静态配置托管优化,由一个StaticConfigPropertiesFactoryBean来读取静态配置,它是PropertiesFactoryBean的一个子类,读取到的所有配置放在一个Properties中。
dubbo配置直接引用这个Properties的值。
像下面这样,dubbo接口的group通过disconf的静态配置项dubbo.hst.pay.group定义:
工程使用Spring Boot框架,Spring版本号4.1.1,Spring Boot版本号1.1.9,spring-security版本号4.0.2,启用了Spring Boot框架的Anto Configuration特性。
StaticConfigPropertiesFactoryBean这个bean的configSrc属性值是个占位符,值在外部配置文件定义。
但是启动工程时出现了一个问题,抛出了一个IllegalArgumentException异常,异常日志如下:
Caused by: java.lang.IllegalArgumentException: configSrc error : ${disconf.configSrc}
at com.baidu.disconf.client.haitao.properties.StaticConfigPropertiesFactoryBean.createProperties(StaticConfigPropertiesFactoryBean.java:65)
at org.springframework.beans.factory.config.PropertiesFactoryBean.afterPropertiesSet(PropertiesFactoryBean.java:71)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
... 35 more
排查过程
从日志中看到configSrc这个属性的值不满足条件导致抛出了这个异常,而且值是${disconf.configSrc},这里比较奇怪,在初始化bean的时候框架不是会对占位符进行解析求职处理的么,什么原因导致占位符未被处理bean就开始初始化了?
我们知道Spring框架由PropertySourcesPlaceholderConfigurer来处理占位符,PropertySourcesPlaceholderConfigurer是一个BeanFactoryPostProcessor,在容器所有BeanDefinition被加载之后,bean初始化之前,BeanFactoryPostProcessor会被激活,PropertySourcesPlaceholderConfigurer会加载所有被引入的属性文件,并且查找占位符对应的值,并把对应的值设置到bean属性对应的PropertyValue中,这样在bean初始化时设置每个属性的值时设置的就是处理后的值了。
org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory, List)
就是这在触发这个BeanFactoryPostProcessor的,注意代码行数是162行。
但是现在的现象是configSrc这个属性初始化之后却是未处理的占位符字符串。
分析原因有两种可能,第一种disconf.configSrc这个配置项在文件中未定义或未被容器加载,第二种StaticConfigPropertiesFactoryBean在属性占位符处理之前就初始化了。
第一种原因很容易就排除了,disconf.configSrc这个配置项已经定义了,在这次修改之前就已存在之前一直都是没问题的,且由于ignoreUnresolvablePlaceholders这个设置默认是false,就算它未被定义容器也会提前抛出IllegalArgumentException异常:
org.springframework.util.PropertyPlaceholderHelper.parseStringValue(String, PlaceholderResolver, Set)
所以只能是第二种原因了,那就是StaticConfigPropertiesFactoryBean在属性占位符处理之前就被容器初始化了。Spring框架如此庞大,要找原因最有效的手段就是debug了,把断点打到StaticConfigPropertiesFactoryBean初始化代码处,看它是从哪进来的:
org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory, List)
通过debug发现,这个bean的初始化也是从BeanFactoryPostProcessor调进去的,而且看下代码是94行,在占位符处理的164行前面,好了,这里可以解释configSrc值为什么是错的。
但是这些BeanFactoryPostProcessor是干嘛的?为什么在其它BFPP激活之前就去初始化bean了。
看一下这个BFPP是什么:
这个BFPP是ConfigurationClassPostProcessor,当应用使用了annotation-config或component-scan扫描注解bean时,容器会自动注册一个ConfigurationClassPostProcessor来加载注解定义的BeanDefinition,按常理,这个BFPP也只会生成BeanDefinition,不会执行bean初始化动作。
那么是什么地方导致bean发生初始化了呢?
再看下上面哪个调用栈,在OnMissingBeanCondition.matches之后就调用BeanFactory的getBeansXXX了,然后一步一步触发了createBean。
那这个OnMissingBeanCondition从哪来的呢?
从debug的情况来看跟WebMvcSecurityConfiguration有关。
看下WebMvcSecurityConfiguration这个类,它有一个@ConditionalOnMissingBean注解,这个注解作用在bean定义上,它的作用就是在容器加载它作用的bean时,检查容器中是否存在目标类型(ConditionalOnMissingBean注解的value值)的bean了,如果存在这跳过原始bean的BeanDefinition加载动作。
上面说的那段逻辑就是由OnMissingBeanCondition.matches来完成的。
也就是说上面那整棵调用树所做的事情就是检查容器知否已存在RequestDataValueProcessor类型的bean,如果存在则不再重复重新加载这个RequestDataValueProcessor,如果是普通的bean那还好办直接比较bean的类型是否是RequestDataValueProcessor类型就行了,不必去初始化整个bean,但是如果是FactoryBean那就坏事了,因为FactoryBean类型的bean最终创建的bean的类型并不是FactoryBean本身的类型,而是由它的getObject返回值来决定的,所以要拿到bean类型,会调它的getObject方法创建bean之后再比较类型。
而dubbo消费者bean的类型都是com.alibaba.dubbo.config.spring.ReferenceBean,这恰恰是一个FactoryBean,此时会初始化消费者bean,设置group属性时接着初始化引用的的disconfPropertiesReader bean,所以就导致了悲剧。
org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch
最后只剩下最后一个问题,WebMvcSecurityConfiguration是从哪来的,搜索代码,直接宣布结果:
由于工程使用了AutoConfiguration特性,框架查找并读取所有名称是*AutoConfiguration的类,所以会找到SecurityAutoConfiguration。
这个类定义了@Configuration注解,所以会加载这个类中定义的bean,发现了@Import注解,根据这个标签读取到并解析SpringBootWebSecurityConfiguration,继续读取到它的内部类WebMvcSecurityConfigurationConditions,再读取到内部类的内部类DefaultWebMvcSecurityConfiguration,这个内部类上有@EnableWebMvcSecurity注解,这个注解又@Import了WebMvcSecurityConfiguration,这个类有@EnableWebSecurity注解,注解@Import了SpringWebMvcImportSelector,这个ImportSelector会导入org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration:
总结产生问题的最终原因:
是因为工程使用了Spring Boot的Auto Configuration功能,而根据这个工程的特点会自动加载安全配置SecurityAutoConfiguration,由此经过一系列的直接或间接的import,会导致框架读取WebMvcSecurityConfiguration,而这个配置在加载RequestDataValueProcessor这个bean定义的时候由于有@ConditionalOnMissingBean的作用导致框架会检查容器中是否已经存在了RequestDataValueProcessor类型的bean实例,在检查的过程中当扫描到sendCouponRemoteApiImpl这个dubbo消费者bean时,由于它是一个FactoryBean,getObject方法会被调用进而执行了bean的初始化动作,而由于此时由于处理属性占位符的PropertySourcesPlaceholderConfigurer还未被激活,所以导致bean的属性值没有被正确初始化。
解决方案
治标的方案,考虑到工程主要是提供dubbo服务无需启用安全配置,可以禁调安全配置的自动加载,@EnableAutoConfiguration注解有exclude属性,通过它可以禁用对SecurityAutoConfiguration的自动加载:
治本的方案,spring社区也有人提出了相同的问题:https://jira.spring.io/browse/SEC-3063,已确定是spring的bug,官方已在spring-security-config的4.1.0版本解决了这个问题,删掉了
ConditionalOnMissingBean这个注解,看代码WebMvcSecurityConfiguration也不再使用这个注解了,升级spring-security-config到4.1.0解决问题:
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
最新评论