文章目录

  • 问题描述
  • 原因分析
  • 解决办法
  • 总结


问题描述

最近做项目时,遇到一个很诡异的问题:同样一份代码,在测试环境可以正常启动,但在生产环境却死活起不来,SpringBoot提示循环依赖。按理说,如果有循环依赖问题,测试环境就应该暴露出来了,为什么到生产环境才出现这个问题呢?对此做了一番初步研究。

原因分析

经排查发现,项目中确实存在循环依赖,如下所示。

程序中存在2个Bean:a和b。a构造器注入b,b属性注入a,两者形成循环依赖。

@Component
public class A {

    private final B b;

    @Autowired
    public A(B b) {
        this.b = b;
    }
}
@Component
public class B {

    @Autowired
    private A a;
}

是否会触发循环依赖报错,与Bean的加载顺序有关:

  • 如果先加载b,再加载a,可以正常启动;
  • 如果先加载a,再加载b,则会启动失败。

这个加载顺序跟程序所处的系统环境有关,不是固定的(默认情况下,Spring不保证Bean的加载顺序)。因此会出现,测试环境正常启动、生产环境启动失败的情况。

至于为什么先加载b再加载a可以正常启动、反之则不行,与Spring应对循环依赖问题的三级缓存机制有关:

  • 由于b是属性注入a,如果b先实例化,b在实例化后可以放入缓存,a实例化时从缓存中获取到b,a和b可以顺利完成初始化;
  • 而a是构造器注入b,如果a先实例化,a在实例化构造时要求b已经初始化完成,而b的初始化又依赖于a的实例化,此时a尚未实例化,也就无法从缓存中获取到,这样b和a都无法完成初始化。

解决办法

解决办法有很多,这里列出一些:

  • 在类A上标注@DependsOn("b")注解,强制指定b优先于a加载。
  • 在类A上标注@Lazy注解,让a懒加载。
  • a和b都改成属性注入,让Spring通过三级缓存机制化解循环依赖问题。
  • 重构类A和类B,让它们职责单一,避免循环依赖。

总结

SpringBoot中,Bean的注入方式不同,可能导致“偶现”的循环依赖报错。这里的“偶现”,是针对不同的系统环境而言,由于Bean的加载顺序不同,循环依赖报错可能出现,也可能不出现。为了避免类似的问题,程序开发时最好采用统一的依赖注入方式(建议构造器注入)。