乐趣区

关于spring:15张图带你彻底弄明白spring循环依赖再也不用怕了

1. 由共事抛的一个问题开始

最近项目组的一个共事遇到了一个问题,问我的意见,一下子引起的我的趣味,因为这个问题我也是第一次遇到。平时自认为对 spring 循环依赖问题还是比拟理解的,直到遇到这个和前面的几个问题后,从新刷新了我的意识。

咱们先看看过后出问题的代码片段:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {}
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {}
}

这两段代码中定义了两个 Service 类:TestService1 和 TestService2,在 TestService1 中注入了 TestService2 的实例,同时在 TestService2 中注入了 TestService1 的实例,这里形成了循环依赖。

只不过,这不是一般的循环依赖,因为 TestService1 的 test1 办法上加了一个 @Async 注解。

大家猜猜程序启动后运行后果会怎么?

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] 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.

报错了。。。起因是呈现了循环依赖。

「不迷信呀,spring 不是号称能解决循环依赖问题吗,怎么还会呈现?」

如果把下面的代码略微调整一下:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {}
}

把 TestService1 的 test1 办法上的 @Async 注解去掉,TestService1 和 TestService2 都须要注入对方的实例,同样形成了循环依赖。

然而重新启动我的项目,发现它可能失常运行。这又是为什么?

带着这两个问题,让咱们一起开始 spring 循环依赖的探秘之旅。

2. 什么是循环依赖?

循环依赖:说白是一个或多个对象实例之间存在间接或间接的依赖关系,这种依赖关系形成了形成一个环形调用。

第一种状况:本人依赖本人的间接依赖


第二种状况:两个对象之间的间接依赖


第三种状况:多个对象之间的间接依赖

后面两种状况的间接循环依赖比拟直观,十分好辨认,然而第三种间接循环依赖的状况有时候因为业务代码调用层级很深,不容易辨认进去。

3. 循环依赖的 N 种场景

spring 中呈现循环依赖次要有以下场景:

单例的 setter 注入

这种注入形式应该是 spring 用的最多的,代码如下:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {}
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {}
}

这是一个经典的循环依赖,然而它能失常运行,得益于 spring 的外部机制,让咱们根本无法感知它有问题,因为 spring 默默帮咱们解决了。

spring 外部有三级缓存:

  • singletonObjects 一级缓存,用于保留实例化、注入、初始化实现的 bean 实例
  • earlySingletonObjects 二级缓存,用于保留实例化实现的 bean 实例
  • singletonFactories 三级缓存,用于保留 bean 创立工厂,以便于前面扩大有机会创立代理对象。

上面用一张图通知你,spring 是如何解决循环依赖的:

                                            图 1

仔细的敌人可能会发现在这种场景中第二级缓存作用不大。

那么问题来了,为什么要用第二级缓存呢?

试想一下,如果呈现以下这种状况,咱们要如何解决?

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;
    @Autowired
    private TestService3 testService3;

    public void test1() {}
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {}
}
@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;

    public void test3() {}
}

TestService1 依赖于 TestService2 和 TestService3,而 TestService2 依赖于 TestService1,同时 TestService3 也依赖于 TestService1。

依照上图的流程能够把 TestService1 注入到 TestService2,并且 TestService1 的实例是从第三级缓存中获取的。

假如不必第二级缓存,TestService1 注入到 TestService3 的流程如图:

                                               图 2

TestService1 注入到 TestService3 又须要从第三级缓存中获取实例,而第三级缓存里保留的并非真正的实例对象,而是 ObjectFactory 对象。说白了,两次从三级缓存中获取都是 ObjectFactory 对象,而通过它创立的实例对象每次可能都不一样的。

这样不是有问题?

为了解决这个问题,spring 引入的第二级缓存。下面图 1 其实 TestService1 对象的实例曾经被增加到第二级缓存中了,而在 TestService1 注入到 TestService3 时,只用从第二级缓存中获取该对象即可。

                                                 图 3

还有个问题,第三级缓存中为什么要增加 ObjectFactory 对象,间接保留实例对象不行吗?

答:不行,因为如果你想对增加到三级缓存中的实例对象进行加强,间接用实例对象是行不通的。

针对这种场景 spring 是怎么做的呢?

答案就在 AbstractAutowireCapableBeanFactory 类 doCreateBean 办法的这段代码中:


它定义了一个匿名外部类,通过 getEarlyBeanReference 办法获取代理对象,其实底层是通过 AbstractAutoProxyCreator 类的 getEarlyBeanReference 生成代理对象。

多例的 setter 注入

这种注入办法偶尔会有,特地是在多线程的场景下,具体代码如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {}
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {}
}

很多人说这种状况 spring 容器启动会报错,其实是不对的,我十分负责任的通知你程序可能失常启动。

为什么呢?

其实在 AbstractApplicationContext 类的 refresh 办法中通知了咱们答案,它会调用 finishBeanFactoryInitialization 办法,该办法的作用是为了 spring 容器启动的时候提前初始化一些 bean。该办法的外部又调用了 preInstantiateSingletons 办法

标红的中央显著可能看出:非形象、单例 并且非懒加载的类能力被提前初始 bean。

而多例即 SCOPE_PROTOTYPE 类型的类,非单例,不会被提前初始化 bean,所以程序可能失常启动。

如何让他提前初始化 bean 呢?

只须要再定义一个单例的类,在它外面注入 TestService1

@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;
}

重新启动程序,执行后果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然呈现了循环依赖。

留神:这种循环依赖问题是无奈解决的,因为它没有用缓存,每次都会生成一个新对象。

结构器注入

这种注入形式是 spring4.x 以上的版本中官网举荐的形式,具体如下代码:

@Service
publicclass TestService1 {public TestService1(TestService2 testService2) {}}
@Service
publicclass TestService2 {public TestService2(TestService1 testService1) {}}

运行后果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

呈现了循环依赖,为什么呢?


从图中的流程看出结构器注入只是增加了三级缓存,并没有应用缓存,所以也无奈解决循环依赖问题。

单例的代理对象 setter 注入

这种注入形式其实也比拟罕用,比方平时应用:@Async注解的场景,会通过 AOP 主动生成代理对象。

我那位共事的问题也是这种状况。

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {}
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {}
}

从后面得悉程序启动会报错,呈现了循环依赖:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] 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.

为什么会循环依赖呢?

答案就在上面这张图中:

说白了,bean 初始化实现之后,前面还有一步去查看:第二级缓存 和 原始对象 是否相等。因为它对后面流程来说无关紧要,所以后面的流程图中省略了,然而在这里是关键点,咱们重点说说:

那位共事的问题正好是走到这段代码,发现第二级缓存 和 原始对象不相等,所以抛出了循环依赖的异样。

如果这时候把 TestService1 改个名字,改成:TestService6,其余的都不变。

@Service
publicclass TestService6 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {}
}

再重新启动一下程序,神奇般的好了。

what?这又是为什么?

这就要从 spring 的 bean 加载程序说起了,默认状况下,spring 是依照文件残缺门路递归查找的,按门路 + 文件名排序,排在后面的先加载。所以 TestService1 比 TestService2 先加载,而改了文件名称之后,TestService2 比 TestService6 先加载。

为什么 TestService2 比 TestService6 先加载就没问题呢?

答案在上面这张图中:

这种状况 testService6 中其实第二级缓存是空的,不须要跟原始对象判断,所以不会抛出循环依赖。

DependsOn 循环依赖

还有一种有些非凡的场景,比方咱们须要在实例化 Bean A 之前,先实例化 Bean B,这个时候就能够应用 @DependsOn 注解。

@DependsOn(value = "testService2")
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {}
}
@DependsOn(value = "testService1")
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {}
}

程序启动之后,执行后果:

Circular depends-on relationship between 'testService2' and 'testService1'

这个例子中原本如果 TestService1 和 TestService2 都没有加 @DependsOn 注解是没问题的,反而加了这个注解会呈现循环依赖问题。

这又是为什么?

答案在 AbstractBeanFactory 类的 doGetBean 办法的这段代码中:

它会查看 dependsOn 的实例有没有循环依赖,如果有循环依赖则抛异样。

4. 呈现循环依赖如何解决?

我的项目中如果呈现循环依赖问题,阐明是 spring 默认无奈解决的循环依赖,要看我的项目的打印日志,属于哪种循环依赖。目前蕴含上面几种状况:

生成代理对象产生的循环依赖

这类循环依赖问题解决办法很多,次要有:

  • 应用 @Lazy 注解,提早加载
  • 应用 @DependsOn 注解,指定加载先后关系
  • 批改文件名称,扭转循环依赖类的加载程序

应用 @DependsOn 产生的循环依赖

这类循环依赖问题要找到 @DependsOn 注解循环依赖的中央,迫使它不循环依赖就能够解决问题。

多例循环依赖

这类循环依赖问题能够通过把 bean 改成单例的解决。

结构器循环依赖

这类循环依赖问题能够通过应用 @Lazy 注解解决。

当然最好的解决循环依赖问题最佳计划是从代码设计上躲避,然而简单的零碎中有可能没法防止。

最初说一句(求关注,别白嫖我)

如果这篇文章对您有所帮忙,或者有所启发的话,帮忙扫描下发二维码关注一下,您的反对是我保持写作最大的能源。

求一键三连:点赞、转发、在看。

在公众号中回复:面试、代码神器、开发手册、工夫治理有超赞的粉丝福利,另外回复:加群,能够跟很多 BAT 大厂的前辈交换和学习。

本文由博客群发一文多发等经营工具平台 OpenWrite 公布

退出移动版