SpringBoot 解决Service互相调用导致的循环依赖问题 循环依赖源码debug @Lazy原理

前言

这段时间维护公司的一个springboot项目,开发功能的过程中,由于两个Service互相注入了对方的实例,导致项目启动时就报了循环依赖异常。循环依赖这个东西,平时只出现在了面试题里,这次被自己碰上了,就仔细去研究了一下解决方式,顺带着复习了循环依赖问题。这里就做一个分享,希望可以帮到大家,谢谢!

场景重现

这里就简单写两个Service,让它们互相注入实例,只是为了复现循环依赖的问题,业务逻辑就不写了。会出现两个Service互相调用的情况,也有可能是因为前期代码架构设计不合理,在这里就不讨论了。

AService

java 复制代码
public interface AService {
}
java 复制代码
@Service  
public class AServiceImpl implements AService {  
    @Autowired  
    private BService bService;  
}

BService

java 复制代码
public interface BService {
}
java 复制代码
@Service
public class BServiceImpl implements BService {
    @Autowired
    private AService aService;
}

项目中存在上述两个Service,在项目启动时,会抛出异常:

概念介绍

在介绍解决方案之前,我们先来回顾一下Spring的循环依赖问题。

在Spring框架中,bean的创建遵循着实例化->初始化的过程:

  1. 框架先通过不同类型的BeanDefinitionReader,去读取不同类型的bean定义信息,创建出BeanDefinition。

  2. 然后通过反射的方式,去实例化bean,此时的bean是一个不完整的bean,只有一个引用,内部的属性还未填充。

  3. 然后进入bean的初始化流程,通过populateBean方法,为bean去填充属性。完成这一步才完成了bean的创建。

Spring的循环依赖,简单来说就是两个及以上的bean之间,互相注入了对方的实例,导致在创建bean的时候,出现的死循环的情况。例如A B两个bean,框架在实例化A时,发现需要注入B,然后先去实例化B,然后在实例化B的过程中,又发现需要去注入A,然后再去实例化A,以此类推。

画个图理解一下

源码解读

下面在SpringBoot v2.7.6的环境下(对应Spring版本v5.3),跟踪一下这块的源码,对整个流程加深一下理解。

提供一张流程图,可以参考着看一下,帮助理解

refresh方法

这里从主启动类中的run方法开始:

调用了SpringApplication类中的重载run方法

最后调用了下面这个重载方法,在这个方法中,refreshContext这个方法的调用,作用是刷新上下文,Spring中的refresh方法就是从这里调用。

如下,在这个方法中,调用了一个本类中的refresh方法

这个方法中,进行了applicationContextrefresh方法调用

这个进入的是AbstractApplicationContext这个类,refresh方法就是在这个类中定义的。

在这个方法中,进行了finishBeanFactoryInitialization方法调用,该方法的作用是,完成剩余的单例bean的创建。

finishBeanFactoryInitialization方法中,通过调用beanFactory工厂对象的preInstantiateSingletons方法,完成剩余单例bean的创建。

preInstantiateSingletons这个方法中,就进行了所有还未实例化的单例bean的实例化,我们来看一下,具体的流程。

容器中不只有我们定义的两个Service,还有其他很多容器自带的bean,在这里我们加一个断点,同时进行一些设置,方便我们直接来观察AService的创建过程:

方法一开始,获取到了beanNames,这是一个List集合,里面封装了容器需要实例化的所有的bean的名称。

在处理过程中,先去判断了这个bean,是否 不是抽象&不是懒加载&是单例bean,是的话才进行该bean的实例化。

然后又判断了当前bean是否为FactoryBean,如果不是的话,直接调用getBean方法去获取实例,否则需要进行FactoryBean相关的处理。

FactoryBean:该类bean不受bean生命周期的控制,整个对象的创建过程是由用户自己来处理的,更加灵活。只需要调用getObject就可以返回具体的对象。

我们的AService是正常的需要进行生命周期管理的bean,所以直接进入getBean方法,该方法中进一步调用了doGetBean方法。

在Spring的源码中,凡是do开头的方法,一般都是真正干活的方法。例如doGetBean、doCreateBean等。

getSingleton方法

doGetBean方法中,第一步调用了getSingleton方法,目的是从一级缓存中尝试查找当前bean。

可以看到,调用了DefaultSingletonBeanRegistry类中的getSingleton方法之后,又调用了一个重载方法,第二个参数为allowEarlyReference,是一个布尔值。这个参数的含义其实是是否允许提前暴露对象引用

提前暴露对象引用:这是一种试图解开循环依赖闭环的方法。当A实例化之后,将这个不完整的对象的引用,放到一个Map集合中,然后去注入它的属性b;此时去实例化B,发现需要属性a,这时,就可以从Map集合中,获取到不完整的A对象,因为此时已经有了对象的引用了,完全可以在后续的流程中,根据引用来找到对象,再对其进行赋值。这样就避免了在注入属性的过程中,一直闭环了。

看一下重载的getSingleton方法,这里面用到了三个Map集合,这里其实就是所谓的三级缓存,是Spring用来解决循环依赖问题的关键。

上面的代码我们可以看出,当前方法的职责就是,从一级缓存(完整bean实例)以及二级缓存(不完整bean实例)中,查找当前bean。

  • 如果查找不到,且当前bean是在创建中的状态,说明他可能已经创建了不完整对象,这时再去二级缓存查找。
  • 如果二级缓存中还是没有,判断是否允许提前暴露对象引用,如果是,从三级缓存中获取当前bean的代理工厂对象,创建bean实例,放到二级缓存,同时清除三级缓存。
  • 最后返回从一级缓存或者二级缓存中读取的bean实例。

在容器获取bean实例的时候,会从一级缓存以及二级缓存中读取,如果当前bean 已经放到了二级缓存,这里就可以拿到,而不是进入之前提到的闭环,这里就跳出了循环依赖。

再回到doGetBean方法中

我们根据上述过程进行AService的获取,在这里获取到的肯定是一个null。

继续往下走,会再次调用另一个重载的getSingleton方法,这里还是尝试去获取bean实例。

不同的是,这里第二个参数 传入了一个lambda表达式,里面封装的是:如果还是获取不到bean实例,要进行的操作。可以看到这里调用了createBean方法:也就是说,如果这里还是获取不到bean实例,就要去创建bean实例了。

如下图,进入方法后,会再次从一级缓存中,尝试查找当前bean,如果还是没有,会调用传入lambda表达式的getObject方法,实际就是去调用了createBean方法,去创建bean实例。

createBean方法

createBean方法中,会进行doCreateBean方法的调用,之前说过凡是do开头的方法,一般都是真正干活的方法,所以这里就是要进行bean的创建了。

doCreateBean方法中,是通过一个instanceWrapper对象,来进行了bean的实例化,可以看到,首先通过调用createBeanInstance方法,去创建了bean实例,并且将创建后的实例,封装到了wrappedObject属性中,然后再调用getWrappedInstance方法,获取到了实例,这里是不完整的对象,还未填充属性

看一下createBeanInstance方法的具体实现:

最后一行调用了关键方法:instantiateBean

获取到实例化策略,调用了instantiate方法

调用了BeanUtilsinstantiateClass方法。

最后通过调用构造器对象的newInstance方法创建了实例

再回到doCreateBean方法中:

截至这里,bean的实例化过程结束了,现在获取到了一个不完整的bean实例,只创建了引用,还未填充属性,下面的逻辑就是通过populateBean方法填充属性了。

populateBean方法

如下图,在doCreateBean方法中,实例化对象之后,调用了populateBean方法

populateBean方法中,有一段关于BeanPostProcessor的处理逻辑,是去获取BeanPostProcessorCache对象,拿到里面的List集合instantiationAware,去遍历它。

这里的遍历出来的元素,个人理解应该是可以去应用到当前bean上面的后处理器,遍历出来挨个对当前bean进行处理。

其中有一个后处理器叫做AutowiredAnnotationBeanPostProcessor,个人理解是去扫描bean中@Autowired标注的属性,并把他们注册到bean中。这里看一下这个后处理器中对应的postProcessProperties方法:

该方法的第一行,调用findAutowiringMetadata方法,获取到了当前bean通过Autowired需要去注入的属性,可以看到是bService

接下来调用metadata对象的inject方法:方法中遍历elementsToIterate集合,集合中只有一个元素,就是AutowiredFieldElement、字面意思也就是Autowired字段元素,个人理解也就是被Autowired注解标注的属性。

这里再次调用了AutowiredFieldElementinject方法:

接着调用了resolveFieldValue方法,字面意思是去解析字段的值。

接着调用了beanFactoryresolveDependency方法

在这个方法中,调用了doResolveDependency,又一次看到do开头的方法,这就是真正干活的方法,跟进去

这个方法中,存在一行关键代码:descriptor.resolveCandidate

点进这个方法,可以看到,这里又调用了beanFactory对象的getBean方法,这次传入的beanName是BServiceImpl,也就是说要去获取BService,然后填充给AService中的属性bService。

接下来就跟上面的流程一样,去经历getBean、doGetBean、getSingleton、createBean、populateBean 方法等,最后依然会走到descriptor.resolveCandidate方法,此时的autowiredBeanName的值变成了AServiceImpl,也就是需要注入的bean的名称是AServiceImpl,要将它注入到BService中

然后又会走到beanFactory.getBean方法,传入的beanName这次是AServiceImpl,这里为了填充BService的属性,又要去获取AServiceImpl

再次走到doGetBean方法中的第二个getSingleton方法中时,会遇到一个重要的方法调用:beforeSingletonCreation方法,这里是在继续调用createBean方法去创建bean之前的一个判断。

如下图,传入的beanName为AServiceImpl,说明这里又是要去创建AService

beforeSingletonCreation方法中,会进行两个集合的元素判断,后一个是singletonsCurrentlyInCreation,看注释意思为当前正在创建过程中的bean名称,执行到这里,如果发现这个集合中有当前传进来的bean名称,那意思岂不是就是我正在创建bean,突然发现这个bean已经在创建中了,然后这里会抛出异常BeanCurrentlyInCreationException

走到这个异常类中可以发现,这里是发现了一个循环引用:

最后会包装成为一个UnsatisfiedDependencyException异常来抛出,最后变成InvocationTargetException直接导致程序终止。

以上就是SpringBoot中,出现循环依赖时的流程分析了。

解决方案

优化代码结构

重新设计代码结构或调用流程,尽量避免两个Service互相调用的过程。

@Lazy

在其中一个Service使用@Autowired注解注入另一个Service时,添加@Lazy注解,延迟它的加载。

java 复制代码
@Service
public class AServiceImpl implements AService {
    @Autowired
    @Lazy
    private BService bService;
}
java 复制代码
@Service  
public class BServiceImpl implements BService {  
    @Autowired  
    private AService aService;  
}

原理解析

使用了上面的@Lazy注解,可以发现 的确没有出现循环依赖的问题。那为什么加了一个注解 就可以避免呢?下面我们来深究一下原理:

@Autowired注解标注在某个属性上时,在populateBean方法执行到AutowiredAnnotationBeanPostProcessor这个Bean后处理器时,会调用其内部类AutowiredFieldElementresolveFieldValue方法,然后会调用到beanFactory对象的resolveDependency方法(上面的源码解读部分已经讲过了,不熟悉的朋友可以翻上去看一下)。

如果还同时标注了@Lazy注解,那么在这个方法中,有一个关键调用:

getLazyResolutionProxyIfNecessary这个方法,从字面意思来看,就是:如果必要,获取延迟代理对象。这个地方 实际上是获取了一个代理对象,来替换了本该获取的注入属性对象。

点进这个方法看一下:

可以看到,先调用了isLazy方法,来判断当前属性,是否标注了@Lazy注解,如果是的话,调用了buildLazyResolutionProxy方法,来创建了延迟代理对象。

再看一下这个方法:

后面的逻辑,就是创建了代理对象,当通过注入属性调用目标方法时,实际会调用到JdkDynamicAopProxy类的invoke方法中,实现目标方法的调用。(jdk相关原理,在其它文章有介绍,点击查看)。


综上所述 :标注了@Lazy注解之后,在注入属性时,不会再去容器中查找,而是创建一个代理对象,替代需要注入的属性。同时,当前bean也作为一个完整对象,放到容器中。后续目标方法的调用,则是通过代理对象完成。

后记

以上,就是关于Springboot中出现循环依赖问题的解析了。感谢你的阅读,以上内容来源于自己的自学,如果有不对的地方,欢迎在评论区指正!谢谢~

相关推荐
Albert Edison3 小时前
【最新版】IntelliJ IDEA 2025 创建 SpringBoot 项目
java·spring boot·intellij-idea
Piper蛋窝4 小时前
深入 Go 语言垃圾回收:从原理到内建类型 Slice、Map 的陷阱以及为何需要 strings.Builder
后端·go
六毛的毛7 小时前
Springboot开发常见注解一览
java·spring boot·后端
AntBlack7 小时前
拖了五个月 ,不当韭菜体验版算是正式发布了
前端·后端·python
31535669137 小时前
一个简单的脚本,让pdf开启夜间模式
前端·后端
uzong7 小时前
curl案例讲解
后端
开开心心就好8 小时前
免费PDF处理软件,支持多种操作
运维·服务器·前端·spring boot·智能手机·pdf·电脑
一只叫煤球的猫8 小时前
真实事故复盘:Redis分布式锁居然失效了?公司十年老程序员踩的坑
java·redis·后端
猴哥源码8 小时前
基于Java+SpringBoot的农事管理系统
java·spring boot
大鸡腿同学9 小时前
身弱武修法:玄之又玄,奇妙之门
后端