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中出现循环依赖问题的解析了。感谢你的阅读,以上内容来源于自己的自学,如果有不对的地方,欢迎在评论区指正!谢谢~

相关推荐
码农派大星。6 分钟前
Spring Boot 配置文件
java·spring boot·后端
江深竹静,一苇以航15 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
杜杜的man1 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*1 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu1 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s1 小时前
Golang--协程和管道
开发语言·后端·golang
为什么这亚子1 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算
豪宇刘1 小时前
SpringBoot+Shiro权限管理
java·spring boot·spring
想进大厂的小王1 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea