【Spring之依赖注入】2. Spring处理@Async导致的循环依赖失败问题

我们知道Spring内部可以解决循环依赖的问题,但Spring的异步(@Async)会使得循环依赖失败。本文介绍其原因和解决方案。

1 问题复现

1.1 配置类

定义配置类,并添加@EnableAsync注解以启用异步功能。目的:就是使用我们自定义的线程池来进行异步执行
如下:

AsyncConfig类 是一个Spring配置类,用于定义和管理异步任务执行的配置。其中包含了Bean的定义和初始化。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@EnableAsync
@Configuration
public class AsyncConfig {
	
    @Bean("asyncExecutor")
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(50);
        // 设置最大线程数
        executor.setMaxPoolSize(200);
        // 配置队列大小
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        // 设置默认线程名称
        executor.setThreadNamePrefix("THREAD-ASYNC");
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //执行初始化
        executor.initialize();
        return executor;
    }
}

解析:

# 解析一下 asyncExecutor() 方法:
@Bean("asyncExecutor")1.这个注解表示该方法将返回一个对象,这个对象将被注册到Spring的应用上下文中作为一个Bean,并且该Bean的名称是 asyncExecutor。
2.方法最后返回了配置好的 ThreadPoolTaskExecutor 对象,这个对象将被注册为Spring应用上下文中的一个Bean,名为 asyncExecutor。
在定义了这个配置类之后,你就可以在Spring的其他组件中通过 @Autowired@Resource 注解来注入这个 Executor Bean,并使用它来执行异步任务。
3.同时,你也可以在方法上使用 @Async("asyncExecutor") 注解来指定使用 asyncExecutor 线程池来执行该方法。

1.2 定义Service

使用循环依赖

package com.dlkhs.service;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
 
@Service
public class A {
    @Autowired
    private B b;
 
    @Async("asyncExecutor")
    public void print() {
        System.out.println("Hello World");
    }
}

package com.dlkhs.service;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
@Service
public class B {
    @Autowired
    private A a;
}

1.3 定义Controller

package com.dlkhs.controller;
 
import com.knife.service.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class HelloController {
    @Autowired
    private A a;
 
    @GetMapping("/test")
    public String test() {
        a.print();
        return "测试循环依赖的异步使用:成功";
    }
}

1.4 启动springboot报错

Caused by: 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.
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:624) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:226) ~[spring-beans-5.2.6.RELEASE.jar:5.2.6.RELEASE]

2.原因分析:看@Async标记的bean注入时机

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

在这里插入图片描述

真正创建代理对象的代码在AbstractAdvisingBeanPostProcessor中的postProcessAfterInitialization方法中,看核心逻辑代码:

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

// 这个方法主要是为打了@Async注解的bean生成代理对象
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    // 这里是重点,这里返回true
    if (isEligible(bean, beanName)) {
        // 工厂模式生成一个proxyFactory
        ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
        if (!proxyFactory.isProxyTargetClass()) {
            evaluateProxyInterfaces(bean.getClass(), proxyFactory);
        }
        // 切入切面并创建一个代理对象
        proxyFactory.addAdvisor(this.advisor);
        customizeProxyFactory(proxyFactory);
        return proxyFactory.getProxy(getProxyClassLoader());
    }
    // No proxy needed.
    return bean;
}

protected boolean isEligible(Class<?> targetClass) {
    // 首次从eligibleBeans这个map中一定是拿不到的
    Boolean eligible = this.eligibleBeans.get(targetClass);
    if (eligible != null) {
        return eligible;
    }
    // 如果没有advisor,也就是切面,直接返回false
    if (this.advisor == null) {
        return false;
    }
    // 这里判断AsyncAnnotationAdvisor能否切入,因为我们的bean是打了@Aysnc注解,这里是一定能切入的,最终会返回true
    eligible = AopUtils.canApply(this.advisor, targetClass);
    this.eligibleBeans.put(targetClass, eligible);
    return eligible;
}

至此方法上有@Aysnc注解的bean就创建完成了,结果是生成了一个代理对象

2.1 循环依赖生成过程

正确的循环依赖

  • beanA开始初始化,beanA实例化完成后给beanA的依赖属性beanB进行赋值;
  • beanB开始初始化,beanB实例化完成后给beanB的依赖属性beanA进行赋值;

但是我们上述的例子有@Async注解:所以属于不正确的循环依赖

  • 因为beanB是支持循环依赖的,所以可以在earlySingletonObjects中可以拿到beanB的早期的引用,但是因为beanA所在的方法上有@Aysnc注解,所以并不能在earlySingletonObjects中可以拿到早期的引用;
  • 接下来执行执行initializeBean(Object existingBean, String beanName)方法,这里beanB可以正常实例化完成,但是因为beanA上有@Aysnc注解,所以向Spring IOC容器中增加了一个代理对象,也就是说beanBbeanA并不是一个原始对象,而是一个代理对象

总结:B实例完成了实例化(也就是说B里面的属性A是原始对象),但A实例却是个代理对象,所以导致B实例里面的是属性A不是最终放入到容器的实例对象;所以在执行自检程序之后,就报错了;

2.2 自检程序 doCreateBean方法

接下来进行执行doCreateBean方法时对进行检测

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {
    if (earlySingletonExposure) {
        Object earlySingletonReference = getSingleton(beanName, false);
        if (earlySingletonReference != null) {
            if (exposedObject == bean) {
                exposedObject = earlySingletonReference;
            }else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)){
           String[] dependentBeans = getDependentBeans(beanName);
           Set<String> actualDependentBeans = new LinkedHashSet< (dependentBeans.length);
// 重点在这里,这里会遍历所有依赖的bean,如果beanB依赖beanA和缓存中的beanA不相等
// 也就是说beanB本来依赖的是一个原始对象beanA,但是这个时候发现beanA是一个代理对象,就会增加到actualDependentBeans
        for (String dependentBean : dependentBeans) {
            if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                actualDependentBeans.add(dependentBean);
            }
        }
        // 发现actualDependentBeans不为空,就发生了我们最开始的错误
        if (!actualDependentBeans.isEmpty()) {
        //...省略
		throw new BeanCurrentlyInCreationException
		return exposedObject;
	}

不一致情况:也就是说beanB本来依赖的是一个原始对象beanA,但是这个时候发现beanA是一个代理对象

执行自检程序:由于allowRawInjectionDespiteWrapping默认值是false,表示不允许上面不一致的情况发生,就报错了;(若一致则会被赋值为true)

3.解决方案

一共有三种解决方案:

  • 懒加载:使用@Lazy或者@ComponentScan(lazyInit = true ) 【注:后者不建议使用】
  • 不让@Async的方法有循环依赖
  • 将allowRawInjectionDespiteWrapping设置为true【非常不建议】

3.1 懒加载@Lazy

使用@Lazy。不建议使用@ComponentScan(lazyInit = true),因为它是全局的,容易产生误伤。

两种实例写法

  • 法1. A类注入的b成员上边写@Lazy
  • 法2: B类注入的a成员上边写@Lazy
3.1.1 将@Lazy写到A类的b成员上边
package com.dlkhs.service;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
 
@Service
public class A {
    @Lazy
    @Autowired
    private B b;
 
    @Async
    public void print() {
        System.out.println("Hello World");
    }
}

3.1.2 将@Lazy写到B类的a成员上边
package com.dlkhs.service;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
 
@Component
public class B {
    @Lazy
    @Autowired
    private A a;
}

3.1.3 原理分析

以@Lazy放到A类注入的b成员上边为例:

package com.dlkhs.service;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
 
@Service
public class A {
    @Lazy
    @Autowired
    private B b;
 
    @Async
    public void print() {
        System.out.println("Hello World");
    }
}

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

B 加载时,将前边生成的B代理对象取出,再注入 A 就能直接从一级缓存中获取到 A,这样 B 也能正常初始化完成了。所以,循环依赖的问题就解决了。

3.2 不要让@Async的Bean参与循环依赖

通俗说就是,不要让有参与循环依赖对象类里含有异步执行的方法;

若当前对象必须要有循环依赖的话,则考虑把该异步执行的方法移植到相关serviceimpl类外面;

即:新建一个类,加上@Service注解,然后把之前要异步执行的方法和注入的循环依赖对象,放进去即可;

3.3 allowRawInjectionDespiteWrapping设置为true

不建议使用!!!

配置后,容器启动虽然不报错了。但是:Bean A的@Aysnc方法不起作用了。因为Bean B里面依赖的a是个原始对象,所以它不能执行异步操作(即使容器内的a是个代理对象)

4. 扩展

4.1 @Transactional注解为什么不会导致启动失败

  • 疑惑:同为创建动态代理对象,同作为注解标注在类/方法上,为何@Transactional就不会出现这种启动报错呢?

  • 原因:它们代理的创建的方式不同;

    • @Transactional创建代理的方式:使用自动代理创建器InfrastructureAdvisorAutoProxyCreator(AbstractAutoProxyCreator的子类),它实现了getEarlyBeanReference()方法从而很好的对循环依赖提供了支持;
    • @Async创建代理的方式:使用AsyncAnnotationBeanPostProcessor单独的后置处理器。它只在一处postProcessAfterInitialization()实现了对代理对象的创建,因此若它被循环依赖了,就会报错。
  • 68
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大龄烤红薯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值