Spring Boot 中使用 @Async 注解导致循环依赖的原因及解决方案

Spring Boot 中使用 @Async 注解导致循环依赖的原因及解决方案,第1张

Spring Boot 中使用 @Async 注解导致循环依赖的原因及解决方案 前言

在写这篇文章之前,我写了一篇关于循环依赖的文章,为什么这篇文章我又说和循环依赖有关的话题呢,其实,是因为我原本想写一篇关于 @Async 原理分析的文章的,后来为了能更深入理解 @Async 以便我接下来的写的文章,无意之间看到了 @Async 也会导致循环依赖的问题。

关于循环依赖怎么解决以及源码分析可以看我这一篇文章:Spring 源码分析如何解决喜欢依赖的问题

Spring 是允许循环依赖的,换句话说,Spring 自身是已经解决了循环依赖这个问题,但是在这里竟然又出现了。比如以下添加 @Async 注解的代码:


执行完会报以下的错误:

在我们分析产生循环依赖的原因以及解决的方法之前,我们看一下 @Async 是什么。

@Async 是什么

@Async注解是Spring为我们提供的异步调用的注解,@Async可以作用到类或者方法上,标记了@Async注解的方法将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的 *** 作。

原因分析

下面我们根据报错,定位一下异常错误的地方:

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args){
...
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
    addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
...

//属性装配,属性赋值的时候,如果有发现属性引用了另外一个 bean,则调用 getBean 方法
populateBean(beanName, mbd, instanceWrapper);

//标注有 @Async 的 bean 的代理对象在此处会被生成, 重点的类:AsyncAnnotationBeanPostProcessor:它是一个后置处理器
//所以此句执行完成后  exposedObject 就会是个代理对象而非原始对象了
exposedObject = initializeBean(beanName, exposedObject, mbd);

...
//这里是报错的重点~~~
//如果 bean 允许被早期暴露,进入代码
if (earlySingletonExposure) {
    //这里主要把实例放入二级缓存中
    Object earlySingletonReference = getSingleton(beanName, false);
    if (earlySingletonReference != null) {
        //上面分析了exposedObject 是被 @Aysnc 代理过的对象, 而 bean 是原始对象 所以此处不相等,跳到 else 逻辑
        if (exposedObject == bean) {
            exposedObject = earlySingletonReference;
        }
        //allowRawInjectionDespiteWrapping 标注是否允许此 bean 的原始类型被注入到其它 bean 里面,即使自己最终会被包装(代理)
        //默认是 false 表示不允许,如果改为 true 表示允许,就不会报错了
        //另外 hasDependentBean 记录着每个 bean 它所依赖的 bean 的 Map
        else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
            String[] dependentBeans = getDependentBeans(beanName);
            Set actualDependentBeans = new linkedHashSet<>(dependentBeans.length);

            // 对所有的依赖进行一一检查
            for (String dependentBean : dependentBeans) {
                if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                    actualDependentBeans.add(dependentBean);
                }
            }

            // 若存在这种真正的依赖,那就报错了~~~  则个异常就是上面看到的异常信息
            if (!actualDependentBeans.isEmpty()) {
                throw new BeanCurrentlyInCreationException(beanName,
                        "Bean with name '" + beanName + "' has been injected into other beans [" +
                        StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                        "] 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.");
            }
        }
    }
}
...
}

被 @Async 标记的 bean 注入时机

我们从源码的角度来看一下被 @Async 标记的 bean 是如何注入到 Spring 容器里的。在我们开启 @EnableAsync 注解之后代表可以向 Spring 容器中注入 AsyncAnnotationBeanPostProcessor,它是一个后置处理器,我们看一下他的类图:

AsyncAnnotationBeanPostProcessor 它是一个 BeanPostProcessor,实现了 postProcessAfterInitialization 方法。此处我们看源码,建立代理的动做在抽象父类 AbstractAdvisingBeanPostProcessor 上(以下代码会删减,只保留核心逻辑代码):

	// 这个 map 用来缓存所有被 postProcessAfterInitialization 这个方法处理的 bean
	private final Map, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);

	// 这个方法主要是为打了@Async 注解的 bean 生成代理对象
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
		// 这里是重点,这里返回true
		if (isEligible(bean, beanName)) {
		  //copy属性 proxyFactory.copyFrom(this); 工厂模式生成一个新的 ProxyFactory 
			ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
			  //如果没有强制采用CGLIB 去探测它的接口
			if (!proxyFactory.isProxyTargetClass()) {
				evaluateProxyInterfaces(bean.getClass(), proxyFactory);
			}
			//切入切面并创建一个getProxy 代理对象
			proxyFactory.addAdvisor(this.advisor);
			customizeProxyFactory(proxyFactory);
			return proxyFactory.getProxy(getProxyClassLoader());
		}
		// No proxy needed.
		return bean;
	}

	protected boolean isEligible(Class targetClass) {
		//首次从 eligibleBeans 这个 map 中获取值肯定为 null
		Boolean eligible = this.eligibleBeans.get(targetClass);
		if (eligible != null) {
			return eligible;
		}
		//如果根本就没有配置 advisor,也就是切面,直接返回 false
		if (this.advisor == null) {
			return false;
		}
        //最关键的就是 canApply 这个方法,这里判断 AsyncAnnotationAdvisor 能否切入,如果 AsyncAnnotationAdvisor  能切入它,那这里最终返回 true
        //本例中方法标注有 @Aysnc 注解,所以肯定是可以被切入的。
		eligible = AopUtils.canApply(this.advisor, targetClass);
		this.eligibleBeans.put(targetClass, eligible);
		return eligible;
	}

至此为止,标志了 @Aysnc 注解的 bean 就创建完成了,最终是生成了一个代理对象。

从上面源码中,我们知道标志 @Aysnc 注解的 bean 最后生成了一个代理对象,下面我们分析一下这次的问题:

A 开始初始化,A 实例化完成后给 A 的依赖属性 B 进行赋值

B 开始初始化,B 实例化完成后给 B 的依赖属性 A 进行赋值

因为 A 是支持循环依赖的,所以可以在 earlySingletonObjects 中可以拿到 A 的早期引用的,但是因为 B 标志了 @Aysnc 注解并不能在 earlySingletonObjects 中可以拿到早期引用

此时 B 完成初始化,完成属性的赋值,此时属性 field 持有的是 bean A 原始类型的引用

接下来执行执行 initializeBean(Object existingBean, String beanName) 方法,这里 A 可以正常实例化完成,但是因为 B 标志了 @Aysnc 注解,所以向 Spring IOC 容器中增加了一个代理对象,也就是说 A 的 B 并不是一个原始对象,而是一个代理对象。

解决 @Aysnc 导致循环依赖的方法 加上 @Lazy 注解

代码如下:

执行的结果:

那为什么加上 @Lazy 之后就能正常访问了呢?

比如我们写了以下的代码:

public class A {

    private B b;
    
    @Async
    public A(@Lazy B b) {
        this.b = b;
    }
}

这里假设 A 先加载,在创建 A 的实例时,会触发依赖属性 B 的加载,在加载 B 时发现它是一个被 @Lazy 标记过的属性。那么就不会去直接加载 B,而是产生一个代理对象注入到了 A 中,这样 A 就能正常的初始化完成放入一级缓存了。当然,B 加载时,再去注入 A 就能直接从一级缓存中获取到 A,这样 B 也能正常初始化完成了。所以,循环依赖的问题就能解决了。

最后,到此为止,我们就知道为什么会发生循环依赖这种问题了。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5709881.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存