• 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修饰

  • 相关阅读:
    POJ题目分类
    最短路&记录记录记录路径
    博弈论
    生成树模板总结
    弱鸡的暑假图论安排
    面试随缘做题--day2
    面试随缘做题---day1
    PAT第四章速刷
    PAT第二章知识点快速复习
    sql语句快速复习
  • 原文地址:https://www.cnblogs.com/abcwt112/p/12797203.html
Copyright © 2020-2023  润新知