Spring 记录一次项目启动失败的问题

主题

最近学习了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)
View Code

大致的意思是 创建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修饰

 

posted @ 2020-04-28 20:57  abcwt112  阅读(616)  评论(0编辑  收藏  举报