为什么会有循环依赖?Spring 为什么允许两个 Bean 互相引用?

上一篇我们分析了:

《@Transactional 为什么会失效?一次 Debug 看懂 Spring 踩过的所有坑》

有读者留言:

既然 Spring 负责创建 Bean、注入 Bean、生成代理对象。

那如果两个 Bean 互相依赖怎么办?

例如:

@Service
public class AService {

    @Autowired
    private BService bService;
}
@Service
public class BService {

    @Autowired
    private AService aService;
}

A 依赖 B。

B 又依赖 A。

这不就是死循环吗?

正常情况下不是应该直接报错吗?

但神奇的是:

Spring 居然允许这种情况存在。

为什么?

很多文章一上来就讲三级缓存。

结果大家背了半天:

一级缓存

二级缓存

三级缓存

最后连循环依赖到底是怎么产生的都没搞明白。

所以这一篇我们先不急着看三级缓存。

先跟一次源码。

看看循环依赖到底是在什么地方产生的。


什么是循环依赖?

所谓循环依赖。

本质上就是:

A 依赖 B

B 依赖 A

形成一个闭环。

例如:

@Service
public class UserService {

    @Autowired
    private OrderService orderService;
}
@Service
public class OrderService {

    @Autowired
    private UserService userService;
}

这种情况就属于循环依赖。

很多人觉得:

这是代码写错了。

实际上在大型项目里非常常见。

因为业务之间本来就存在关联。

用户查订单。

订单查用户。

写着写着就形成了依赖闭环。


如果没有 Spring 会怎样?

假设我们自己写代码:

AService a = new AService();

BService b = new BService();

a.setBService(b);

b.setAService(a);

是不是完全没问题?

因为对象已经创建出来了。

后面再互相引用即可。

所以循环依赖真正的问题不是:

A 引用 B

B 引用 A

而是:

创建 A 的时候需要 B

创建 B 的时候又需要 A

这才是 Spring 面临的问题。


从源码开始 Debug

项目启动时。

Spring 会先创建 Bean。

入口最终会来到:

AbstractBeanFactory#doGetBean()

继续进入:

AbstractAutowireCapableBeanFactory#createBean()

再往下:

AbstractAutowireCapableBeanFactory#doCreateBean()

这里就是 Bean 创建的核心方法。

打个断点。

看看里面做了什么。

首先:

BeanWrapper instanceWrapper =
        createBeanInstance(beanName, mbd, args);

执行完之后。

AService 已经实例化完成。

相当于:

AService aService = new AService();

此时对象已经存在。

但是:

@Autowired 还没执行

@PostConstruct 还没执行

初始化方法还没执行

也就是说:

对象创建了

但还没有初始化完成

真正的问题出现了

继续往下走。

会进入:

populateBean(beanName, mbd, instanceWrapper);

看到这个名字就能猜出来。

populate

填充

这里开始处理依赖注入。

例如:

@Autowired
private BService bService;

Spring 发现需要 BService。

于是再次调用:

getBean("bService");

开始创建 BService。

流程再次进入:

doCreateBean()

然后:

createBeanInstance()

实例化 BService。

接着:

populateBean()

开始注入属性。

结果发现:

@Autowired
private AService aService;

于是再次执行:

getBean("aService");

此时流程变成:

getBean(A)
    ↓
createBean(A)
    ↓
populateBean(A)
    ↓
getBean(B)
    ↓
createBean(B)
    ↓
populateBean(B)
    ↓
getBean(A)

看到这里。

问题就来了。


为什么没有无限递归?

按正常逻辑:

A 创建需要 B

B 创建需要 A

A 创建需要 B

B 创建需要 A
...

应该无限循环。

最后:

StackOverflowError

才对。

但 Spring 并没有。

项目依然启动成功。

为什么?

关键在于:

当第二次执行:

getBean("aService")

时。

Spring 并没有重新创建 AService。

而是用了另外一种办法。


Spring 发现了一个关键点

仔细观察刚才的流程。

第一次创建 AService 时:

createBeanInstance()

已经执行过了。

也就是说:

new AService()

其实已经完成。

虽然:

属性还没注入

初始化还没完成

但对象已经存在。

于是 Spring 想到一个办法:

既然对象已经创建出来了

能不能提前拿出来?

如果可以:

创建 A

↓

提前暴露 A

↓

创建 B

↓

B 拿到 A

↓

B 创建完成

↓

继续完成 A

循环依赖问题似乎就解决了。


真正难的地方是什么?

很多人以为:

循环依赖难点是:

A 引用 B

B 引用 A

其实不是。

真正难的是:

A 还没有初始化完成

Spring 能不能把它提前交给别人使用?

如果只是普通对象。

似乎问题不大。

但别忘了。

Spring 还有:

@Transactional
@Async
AOP

这些功能最终都会生成代理对象。

问题来了。

提前暴露出去的到底是:

原始对象?

还是:

代理对象?

如果处理不好。

最终可能出现:

B 里面拿到的是原始对象

Spring 容器保存的是代理对象

那整个框架就乱了。

所以循环依赖真正要解决的从来不是:

A 引用 B

B 引用 A

而是:

对象还没初始化完成

Spring 如何提前暴露它

并保证最终对象一致

总结

今天我们没有讲三级缓存。

而是先定位问题。

通过源码可以看到:

循环依赖产生的位置就在:

doCreateBean()

中的:

populateBean()

当 A 创建过程中需要 B。

B 创建过程中又需要 A。

循环依赖就出现了。

真正的问题是:

A 已经实例化

但还没有初始化完成

Spring 为什么还能把它交给 B 使用?

答案就藏在下一篇的源码里。


上一篇:

《@Transactional 为什么会失效?一次 Debug 看懂 Spring 踩过的所有坑》

下一篇:

《Spring 为什么必须三级缓存?看懂三级缓存才真正理解循环依赖》

如果这篇对你有帮助,欢迎点赞、收藏。

你在项目里遇到过循环依赖吗?

评论区聊聊。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐