Spring为什么会出现循环引用异常

引子

@Configuration
@EnableAsync
public class Test {
    @Bean
    public A a(){ return new A();}
    @Bean
    public B b(){ return new B();}

    public class A{
        @Resource
        private B b;
        public void a(){

        }
    }

    public class B{
        @Resource
        private A a;
        public void b(){

        }
    }
}

对于上方代码,你觉得运行是否会报错?

答案是不会,虽然A和B之间循环引用,但Spring早就想到这一点,并进行处理了。
接着我们在a()b()方法上加上注解@Async,并且运行

@Configuration
@EnableAsync
public class Test {
    @Bean
    public A a(){ return new A();}
    @Bean
    public B b(){ return new B();}

    public class A{
        @Resource
        private B b;
        @Async
        public void a(){

        }
    }

    public class B{
        @Resource
        private A a;
        @Async
        public void b(){

        }
    }
}

错误信息如下:

Error creating bean with name 'a': Bean with name 'a' has been injected into other beans [b] 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 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.

意思是创建a时,其他依赖a的bean获取到的a和最终加入缓存的不一样


原因分析:

bean构建步骤

首先看一下Spring怎么创建Bean的,我们这里讨论的是单例模式:

缓存说明

bean创建过程中的缓存:

  • singletonObjects:构建bean并初始化完成后加入的,也称一级缓存
  • earlySingletonObjects:处理依赖过程中被其他bean所引用后加入,也称二级缓存
  • singletonFactories:构建完成后加入,也称三级缓存

同一bean只会在一个缓存中出现

没有循环引用bean的构建

在这里插入图片描述

循环引用bean的构建

在这里插入图片描述
上图完美解释了为什么A,B互相引用,却不会导致循环的原因。为什么只是加入了@Aysnc注解却报错,我们需要知道一些其他的东西

源码分析

在这里插入图片描述

上面代码就是bean创建的核心部分,从红框部分讲解

  1. 创建或获取bean的对象
  2. 将对象加入三级缓存,值得注意的是,加入的是一个lambda表达式,调用了getEarlyBeanReference()方法,这里我们之后会说明
  3. 填充依赖bean,如果依赖bean不存在,会再次进入doCreateBean()。若存在,则会从缓存中获取;要注意的是,如果是从三级缓存中获取,则会加入到二级缓存
  4. 初始化bean,执行BeanPostProcessorpostProcessBeforeInitialization/postProcessAfterInitialization,实现如 InitializingBeanafterPropertiesSet或自定义 init 方法,或AOP的实现,并返回初始化后的bean
  5. 判断是否循环引用,是的话判断其他引用的bean和当前的bean是否相同,否则删除所有依赖于当前bean一级缓存中的bean(前提是这个bean只用来进行类型检查),不能删除则报错。

很明显,之前我们遇到的问题,就是在4、5步出现的。因为第4步初始化时返回一个和之前不同的A对象,并且A被B所引用了,进入了二级缓存。又因为B不是用来类型检查,也就是说它的引用是有效的,最终报错

接下来我们就要看看第4步为什么会返回一个和之前不相同的对象,这其中主要类是BeanPostProcessor

BeanPostProcessor - bean初始化前后处理器

BeanPostProcessor 有两个方法:

  • postProcessBeforeInitialization(Object bean, String beanName):bean初始化之前调用,并返回处理后的bean
  • postProcessAfterInitialization(Object bean, String beanName):bean初始化之后调用,并返回处理后的bean

第4步exposedObject = initializeBean(beanName, exposedObject, mbd);进入:
在这里插入图片描述

  1. 执行所有BeanPostProcessor的postProcessBeforeInitialization方法
  2. 执行bean的初始化方法
  3. 执行所有BeanPostProcessor的postProcessAfterInitialization方法

回到注解@Async,为什么它能生效,就是因为Spring注册了一个AsyncAnnotationBeanPostProcessor类的bean,它检测带有@Async方法的对象,代理并返回一个新对象

总结问题

因为A在创建过程中被B引用,并且A在初始化之后发生改变,导致B引用了一个不正确的对象,结果自然出现错误。
下面我们会提出怎么解决这个问题,并且提出一些不同的原因导致的这种错误


解决方案:

  1. 手动获取依赖bean
  2. 实现自定义BeanPostProcessor
  3. 向Spring提供一个AsyncAnnotationAdvisor

1.手动获取依赖bean

既然是因为循环引用导致bean的提前暴露,那我们大可以让Spring走完全部的流程,我们自己手动注入依赖。

@Configuration
@EnableAsync
public class Test {
    @Bean
    public A a(){ return new A();}
    @Bean
    public B b(){ return new B();}

    /**
     * 继承ApplicationContextAware,获取ApplicationContext。
     */
    public class A implements ApplicationContextAware {
        private ApplicationContext context;
        private B b;

        /**
         * b为空则使用上下文获取此bean
         * @return
         */
        public B getB() {
            if (b == null){
                b = context.getBean("b",B.class);
            }
            return b;
        }

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            context = applicationContext;
        }


        @Async
        public void a(){

        }
    }

    public class B  implements ApplicationContextAware {
        private ApplicationContext context;
        private A a;

        public A getA() {
            if (a == null){
                a = context.getBean("a",A.class);
            }
            return a;
        }

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            context = applicationContext;
        }
        @Async
        public void b(){

        }
    }
}

如上,使用上下文手动获取Bean。一定要在Spring的bean构建和初始化流程结束后才可调用getA()getB(),否则在构建或初始化过程中又触发依赖bean的构建,那样就等同于使用@Resource

2.实现自定义BeanPostProcessor

还记得第2步加入三级缓存的lambda表达式里的getEarlyBeanReference方法吗,我们看看它的源码
在这里插入图片描述
一个简单的循环,大致内容是调用实现了SmartInstantiationAwareBeanPostProcessor接口的getEarlyBeanReference方法,我们接着看getEarlyBeanReference方法到底是做什么的
在这里插入图片描述
在三级缓存被调用时,调用此方法,能返回postProcessBeforeInitialization / postProcessAfterInitialization处理后的对象。之后在调用postProcessBeforeInitialization / postProcessAfterInitialization时会直接返回原始bean。

按照这个逻辑,使用@Async不应该会报错,这是因为实现@Async功能的AsyncAnnotationBeanPostProcessor类并没有实现getEarlyBeanReference方法,所以我们大可以自己实现一个@Aysnc处理类,并且加入到Spring中。关键代码Spring已经帮我们写好,我们只需要实现SmartInstantiationAwareBeanPostProcessor接口的getEarlyBeanReference方法,使之在被循环引用时能返回初始化之后的对象。


/**
 * 自定义异步方法代理处理,实现{@link #getEarlyBeanReference}
 * 使循环引用时能获得正确的对象
 */
public class MyAsyncAnnotationBeanPostProcessor extends AsyncAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor {


    private Set worked = new HashSet();



    /**
     * 假设在bean还未创建完成时被其他bean引用,在缓存中获取就会触发此方法
     * 返回{@link #postProcessBeforeInitialization} / {@link #postProcessAfterInitialization}所返回的
     * 之后再调用{@link #postProcessBeforeInitialization} / {@link #postProcessAfterInitialization}不做处理
     * @param bean 原始bean
     * @param beanName bean名称
     * @return 处理后的结果
     * @throws BeansException
     */
    public Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
        Object wraBean = super.postProcessAfterInitialization(bean, beanName);
        worked.add(bean);
        return wraBean;
    }

    /**
     * 判断是否处理过,已经处理直接返回原始bean,反正则进行处理
     * @param bean 原始bean
     * @param beanName bean名称
     * @return 处理后的结果
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        if (worked.contains(bean))return bean;
        Object wraBean = super.postProcessAfterInitialization(bean, beanName);
        worked.remove(bean);
        return wraBean;
    }
}

修改后的Test.java

@Configuration
//@EnableAsync
public class Test {
    @Bean
    public A a(){ return new A();}
    @Bean
    public B b(){ return new B();}
    @Bean
    public AsyncAnnotationBeanPostProcessor asyncAnnotationBeanPostProcessor(){
        return new MyAsyncAnnotationBeanPostProcessor();
    }
    
    public class A {
        @Resource
        private B b;

        @Async
        public void a(){

        }
    }

    public class B {
        @Resource
        private A a;

        @Async
        public void b(){

        }
    }
}

注意:@EnableAsync会提供一个AsyncAnnotationBeanPostProcessor bean,这与我们向Spring提供的所冲突,所以必须注释它

3.向Spring提供一个AsyncAnnotationAdvisor

除了@Aysnc会进行代理,Spring还有许多功能使用到代理。要知道@Transactional是我们非常常用的注解,它也会导致Spring代理bean,并且服务之间相互依赖非常常见,为什么不会发生错误呢?

这归功于SmartInstantiationAwareBeanPostProcessor,Spring AOP 的主要实现类是AbstractAutoProxyCreator,这是一个抽象类,它主要功能是获取切面或织入点,对对象代理返回代理对象,它的几个实现类如下:

  • DefaultAdvisorAutoProxyCreator:默认的,根据切面代理,不支持AspectJ注解
  • BeanNameAutoProxyCreator:根据bean名称代理,非常不常用,并且没有切面
  • InfrastructureAdvisorAutoProxyCreator:只对框架内的切面进行代理,比如说实现@Transactional的切面。从源码来说,BeanDefinition的role定义为2,这个切面就会被处理
  • AspectJAwareAdvisorAutoProxyCreator:引入了AspectJ的理念,和DefaultAdvisorAutoProxyCreator没有太大区别。对切面进行排序,比方说Before排序在After之前
  • AnnotationAwareAspectJAutoProxyCreator:最完全的处理器,不仅能处理Spring定义的切面,也能处理AspectJ内的注解

以上类在你向Spring注入切面bean后,创建bean时就会触发,并自动代理bean。它们其中并没有AsyncAnnotationBeanPostProcessor,这是因为AsyncAnnotationBeanPostProcessor只进行@Async的处理,并且@Async这个功能定义的切面不会注册到Spring中。而上方的所有类会从Spring中获取所有切面进行分析处理。

看看下方的代码:

@Configuration
//@EnableAsync
public class Test {
    @Bean
    public A a(){ return new A();}
    @Bean
    public B b(){ return new B();}
    @Bean
    public AsyncAnnotationBeanPostProcessor asyncAnnotationBeanPostProcessor(){
        return new MyAsyncAnnotationBeanPostProcessor();
    }

    public class A {

        @Resource
        private B b;

        @Async
        @Transactional
        public void a(){

        }
    }

    public class B {

        @Resource
        private A a;

        @Async
        public void b(){

        }
    }
}

报错信息:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'main': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Bean with name 'a' has been injected into other beans [b] 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 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.

查看报错信息,和之前一样,还是引用bean和最终bean不一样导致的。至于为什么,查看下方流程图
在这里插入图片描述
getEarlyBeanReference执行时,被多个处理器进行了处理。之后在初始化时执行,给的是一个原始对象,但是在第一个代理的处理器之后,他们所存储的是前一个代理的处理器返回内容,所以postProcessAfterInitialization还会对其进行处理,最终返回一个与原始对象不同的对象

AsyncAnnotationBeanPostProcessor可以看做成只处理@Async的切面,而AbstractAutoProxyCreator则是处理所有的切面,所以我们只需要定义一个处理@Async的切面。那我们可以将AsyncAnnotationBeanPostProcessor中处理的切面交给AbstractAutoProxyCreator

代码如下:

@Configuration
//@EnableAsync
public class Test {
    @Bean
    public A a(){ return new A();}
    @Bean
    public B b(){ return new B();}
//    @Bean
//    public AsyncAnnotationBeanPostProcessor asyncAnnotationBeanPostProcessor(){
//        return new MyAsyncAnnotationBeanPostProcessor();
//    }

    /**
     * 注册一个AsyncAnnotationAdvisor
     * AsyncAnnotationAdvisor是Spring用来处理@Async的切面,不需要我们自己定义
     * @return
     */
    @Bean
    public AsyncAnnotationAdvisor asyncAnnotationAdvisor(){
        AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor();
        return advisor;
    }

    public class A {

        @Resource
        private B b;

        @Async
        @Transactional
        public void a(){
            System.out.println(1);
        }
    }

    public class B {

        @Resource
        private A a;

        @Async
        public void b(){

        }
    }
}

框架注册的代理处理器冲突问题

这个bug是我踩的坑。下面看看这个示例。

项目引入如下依赖

pom.xml

		<dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.5.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.7</version>
        </dependency>
    </dependencies>

application.yml

spring:
  datasource:
    druid:
      aop-patterns: com.*
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.3.12:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: 123456

Test.java

@Configuration
public class Test {
    @Bean
    public A a(){ return new A();}
    @Bean
    public B b(){ return new B();}
    
    public class A {

        @Resource
        private B b;

        @Transactional
        public void a(){
            System.out.println(1);
        }
    }

    public class B {

        @Resource
        private A a;

        @Transactional
        public void b(){

        }
    }
}

上述代码同样会和之前报一样的错误。

如何找出问题所在

出现这个错误,基本可以断定是注册了多个代理的BeanPostPocessor,并且产生了循环引用,直接进入org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#getEarlyBeanReference中并添加一个断点启动。
在这里插入图片描述
启动并观察this.beanPostProcessorCache.smartInstantiationAware,看看里面是否有多个代理BeanPostPocessor
在这里插入图片描述
明显看到AnnotationAwareAspectJAutoProxyCreatorDefaultAdvisorAutoProxyCreator,说明引入的maven依赖中注册了多个。
我使用idea,复制一下名称,ctrl+shift+f搜索一下试试
在这里插入图片描述
找到和依赖jar相关的类,进入查看
在这里插入图片描述

如何解决

Druid注册一个DefaultAdvisorAutoProxyCreator,这是不应该的。这个时候我们除了注释yml中的aop-patterns或看看项目作者有没有发现这一问题。查看druid的github,发现再1.1.12版本就已经修复这个问题,更新版本,解决问题。
在这里插入图片描述
项目中可能产生此类错误的不是我所写的示例,但都可以归到这一类目。如果升级版本不可以解决问题,可以看看我是如何解决@Async的问题,举一反三。

AnnotationAwareAspectJAutoProxyCreator则是Spring boot自动构建的,看看源码:
在这里插入图片描述
下方是上方代码的流程图
在这里插入图片描述
控制开关是spring.aop.auto,根据是否引入aspectjweaver-1.9.7.jar来提供两个BeanPostProcessor,AnnotationAwareAspectJAutoProxyCreatorInfrastructureAdvisorAutoProxyCreator

我推荐引入aspectjweaver-1.9.7.jar来向Spring提供AnnotationAwareAspectJAutoProxyCreator,因为InfrastructureAdvisorAutoProxyCreator只对框架内的切面有效。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值