浅谈循环依赖
说明
-
循环依赖是一个大家讨论很多的话题,它更多是一个工程上的问题而不是技术问题,我们需要首先有一定的认知:
- 如同两个人相互帮忙,两个类之间你调用我的,我调用你的是很正常也很自然的需求模型。
- 单一依赖确实有好处,改动一个最顶层类时不需要在意对底部类的影响,但是从本来就自然的模型非要理顺的话就需要额外付出代价,例如额外的拆分类。
-
循环依赖可以分这几种:
- 从小的来说是类之间的相互引用。
- 再大一点的来说是同一个项目下不同模块之间的引用。
- 再再大一点的来说涉及到微服务或不同类库之间引用。
对于微服务级别或是模块级别的引用来说解决循环依赖是有必要的,因为这可能牵扯到不同人分工协作的问题,而类之间尤其是同一模块下的类之间是否禁止循环依赖实际上是有争议的,本文只讨论同一模块下的类之间的循环引用。
-
一些解决循环依赖的方法类似
@Lazy
、getBean
等实际上解决的不是循环依赖,而是解决的springboot
启动时的循环依赖检测,但本质上它们还是相互引用,所以这里不讨论这些方法,只讨论拆分类的方法。 -
个人目前比较认同的是同一个人写的同一个模块下的功能是可以循环依赖的,增加额外的拆分类反而会增加复杂度及影响效率,但
springboot 2.6
版本之后默认禁止了循环依赖,所以个人也在思考,如果想要拆分的化要怎么拆分,目前总结了如下两种不同类型的循环引用拆分示例。
示例
情形一
-
最常见的
老师
、学生
这种或是主表
、子表
相互关联的:@Component public class Teacher { @Autowired private Student student; public void method() { //获取某教师下学生类别 List<String> students = student.getStudentsByTeacher("xxx"); System.out.println(students); } }
@Component public class Student { @Autowired private Teacher teacher; public void method() { //获取学生归属的教师 String teacherStr = teacher.getTeacherByStudent("xxx"); System.out.println(teacherStr); } }
-
这种拆分比较简单,类似数据库多对多的中间表,我们也创建一个中间类,然后
Teacher
和Student
类不要依赖彼此,直接抽取方法到中间类中或是都引用中间类:@Component public class TeacherStudent { @Autowired private Teacher teacher; @Autowired private Student student; public void method1() { //获取某教师下学生类别 List<String> students = student.getStudentsByTeacher("xxx"); System.out.println(students); } public void method2() { //获取学生归属的教师 String teacherStr = teacher.getTeacherByStudent("xxx"); System.out.println(teacherStr); } }
情形二
-
另一种常用的场景是引用第三方类库
A
,然后在配置类B
中用@Bean
来实例化,而类A
是通过读取数据库中的配置(通过类C
)来组装参数,而当数据库配置变更时(类C
中更新),由于参数变化同时也要重置类A
实例,我们在集成微信、钉钉等SDK时会经常遇到此情况,如果直接按照此逻辑写的话,就是下述的代码://B本身是个配置类 @Configuration class B { @Autowired private C c; @Bean public A init(){ A a = new A(); //引用c的数据库中数据来组装成A实例 a.setProp(c.getProp()); return a; } }
@Component class C { @Autowired private A a; public void update(){ //修改数据库相关后,又来重置A实例 a.reset(); } }
-
此情况下最主要的耦合就是
类A
需要类C
的数据来作为配置项,所以把这个耦合独立出来,而类B
中去除类C
的引用,仅仅是生成类A
的bean
://用@PostConstruct @Component public class D { @Autowired private A a; @Autowired private C c; @PostConstruct public void init(){ a.setProp(c.getProp()); } } //或是@Autowired注解到方法上 @Component public class D { @Autowired public void init(A a, C c) { a.setProp(c.getProp()); } }
-
虽然从需求上
类A
依赖类C
,但本质上类A
并不需要依赖任何类,和第一种情况不同的是类A
是一个第三方的类库,我们无法修改其引用及方法,而其本身并不是个bean
,需要我们额外去操作。
结果
- 上述的情况都是额外增加一个拆分类来处理,这样无形中增加了代码量,尤其是第一种情形太常见了,除非是项目初始时就规定好禁止
service
层互相调用,而是单独再划分一层来处理(类似阿里的manager
层),否则的话个人宁愿用@Lazy
来解决掉循环依赖的报错。 - 解决循环依赖上述同模块内的相对简单些,只是增加代码量而已,当涉及到模块或微服务时,则完全不一样,要考虑业务逻辑及架构等一系列问题,感觉很是麻烦。
- 以上只是个人见解,有更好的观点可发到评论区一起讨论下。