spring中的循环依赖问题

问题表现

代码上线,发现启动报错

很明显发生了循环依赖。

上线人员表示,我就是按照之前的代码结构加的,之前代码没有问题,现在怎么就有问题了呢。

我们看下代码

代码

我将这个问题,做一个demo,用来描述这个问题

demo整体结构

很简单,一个启动类,三个bean。用来复现问题

具体代码

Config

public class Config {
    public void say() {
        System.out.println("do say");
    }
}

TestService

@Service
public class TestService {
    @Autowired
    private Config config;
    @PostConstruct
    private void init() {
        System.out.println("this is BeanTest init function. ");
        config.say();
    }
    @Bean
    public Config config(){
        System.out.println("new Config");
        return  new Config();
    }
}

TService

@Service
public class TService {
    @Autowired
    private Config config;
}

上面就是demo 的代码,他基本描述了真实项目里的问题。

问题分析

原因

这里主要是三个bean ,TService ,TestService 和Config,他们之间的关系如下所示:

如果我们debug ,会发现最终会直接抛出了BeanCurrentlyInCreationException 这个异常。

这个初始化的流程是:TService 初始化的时候,里面注入了Config ,因此需要先初始化Config。Config 在TestService 这个bean 里被@bean 注解修饰,因此要想初始化成功Config ,就需要初始化出TestService,当初始化

TestService的时候,发现Config 被注入了,因此有需要初始化Config ,这样就形成了一个环,抛出异常。

为什么别的地方没问题

根据上面的结论来看,如果项目初始化TestService 这个bean ,就会发现没有问题。查资料,bean 初始化的顺序为名字的字母排序,因此如果把TService 改成ZService ,在这里理论上就不会有问题。

事实证明 确实如此。

解决办法

这个问题出现的并不高级,解决办法非常多,比如调整bean 的加载逻辑,Config 移出来,等等

问题延伸

三种循环依赖的场景

  1. 构造器的循环依赖:这种依赖spring是处理不了的,直接抛出BeanCurrentlylnCreationException异常。

  1. 单例模式下的setter循环依赖:通过“三级缓存”处理循环依赖,能处理。

  1. 非单例循环依赖:无法处理。原型(Prototype)的场景是不支持循环依赖的,通常会走到AbstractBeanFactory类中下面的判断,抛出异常。

Spring 三级缓存解决循环依赖

本质原理

java中只有值传递,但是这里可以借用一个引用传递的概念,这个引用传递指的是,当获得对象的引用时,对象的属性是可以延后设置的。(但是构造器必须是在获取引用之前,毕竟引用是靠构造器给你生成的)

具体分析

Spring创建Bean的流程

分析这个问题,我们先来看看bean的整个创建流程

对Bean的创建最为核心三个方法解释如下:

  1. createBeanInstance:实例化,其实也就是调用对象的构造方法实例化对象

  1. populateBean:填充属性,这一步主要是对bean的依赖属性进行注入(@Autowired)

  1. initializeBean:回到一些形如initMethod、InitializingBean等方法

从对单例Bean的初始化可以看出,循环依赖主要发生在第二步(populateBean),也就是field属性注入的处理。

处理方案

Spring内部维护了三个Map,也就是我们通常说的三级缓存。在Spring的DefaultSingletonBeanRegistry类中,会赫然发现类上方挂着这三个Map

  • singletonObjects : (一级缓存)“单例池”“容器”,缓存创建完成单例Bean的地方。

  • earlySingletonObjects:(二级缓存)映射Bean的早期引用,也就是说在这个Map里的Bean不是完整的,甚至还不能称之为“Bean”,只是一个Instance,用于解决循环依赖

  • singletonFactories:(三级缓存) 映射创建Bean的原始工厂,用于解决循环依赖

当 Spring 为某个 Bean 填充属性的时候,它首先会寻找需要注入对象的名称,然后依次执行 getSingleton() 方法得到所需注入的对象,而获取对象的过程就是先从一级缓存中获取,一级缓存中没有就从二级缓存中获取,二级缓存中没有就从三级缓存中获取,如果三级缓存中也没有,那么就会去执行 doCreateBean() 方法创建这个 Bean。

Spring 是如何通过这三级缓存来解决循环依赖的呢?

我们举一个简单的例子,把上面的Config 和 TService变一下

@Service
public class Config {
    @Autowired
    private TService tService;
    public void say() {
        System.out.println("do say");
    }
}
@Service
public class TService {
    @Autowired
    private Config config;
}

发现可以正常启动

这里的加载流程是

  1. 实例化 TService 还未完成属性填充和初始化方法(@PostConstruct)的执行,TService 只是一个半成品。

  1. 为 TService 创建一个 Bean工厂,并放入到 singletonFactories 中。

  1. 发现 TService需要注入Config 对象,但是一级、二级、三级缓存均未发现对象 Config。

  1. 实例化Config,此时 Config 还未完成属性填充和初始化方法(@PostConstruct)的执行,Config 只是一个半成品。

  1. 为 Config 创建一个 Bean工厂,并放入到 singletonFactories 中。

  1. 发现 Config 需要注入 TService 对象,此时在一级、二级未发现对象TService,但是在三级缓存中发现了对象 TService,从三级缓存中得到对象 TService,并将对象 TService 放入二级缓存中,同时删除三级缓存中的对象TService。(注意,此时的TService还是一个半成品,并没有完成属性填充和执行初始化方法)

  1. 将对象 TService 注入到对象Config 中。对象Config 完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 Config。(此时对象 Config 已经是一个成品)

  1. 对象 TService得到对象Config,将对象 Config 注入到对象 TService 中。(对象 TService 得到的是一个完整的对象 Config)

  1. 对象 TService完成属性填充,执行初始化方法,并放入到一级缓存中,同时删除二级缓存中的对象 TService。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值