likes
comments
collection
share

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

作者站长头像
站长
· 阅读数 9

前言

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

场景重现

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

AService

public interface AService {
}
@Service  
public class AServiceImpl implements AService {  
    @Autowired  
    private BService bService;  
}

BService

public interface BService {
}
@Service
public class BServiceImpl implements BService {
    @Autowired
    private AService aService;
}

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

概念介绍

在介绍解决方案之前,我们先来回顾一下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 解决Service互相调用导致的循环依赖问题 @Lazy

源码解读

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

refresh方法

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

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

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

getSingleton方法

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

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

再回到doGetBean方法中

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

createBean方法

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

调用了BeanUtilsinstantiateClass方法。

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

再回到doCreateBean方法中:

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

populateBean方法

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

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

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

这里再次调用了AutowiredFieldElementinject方法:

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

接着调用了beanFactoryresolveDependency方法

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

springboot 解决Service互相调用导致的循环依赖问题 @Lazy

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

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

解决方案

  1. 重新设计代码结构,尽量避免两个Service互调的过程。
  2. 在其中一个Service使用@Autowired注解注入另一个Service时,添加@Lazy注解,延迟它的加载。
    @Service
    public class AServiceImpl implements AService {
        @Autowired
        @Lazy
        private BService bService;
    }
    
    @Service  
    public class BServiceImpl implements BService {  
        @Autowired  
        private AService aService;  
    }
    

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

转载自:https://juejin.cn/post/7352387633764679714
评论
请登录