spring循环依赖报错问题

1,936 阅读6分钟

记一次spring循环依赖报错

本文只是就遇到的循环依赖问题进行描述,对于spring的三级缓存,及其解决循环依赖方案,没有做出描述,如有需要,可参考别的文章讲解;
本以为看过spring相关bean注入流程,了解spring三级缓存,及其解决循环依赖的方式,感觉除了构造注入,其他并不会导致循环依赖报错。因为一直认为spring三级缓存,很好的解决了除构造注入之外的循环依赖问题;
昨天下午部署测试服务器,服务报循环依赖错误,无法启动;来来回回,搞了几个小时,算是正常启动了。本以为万事大吉,晚上启动,再次报错,又是几个小时的查找,就是认为项目中拦截器,包含构造方法注入,在搞鬼,改了其注入方式后,瞬间启动成功。
然而今天下午再次重启服务,昨天晚上修改的地方,再次报错,赶紧查看代码,昨天修改的地方,是不是被别人覆盖了,然而并没有,这个辣鸡问题,怎么这么顽强,我手这么臭吗,每次我启动就报错,心里一万个草泥马奔腾而过;
经过这两天的反复查找,开始怀疑自己的认知,以下是循环依赖的情况分析:

企业微信截图_93b94da9-4dfd-4f66-ae57-c2ade99dd09e.png 如上图所示几种不同的注入,产生循环依赖;

1.单例set注入

这种注入方式,是我们在开发中比较常用的;该注入方式产生的循环依赖,spring三级缓存,很好的自动帮咱们解决了,一般不会导致项目无法启动;

2.多例的setter注入

这种注入方式是指,bean的作用域为prototype,即每次用到时都会创建一个新的对象,是相对于上面的单例而言,spring三级缓存,解决的是单例循环引用问题,因而在这种多例中的循环引用,spring并没有解决,在日常的开发中,多例注入用的相对也较少;

3.构造器注入

该注入方式,是类中指定了,有参的构造函数注入;spring在实例化注入类时,用的是Java反射,调用对应的构造方法,创建对象;如果没有指定的构造方法,调用的是无参构造函数;有指定的有参构造方法,会调用指定的有参构造方法;在调用有参构造时,参数必须是初始化完成的; 代码示例:

public class TestService1 {

    public TestService1(TestService2 testService2) {
    }
}

@Service
public class TestService2 {

    public TestService2(TestService1 testService1) {
    }
}

spring注入流程大概流程如下:

企业微信截图_56d71a84-62b0-4f42-a717-d80c12c2bd40.png 可知构造注入,并未用到三级缓存;因而TestService1中的TestService2并不能被完全实例化,对应的TestService2中的TestService1也是同理;

单例的代理对象setter注入

这种方式,常见于利用@Async注解,实现方法的异步执行;@Async注解,spring会生成一个代理对象,这也是@Async注解的方法,在本类中调用不生效的原因;spring在注入有@Async注解的类时,大概流程如下图:

企业微信截图_261696a1-fe39-4abb-adc1-348f9fab8345.png 代码片段如下:

@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }

    public TestService1() {
        //spring实例化该类时,会默认调用该构造函数,此处打印是为了确认TestService1和TestService2的构建顺序
        System.out.println("************** TestService1 ***************");
    }
}

@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;

    //@Async
    public void test2() {
    }
    public TestService2() {
    //spring实例化该类时,会默认调用该构造函数,此处打印是为了确认TestService1和TestService2的构建顺序
        System.out.println("************** TestService2 ***************");
    }
}

如上面的流程图所示,spring在构建包含@Async注解的类时,会在创建完成代理对象之后,与缓存中的对象对比,如果不是同一个的话,会提示换换依赖错误;因为缓存中存的是本类对象,不会与生成的代理对象相同。相关源码如下:

image.png

另:spring注入类时,加载也是有顺序的,如果上面的例子中,先加载TestService2,然后在加载TestService1,就不会有循环依赖错误,因为在加载TestService2时,初始化的过程中,会去创建TestService1,TestService1创建过程中,源码debuge解析如下:

image.png getSingleton()方法如下

image.png 从缓存中取出的对象为null,如下:

image.png 具体的加载顺序测试,可以修改下类名,比如把 TestService1 改成 TestService6 ,然后再看 TestService6 与 TestService2 的启动顺序,来进行验证;

DependsOn循环依赖

还有一种有些特殊的场景,比如我们需要在实例化Bean A之前,先实例化Bean B,这个时候就可以使用@DependsOn注解。

@Service
public class TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}

@DependsOn(value = "testService1")
@Service
public class TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

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

如何解决循环依赖

根据上面的描述,能产生循环依赖异常的,有构造注入、多例注入、生成代理类的注入、以及DependsOn注入,解决方案大概如下:

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

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

  1. 使用@Lazy注解,延迟加载
  2. 使用@DependsOn注解,指定加载先后关系
  3. 修改文件名称,改变循环依赖类的加载顺序使用

@DependsOn产生的循环依赖

这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题。

多例循环依赖

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

构造器循环依赖

这类循环依赖问题可以通过使用@Lazy注解解决。

如上大部分可以通过@Lazy,懒加载方式解决,我们项目中反复出现循环依赖异常,大概率也是因为使用@Async,生成代理类引起的,起初一直以为是构造注入的原因,改了@Async修饰类的加载方式,目前项目没哟再出现异常;

我们项目中,之所以会有循环依赖问题,主要原因还是因为项目包含业务模块太多,各个模块耦合度较高,导致相互引用比较复查,从报错看,依赖链很长,造成排查循环依赖问题很麻烦,因此从系统设计角度考虑,能拆分的尽量早点拆分,降低耦合度,这也是解决循环依赖的一个思路;

此篇文章中的流程图及测试代码,主要参考www.zhihu.com/question/43… 感谢作者,文章写的比较详细;但有一处个人感觉说法欠妥,就是在讲解@Async产生代理对象,改变加载顺序,不会有循环依赖问题,在解释具体原因时,流程图里在加载TestService6时,把TestService6放入三级缓存后,初始化TestService6的属性TestService2时,会把TestService2放入二级缓存,接着应该是TestService6初始化完成,而不是流程图里的TestService2初始化完成,放入一级缓存;应该是TestService6初始化完成,放入一级缓存,再去把TestService2初始化完成,最后放入一级缓存。

以上也是个人理解,如有不妥,欢迎批评指正。

参考: www.zhihu.com/question/43…