循环依赖问题

常见三种循环依赖

  • A-->A
  • A-->B 且 B-->A
  • A-->B , B-->C , C-->A

Spring中使用了三级缓存来解决循环依赖的问题,我们今天从零出发,一点点揭示三级缓存的面纱。

三级缓存

通过源码,我们了解到三级缓存分别是
第一级缓存 常称为单例池 singletonObjects
第二级缓存 earlySngletonObjects
第三级缓存 singletonFactories

我们从一个Bean的生命周期出发:即实例化-->属性赋值-->初始化-->销毁
在这里,说的更细一点就是: 通过无参构造方法实例化出一个“普通对象”-->依赖注入-->初始化前-->初始化-->初始化后(在这里进行AOP)-->放入单例池Map-->成为Bean对象

  • 可以看到一个词是“普通对象”,别急,看到后面我们就能明白他的含义。
  • 另一个词是单例池Map,其实也就是一级缓存,它存在有什么意义呢?实际上是为了实现“单例”,而他是Map,想必你已经知道是为什么了,Map的Key不可重复,所以Map<BeanName,Object>即可实现单例(这个思想在后边有应用)

现在我们假设有两个Bean发生了循环依赖,分别是AService和BService,两者互相注入,我们从AService的角度出发:
AService的生命周期经过了以下几个阶段:

  1. 实例化-->成为AService的普通对象
  2. 填充BService属性
  3. 填充其他属性
  4. 其他步骤(包括AOP)
  5. 放入单例池

这里将2、3步分开为了有更直观的效果,当到第2步时,我们需要BService这个Bean,那么我们去哪里找?很容易想到:去单例池中找,但是此时单例池中还没有BService,所以找不到,那就会进入创建BService的过程所以生命周期转变为

  1. 实例化-->成为AService的普通对象
  2. 填充BService属性-->去单例池中找,没找到,开始创建BService
    2.1 实例化-->成为BService普通对象
    2.2 填充AService属性
    2.3 填充其他属性
    2.4 其他步骤(包括AOP)
    2.5 放入单例池
  3. 填充其他属性
  4. 其他步骤(包括AOP)
  5. 放入单例池

在2.1步,BService又需要AService这个Bean,第一步还是去单例池中找,但仍然没找到,此时怎么办呢,难道进入创建AService阶段吗?这样不就又回去了吗?第一个问题出现了,我们这里给出一个解决方案:

  1. 实例化-->成为AService的普通对象-->放入Map1<"AService",AService普通对象>
  2. 填充BService属性-->去单例池中找,没找到,开始创建BService
    2.1 实例化-->成为BService普通对象
    2.2 填充AService属性-->去单例池找-->找不到,去Map1中找,找到AService普通对象
    2.3 填充其他属性
    2.4 其他步骤(包括AOP)
    2.5 放入单例池
  3. 填充其他属性
  4. 其他步骤(包括AOP)
  5. 放入单例池

问题解决了,即在AService刚刚实例化后即用一个Map1记住他,后续BService用的时候从里边取出即可,皆大欢喜,然而,问题出现了。
有一个我们前面提到但是一直没说的事情,AOP,它和这里的三级缓存有什么关系呢?了解过AOP的都知道,AOP的底层基于的是动态代理,即创建一个代理对象,看到这里,应该懂了前面为什么强调了一个“普通对象”,原因就在于,二者不完全一样。我们回到上一个问题,看似解决了,但是假如AService中存在AOP,那么前面不变,第4步中AOP会创建出一个AService的代理对象,并在第五步中理所应当的把代理对象放入单例池中,这样就产生了严重的问题:BService中使用的AService对象和单例池中的AService对象是不同的,想要解决这个问题,按常理说,我们需要让BService中使用的AService对象也变成AService的代理对象,如何解决?很显然,提前AService的AOP过程,让其提前生成代理对象,所以,生命周期又变成了这样:

  1. 实例化-->成为AService的普通对象-->判断是否发生循环依赖,如果有,提前AOP产生代理对象-->放入Map1<"AService",AService代理对象>
  2. 填充BService属性-->去单例池中找,没找到,开始创建BService
    2.1 实例化-->成为BService普通对象
    2.2 填充AService属性-->去单例池找-->找不到,去Map1中找,找到AService代理对象
    2.3 填充其他属性
    2.4 其他步骤(包括AOP)
    2.5 放入单例池
  3. 填充其他属性
  4. 其他步骤(包括AOP)
  5. 放入单例池
    这里需要解释以下为什么第一步要判断是否发生循环依赖,别忘记了我们的初衷:解决循环依赖问题,如果没有发生循环依赖,我们自然不需要将AOP提前

没错,问题又来了,这次是两个问题:1.提前AOP之后,第4步的AOP该如何处理,是让其跳过还是怎么样? 2.如何在第1步就能判断出发生了循环依赖?
第一个问题我们暂时搁置,我们先来解决第二个问题:如何在第一步就判断出发生了循环依赖?
这显然是很难做到的,程序并没有预知的能力,所以这个解决方案是失败的,但我们可以基于它做改进,我们可以想一想,哪里判断循环依赖更为容易,答案当然是第2.2步,显然在2.2步时我们已经获得了AService依赖B,且B依赖A的信息,所以将判断过程放在这里再合适不过,我们使用一个creatingSet来记录状态,如果一个Bean正在创建中,则加入creatingSet中,所以生命周期又变成了下边这样:

  1. AService加入creatingSet中
  2. 实例化-->成为AService的普通对象
  3. 填充BService属性-->去单例池中找,没找到,开始创建BService
    2.1 实例化-->成为BService普通对象
    2.2 填充AService属性-->去单例池找-->找不到,去creatingSet中找,如果找到了,代表发生了循环依赖-->提前执行AService的AOP,获得AService的代理对象
    2.3 填充其他属性
    2.4 其他步骤(包括AOP)
    2.5 放入单例池
  4. 填充其他属性
  5. 其他步骤(包括AOP)
  6. 放入单例池

我们把判断循环依赖的步骤放在了2.2步,并添加了creatingSet,即可方便的判断是否发生循环依赖。

没错,问题它又又又来了!
现在我们假设情景发生变化,即A-->B , A-->C , B-->A , C-->A 即A同时和BC发生循环依赖,则生命周期变为:

  1. AService加入creatingSet中
  2. 实例化-->成为AService的普通对象
  3. 填充BService属性-->去单例池中找,没找到,开始创建BService
    2.1 实例化-->成为BService普通对象
    2.2 填充AService属性-->去单例池找-->找不到,去creatingSet中找,如果找到了,代表发生了循环依赖-->提前执行AService的AOP,获得AService的代理对象
    2.3 ......
  4. 填充CService属性-->去单例池中找,没找到,开始创建CService
    3.1 实例化-->成为CService普通对象
    3.2 填充CService属性-->去单例池找-->找不到,去creatingSet中找,如果找到了,代表发生了循环依赖-->提前执行AService的AOP,获得AService的代理对象
    3.3 ......
  5. ......

可以看到,在2.2和3.2分别进行了两次AOP,并生成了两个AService的代理对象,分别给了B和C,但我们的AService是一个单例Bean,应该不管在哪里引用都是一个对象才对,这违反了单例,所以我们怎么使BService和CService拿到的是同一个对象呢?我在前面提到了单例池,说到单例池就是为了解决单例问题的,那这种思想不是恰好用在这里吗?我们再使用一个Map去记住代理对象,并让每一个想引用该对象的都去Map里拿就好了,而这个Map有其专属的名字,叫earlySingletonObjects<BeanName,Object>,是不是很眼熟?没错,这就是第二级缓存,顾名思义,它是在某个Bean还没有被创建出来时,别人想要使用这个Bean时取用的地方,所以称之为early,有了它,生命周期变为:

  1. AService加入creatingSet中
  2. 实例化-->成为AService的普通对象
  3. 填充BService属性-->去单例池中找,没找到,开始创建BService
    2.1 实例化-->成为BService普通对象
    2.2 填充AService属性-->去单例池找-->找不到,去creatingSet中找,如果找到了,代表发生了循环依赖-->去earlySingletonObjects找,没找到就创建-->提前执行AService的AOP,获得AService的代理对象
    2.3 ......
  4. 填充CService属性-->去单例池中找,没找到,开始创建CService
    3.1 实例化-->成为CService普通对象
    3.2 填充CService属性-->去单例池找-->找不到,去creatingSet中找,如果找到了,代表发生了循环依赖-->去earlySingletonObjects找,找到了,直接用
    3.3 ......
  5. ......

问题看似完美的解决了,但这里还有一些小细节没有被解决,即在2.2步中,如何在BService中执行AService的AOP?我们知道是创建AService的代理对象,但学过动态代理的应该知道,代理对象一般会有一个属性target指向普通对象,然后在代理对象的方法中调用target.xxx()来调用原始对象的方法,所以我们仍然需要AService的普通对象,我们在第1步已经有了AService的普通对象,那么很简单,我们只需要再用一个Map把它记下来即可,这个Map也有专有的名字,叫singletonFactories,即第三级缓存,但是通过源码我们得知,这个Map的value有所不同,即singletonFactories<BeanName,ObjectFactories>,他的value存的是工厂对象,这是一个函数式接口,即lambda表达式,为什么?实际上,这个lambda表达式包含了某个单例Bean的信息,我们通过该表达式判断该单例Bean是否有AOP,即:

  1. AService加入creatingSet中
  2. 实例化-->成为AService的普通对象-->加入singletonFactories
  3. 填充BService属性-->去单例池中找,没找到,开始创建BService
    2.1 实例化-->成为BService普通对象
    2.2 填充AService属性-->去单例池找-->找不到,去creatingSet中找,如果找到了,代表发生了循环依赖-->去earlySingletonObjects找,没找到-->去singletonFactories找,并通过lambda表达式判断是否需要AOP,如果需要-->提前执行AService的AOP,获得AService的代理对象并放入二级缓存中,如果不需要,直接返回singletonFactories中的AService普通对象并放入二级缓存中
    2.3 ......
  4. 填充CService属性-->去单例池中找,没找到,开始创建CService
    3.1 实例化-->成为CService普通对象
    3.2 填充CService属性-->去单例池找-->找不到,去creatingSet中找,如果找到了,代表发生了循环依赖-->去earlySingletonObjects找,找到了,直接用
    3.3 ......
  5. 填充其他属性
  6. 其他步骤(包括AOP)
  7. 从二级缓存中拿出对象,放入单例池

到这里,只剩最后一个问题没有解决了,就是前面提到的,AOP提前后,第5步的AOP怎么处理,其实正常人的想法是去查二级缓存,如果查到了二级缓存,证明我们已经解决了循环依赖的问题,也就代表提前执行了AOP,但Spring内部不是这样做的,可能是为了解耦合,Spring是使用了一个earlyProxyReferences(其实就是一个Map)来解决,当AOP被提前执行时,会向这个Map中添加一个标记,在正常进行AOP时进行一个判断再决定是否AOP

至此,Spring使用三级缓存来解决循环依赖的整体思路已经介绍完毕
但是,Spring仍有一些无法解决的循环依赖问题,后续我会更新文章来细说。


我们知道,循环依赖一般与依赖注入关系密切,而依赖注入有三种常见方式,分别是:

  • 属性注入(即注解注入)
  • 构造器注入
  • setter注入
    这里面可能大部分人最常用的都是第一种,即使用@Autowired或@Resource注入,但当我们使用@Autowired时,Spring会给我们一个建议:Spring团队不建议使用属性注入的方式,而是建议使用构造器注入,这是为什么呢?
    查询了一些资料,给出了一些理由:比如,使用构造器注入可以使用final(虽然我到现在还没搞明白不加final可能会有什么隐患),再比如使用构造器注入不能注入空对象,可以避免空指针异常等
    但是,还有一个很重要的问题:就是我们上面的提到的循环依赖问题,Spring不能解决使用构造器注入的循环依赖问题,比如在AService中存在构造器AService(BService bService),则AService的生命周期会直接卡在第一步,即根本无法实例化出普通对象,当进入构造器时,Spring看到需要一个BService对象,就会去找这个Bean,但是没找到,所以要创建BService,同时BService中也存在类似的构造方法,所以循环往复,无法打破循环,那么,就没有办法解决了吗?

当然有办法,使用@Lazy注解,我们现在在AService的构造器上添加@Lazy注解,则执行过程如下:尝试实例化AService对象,找到构造器,发现需要BService,去找没找到,本应该进入BService创建阶段,但是由于存在@Lazy注解,所以在这步Spring会生成一个BService的代理对象并给AService使用,则AService的生命周期得以正常进行,并且其中的BService属性赋值为代理对象,进而可以正常进行生命周期

有人觉得这个有问题,AService中使用的是BService的代理对象,但是实际单例池中的却是BService的普通对象,这不是违反了“单例”吗?我们在前面AOP的时候说了这个问题,到这里为什么又可以了呢?
原因在于,Spring自动创建的代理对象和通过AOP创建的代理对象是不同的,所以这里是可接受的,具体细节下次再更。


Echo
1 声望0 粉丝