主题
最近学习了spring相关知识.公司项目也用到了spring..偶然一次版本中发现,本地启动项目没问题,服务器上启动报bean创建异常.
于是研究了一下,对spring有了更深的理解..也记录一下问题原因...
异常
大致错误如下(我本地模拟了一下.原理一样)
1 17:19:02.056 [main] WARN org.springframework.context.support.ClassPathXmlApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'beanA': Bean with name 'beanA' has been injected into other beans [beanB] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example. 2 Exception in thread "main" org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'beanA': Bean with name 'beanA' has been injected into other beans [beanB] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example. 3 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:624) 4 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) 5 at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) 6 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) 7 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321) 8 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) 9 at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:879) 10 at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878) 11 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) 12 at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:144) 13 at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:85) 14 at com.labofjet.sh1.sp.a.BeanA.main(BeanA.java:42)
大致的意思是 创建beanA失败, A的raw(原始)版本被注入了beanB中,a,b 2个bean循环依赖. 但是A最终 been wrapped,就是被代理了.所以BeanB中注入的A(raw)与最终的A(wrapped)是不一致的.所以报了异常.
排查
首先,这次发布,我们没有新增新的service bean..就是加了几个方法..所以如果是循环依赖.那之前的版本就会报错...
排除了循环依赖导致(其实这个也是其中的一个原因)..以后..我找了找这次有没有什么特殊的操作..
发现了这次版本使用了注解@Async.这个注解标记的方法会异步执行.
但是这个注解之前我们项目里其他地方也用到过..之前没遇到过问题...注释掉以后项目确实可以启动...
反复尝试以后确定原因为:
1.bean循环依赖
2.@Async注解
原理
记录一下为什么这2个操作组合在一起会有问题.(还需要特定的Bean的加载顺序)
AsyncAnnotationBeanPostProcessor
@Async这个注解功能是依赖这个Async的BPP来实现的.
原理在于他实现了BeanPostProcessor接口,在bean属性设置以后会有机代理你的bean.返回proxy.
AOP原理就不仔细讲了.可以参考我之前的文章.
大致就是:
1.代理bean,封装advisor
2.调用连接点的方法的前后会调用advisor的advise(interceptor的方法).这里就是AsyncExecutionInterceptor
3.AsyncExecutionInterceptor里找到你@Async注解里定义的executor,提交任务达到异步调用的目的.
加载顺序问题
明白了async的原理以后.我们来研究下为什么会出现问题.
当ABA问题出现的时候,如果加载bean的顺序为先加载A.那spring里操作顺序为:
1.加载A,new A. 这个时候A刚刚被new.属性也没设置,更别说被代理了.
2.注入A的属性..发现需要注入B这个属性.
3.加载B, new B. 类似步骤1.
4.注入B的属性...发现需要A..为了解决循环依赖无限循环下去...所以这里注入了原始的A也就是步骤1的A.
5.B注入属性完成
6.B加载完成,被丢到spring的容器内.可以被使用.
6.A属性注入完成
7.因为A的方法上有@Async注解.A会被AsyncAnnotationBeanPostProcessord代理..返回了proxyA,即wrapped的A
8.A被替换成了proxyA..proxyA被丢到spring容器里,(还没可以被使用,加载方法还没完成,其实容器里的还是原始的A..之后才会被替换.这里只是为了表述逻辑...).
9.检测到A被注入到B中过..B中的A != 容器里的proxyA..于是报错..(A的加载抛出错误)
所以就有了开头的问题..但是这个bean的加载顺序是会影响结果的..如果先加载B就不会有这个问题,因为B方法上没有@Async..不会被proxy.
试验结果是.不同机器上加载bean顺序不一样.但是同一台机器上每次加载的顺序是一样的.目前还没研究顺序是什么因素决定的.
一点思考
明白BPP代理bean在循环依赖过程中可能会造成创建异常...我就有了一个疑问...
为什么@Transaction和我们自己写的@Aspect就没问题???这也是依赖AOP(动态代理)来实现的呀??
SmartInstantiationAwareBeanPostProcessor
原因在于这2个的实现BPP类都实现了这个接口
这个接口的用处在于..当你在ABA中的B获取A的时候给你一次回调机会..让你可以返回proxy的A.而不是原始的rawA.....所以A和B.A就都是proxyA了.就不会抛出异常咯...
另外想说一下..看类的结构都能看出来aspect和transaction的实现方式是非常类似的...共用了很大一部分代码...(只是为每个bean包装advisor的方式不同)
但是很可惜..AsyncAnnotationBeanPostProcessor没有实现这个接口..原因不明
@Lazy
循环依赖的出现导致一个Bean的raw和wrapped会被注入到不同的Bean出现错误...
SmartInstantiationAwareBeanPostProcessor的解决策略是提前返回wrapped Bean.
而@Lazy的解决策略是斩断循环依赖...
@Lazy也会注入在你的Bean(A)里注入属性Bean(B).只是这个属性Bean(B)并不是你@Component的那个Bean(B)...而是代理生产的子类Bean(B')..
现在因为有2个不同的Bean...1个你的@Component的Bean(B)... 1个你的@Component的Bean的子类的Bean(B')...所以不再有循环依赖...
当调用属性Bean的方法的时候转发给你真正@Component的那个Bean来调对应的方法.
小结
spring本身可以解决循环依赖的问题..
但是当特殊的情况下还是会出现Bean创建的问题...(循环依赖+没实现SmartInstantiationAwareBeanPostProcessor的BPP+特定的Bean加载顺序)
解决办法:
1.BPP的实现类实现SmartInstantiationAwareBeanPostProcessor接口..参考@Transaction和@Aspect
2.注入的Bean用@Lazy修饰