记一次spring循环依赖报错
本文只是就遇到的循环依赖问题进行描述,对于spring的三级缓存,及其解决循环依赖方案,没有做出描述,如有需要,可参考别的文章讲解;
本以为看过spring相关bean注入流程,了解spring三级缓存,及其解决循环依赖的方式,感觉除了构造注入,其他并不会导致循环依赖报错。因为一直认为spring三级缓存,很好的解决了除构造注入之外的循环依赖问题;
昨天下午部署测试服务器,服务报循环依赖错误,无法启动;来来回回,搞了几个小时,算是正常启动了。本以为万事大吉,晚上启动,再次报错,又是几个小时的查找,就是认为项目中拦截器,包含构造方法注入,在搞鬼,改了其注入方式后,瞬间启动成功。
然而今天下午再次重启服务,昨天晚上修改的地方,再次报错,赶紧查看代码,昨天修改的地方,是不是被别人覆盖了,然而并没有,这个辣鸡问题,怎么这么顽强,我手这么臭吗,每次我启动就报错,心里一万个草泥马奔腾而过;
经过这两天的反复查找,开始怀疑自己的认知,以下是循环依赖的情况分析:
如上图所示几种不同的注入,产生循环依赖;
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注入流程大概流程如下:
可知构造注入,并未用到三级缓存;因而TestService1中的TestService2并不能被完全实例化,对应的TestService2中的TestService1也是同理;
单例的代理对象setter注入
这种方式,常见于利用@Async注解,实现方法的异步执行;@Async注解,spring会生成一个代理对象,这也是@Async注解的方法,在本类中调用不生效的原因;spring在注入有@Async注解的类时,大概流程如下图:
代码片段如下:
@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注解的类时,会在创建完成代理对象之后,与缓存中的对象对比,如果不是同一个的话,会提示换换依赖错误;因为缓存中存的是本类对象,不会与生成的代理对象相同。相关源码如下:
另:spring注入类时,加载也是有顺序的,如果上面的例子中,先加载TestService2,然后在加载TestService1,就不会有循环依赖错误,因为在加载TestService2时,初始化的过程中,会去创建TestService1,TestService1创建过程中,源码debuge解析如下:
getSingleton()方法如下
从缓存中取出的对象为null,如下:
具体的加载顺序测试,可以修改下类名,比如把 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注入,解决方案大概如下:
生成代理对象产生的循环依赖
这类循环依赖问题解决方法很多,主要有:
- 使用@Lazy注解,延迟加载
- 使用@DependsOn注解,指定加载先后关系
- 修改文件名称,改变循环依赖类的加载顺序使用
@DependsOn产生的循环依赖
这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题。
多例循环依赖
这类循环依赖问题可以通过把bean改成单例的解决。
构造器循环依赖
这类循环依赖问题可以通过使用@Lazy注解解决。
如上大部分可以通过@Lazy,懒加载方式解决,我们项目中反复出现循环依赖异常,大概率也是因为使用@Async,生成代理类引起的,起初一直以为是构造注入的原因,改了@Async修饰类的加载方式,目前项目没哟再出现异常;
我们项目中,之所以会有循环依赖问题,主要原因还是因为项目包含业务模块太多,各个模块耦合度较高,导致相互引用比较复查,从报错看,依赖链很长,造成排查循环依赖问题很麻烦,因此从系统设计角度考虑,能拆分的尽量早点拆分,降低耦合度,这也是解决循环依赖的一个思路;
此篇文章中的流程图及测试代码,主要参考www.zhihu.com/question/43… 感谢作者,文章写的比较详细;但有一处个人感觉说法欠妥,就是在讲解@Async产生代理对象,改变加载顺序,不会有循环依赖问题,在解释具体原因时,流程图里在加载TestService6时,把TestService6放入三级缓存后,初始化TestService6的属性TestService2时,会把TestService2放入二级缓存,接着应该是TestService6初始化完成,而不是流程图里的TestService2初始化完成,放入一级缓存;应该是TestService6初始化完成,放入一级缓存,再去把TestService2初始化完成,最后放入一级缓存。
以上也是个人理解,如有不妥,欢迎批评指正。