文章目录
- 问题描述
- 原因分析
- 解决办法
- 总结
问题描述
最近做项目时,遇到一个很诡异的问题:同样一份代码,在测试环境可以正常启动,但在生产环境却死活起不来,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的加载顺序不同,循环依赖报错可能出现,也可能不出现。为了避免类似的问题,程序开发时最好采用统一的依赖注入方式(建议构造器注入)。