Spring循环依赖解决方案

简介: 之前我们对Spring Bean生命周期和Bean实例化、属性填充、初始化、销毁等整体流程进行全面分析与总结,不熟悉的可查看:Spring Bean生命周期。我们也提到在创建Bean过程中贯穿着循环依赖问题,Spring使用三级缓存解决循环依赖,这也是一个重要的知识点,所以我们下面就来看看Spring是如何使用三级缓存解决循环依赖的

1.概述

之前我们对Spring Bean生命周期和Bean实例化、属性填充、初始化、销毁等整体流程进行全面分析与总结,不熟悉的可查看:Spring Bean生命周期。我们也提到在创建Bean过程中贯穿着循环依赖问题,Spring使用三级缓存解决循环依赖,这也是一个重要的知识点,所以我们下面就来看看Spring是如何使用三级缓存解决循环依赖的。

什么是循环依赖?

循环依赖,也可以叫做循环引用,就是一个或者多个bean对象之间互相引用,存在依赖关系,大致相互引用情况如下:

由上可知,循环依赖其实就是一个闭环,像图中情况二Spring在创建单例bean A的时候发现引用了B,这时候就会去容器中查找单例bean B,发现没有然后就会创建bean B,创建bean B时又发现引用了bean A,这时候又会去容器中查找bean A,发现没有,接下来就会循环重复上面的步骤,这是不是像极了死锁?其实循环依赖就是一个死循环的过程。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

2.循环依赖案例

在讲述循环依赖示例前,我们先来看看什么是依赖注入?知道了依赖注入之后才能了解循环依赖出现的场景所在。

2.1 什么是依赖注入

依赖注入是Spring IOC(控制反转)模块的一个核心概念,DI (Dependency Injection):依赖注入是指在 Spring IOC 容器创建对象的过程中,将所依赖的对象通过配置进行注入,我们可以通过依赖注入的方式来降低对象间的耦合度。这里的配置注入可以基于XML配置文件也可以基于注解配置,当下注解配置开发是主流,所以在这里主要讨论基于注解的注入方式,基于注解的常规注入方式通常有三种:

  • 基于field属性注入
  • 基于setter方法注入
  • 基于构造器注入

接下来就让我们分别来看看这三种常规的注入方式。

field属性注入

这种方式是我们平时开发中使用最多的,原因是这种方式使用起来非常简单,代码更加简洁。

@Service
public class UserService {
    @Autowired
    private UserDAO userDAO;  //通过属性注入

}

setter方法注入

@Service
public class UserService {
    private UserDAO userDAO;

    @Autowired  //通过setter方法实现注入
    public void setUserDAO(serDAO userDAO) {
        this.userDAO = userDAO;
    }
}

构造器注入

@Service
public class UserService {
   
   
  private UserDAO userDAO;

    @Autowired //通过构造器注入
    public UserService(UserDAO userDAO) {
   
   
        this.userDAO = userDAO;
    }
}

2.2 循环依赖示例

首先需要强调一点,虽然Spring允许Bean对象的循环依赖,但事实上,项目中存在Bean的循环依赖,是Bean对象职责划分不明确、代码质量不高的表现,如果存在大量的Bean之间循环依赖,那么代码的整体设计也就越来越糟糕。所以SpringBoot在后续的版本中终于受不了这种滥用,默认把循环依赖给禁用了!从2.6版本开始,如果你的项目里还存在循环依赖,SpringBoot将拒绝启动!

我在2.7版本的Spring Boot中有两个Bean:UserService, RoleServiceUserService需要查询某个用户有哪些角色,RoleService需要查询某个角色关联了哪些用户,这样就形成了相互引用循环依赖啦,代码如下:

@Service
public class UserService {

    @Autowired
    private RoleService roleService;
}
@Service
public class RoleService {
    @Autowired
    private UserService userService;
}

启动项目报错如下:存在循环依赖

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  UserService (field private com.plasticene.fast.service.impl.RoleService com.plasticene.fast.service.impl.UserService.roleService)
↑     ↓
|  RoleService (field private com.plasticene.fast.service.impl.UserService com.plasticene.fast.service.impl.RoleService.userService)
└─────┘

接下来我们在配置文件中配置开启允许循环依赖

spring:
  main:
    allow-circular-references: true

项目就能正常启动了。

上面演示的基于field数据注入方式的循环依赖,在开启允许循环依赖的配置的情况下项目正常启动,接下来我们基于开启配置的情况改为构造器依赖注入看看:

@Service
public class UserService {
   
   

    private RoleService roleService;

    @Autowired
    public UserService(RoleService roleService) {
   
   
        this.roleService = roleService;
    }
}
@Service
public class RoleService {
   
   

    private UserService userService;

    @Autowired
    public RoleService(UserService userService) {
   
   
        this.userService = userService;
    }
}

在开启允许循环依赖的配置启动项目还是会报错。是的,对于构造器的循环依赖,Spring 是无法解决的,只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖

3.循环依赖解决方案

Spring解决循环依赖的核心思想在于提前曝光,使用三级缓存进行提前曝光。

DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中提供如下三个Map作为三级缓存:

public class DefaultSingletonBeanRegistry ... {
   
   
  //1、最终存储单例Bean成品的容器,即实例化和初始化都完成的Bean,称之为"一级缓存"
  Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
  //2、早期Bean单例池,缓存半成品对象,且当前对象已经被其他对象引用了,称之为"二级缓存"
  Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
  //3、单例Bean的工厂池,缓存半成品对象,对象未被引用,使用时在通过工厂创建Bean,称之为"三级缓存"
  Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
}

我们知道在项目启动时会进行Bean的加载、注入到Spring容器中,当创建某个Bean时发现引用依赖于另一个Bean,就会进行依赖查找,就会来到顶层接口BeanFactory的#getBean()方法,所以接下来看看AbstractBeanFactory的#doGetBean()的实现逻辑,发现首先会根据 beanName 从单例 bean 缓存中获取,如果不为空则直接返回

Object sharedInstance = getSingleton(beanName);

这个#getSingleton()是在DefaultSingletonBeanRegistry实现的:

    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
   
   
        // Quick check for existing instance without full singleton lock
        // 从单例缓存中加载bean
        Object singletonObject = this.singletonObjects.get(beanName);
        // 单例缓存中没有获取到bean,同时bean在创建中
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
   
   
            // 从 earlySingletonObjects 获取
            singletonObject = this.earlySingletonObjects.get(beanName);
            // 还是没有加载到bean,并且允许提前创建
            if (singletonObject == null && allowEarlyReference) {
   
   
                // 对单例缓存map加锁
                synchronized (this.singletonObjects) {
   
   
                    // Consistent creation of early reference within full singleton lock
                    // 再次从单例缓存中加载bean
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
   
   
                        // 再次从 earlySingletonObjects 获取
                        singletonObject = this.earlySingletonObjects.get(beanName);
                        if (singletonObject == null) {
   
   
                            // 从 singletonFactories 中获取对应的 ObjectFactory
                            ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                            if (singletonFactory != null) {
   
   
                                // 获得 bean
                                singletonObject = singletonFactory.getObject();
                                // 添加 bean 到 earlySingletonObjects 中
                                this.earlySingletonObjects.put(beanName, singletonObject);
                                // 从 singletonFactories 中移除对应的 ObjectFactory
                                this.singletonFactories.remove(beanName);
                            }
                        }
                    }
                }
            }
        }
        return singletonObject;
    }

这个方法就是从三级缓存中获取Bean对象,可以看到这里先从一级缓存singletonObjects中查找,没有找到的话接着从二级缓存earlySingletonObjects,还是没找到的话最终会去三级缓存singletonFactories中查找,需要注意的是如果在三级缓存中找到,

就会从三级缓存升级到二级缓存了。所以,二级缓存存在的意义,就是缓存三级缓存中的 ObjectFactory 的 #getObject() 方法的执行结果,提早曝光的单例 Bean 对象。

#getSingleton()返回空就会接着执行AbstractBeanFactory的#doGetBean()的下面逻辑,来到:

if (isPrototypeCurrentlyInCreation(beanName)) {
   
   
                throw new BeanCurrentlyInCreationException(beanName);
            }

可以看到原型模式的Bean循环依赖是直接报错,对于单例模式的Bean循环依赖Spring通过三级缓存提前曝光Bean来解决,因为单例Bean在整个容器中就一个,但是原型模式是每次都会创建一个新的Bean,无法使用缓存解决,所以直接报错了。

经过一系列代码之后还是没有当前查找的Bean,就会创建一个Bean,来到代码:

// 上面的缓存中没找到,需要根据不同的模式创建
// bean实例化
// Create bean instance.
if (mbd.isSingleton()) {
   
      // 单例模式
  sharedInstance = getSingleton(beanName, () -> {
   
   
    try {
   
   
      return createBean(beanName, mbd, args);
    }
    catch (BeansException ex) {
   
   
      // Explicitly remove instance from singleton cache: It might have been put there
      // eagerly by the creation process, to allow for circular reference resolution.
      // Also remove any beans that received a temporary reference to the bean.
      // 显式从单例缓存中删除 Bean 实例
      // 因为单例模式下为了解决循环依赖,可能他已经存在了,所以销毁它
      destroySingleton(beanName);
      throw ex;
    }
  });
  bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

最终来到了AbstractAutowireCapableBeanFactory的#createBean(),真正执行逻辑实现的是#doCreateBean()方法的里面代码片段如下所示:

// Eagerly cache singletons to be able to resolve circular references
        // even when triggered by lifecycle interfaces like BeanFactoryAware.
        // <4> 解决单例模式的循环依赖
        boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
                isSingletonCurrentlyInCreation(beanName));
        if (earlySingletonExposure) {
   
   
            if (logger.isTraceEnabled()) {
   
   
                logger.trace("Eagerly caching bean '" + beanName +
                        "' to allow for resolving potential circular references");
            }
            // 提前将创建的 bean 实例加入到 singletonFactories 中
            // 这里是为了后期避免循环依赖
            addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
        }

这里将创建的Bean工厂对象加入到 singletonFactories 三级缓存中,用来生成半成品的Bean并放入到二级缓存中,提前曝光bean意味着别的bean引用它时依赖查找就可以在前面的#getSingleton()中拿到当前bean直接返回啦,从而解决循环依赖

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
   
   
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
   
   
        if (!this.singletonObjects.containsKey(beanName)) {
   
   
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

可以看出,singletonFactories 这个三级缓存是解决 Spring Bean 循环依赖的重要所在。同时这段代码发生在 #createBeanInstance(...) 方法之后,也就是说这个 bean 其实已经被创建出来了,但是它还不是很完美(没有进行属性填充和初始化),但是对于其他依赖它的对象而言已经足够了(可以根据对象引用定位到堆中对象),能够被认出来了。所以 Spring 在这个时候将该对象提前曝光出来,可以被其他对象所使用。

当然也需要注意到addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean))() -> getEarlyBeanReference(beanName, mbd, bean)匿名函数调用,使用lambda方式生成一个ObjectFactory对象放到三级缓存中,提前曝光的是ObjectFactory对象,在被注入时才在ObjectFactory.getObject方式内实时生成代理对象,也就是调用#getEarlyBeanReference()进行实现的。

// 这里如果当前Bean需要aop代理增强,就是这里生成代理Bean对象的
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
   
   
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
   
   
        for (BeanPostProcessor bp : getBeanPostProcessors()) {
   
   
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
   
   
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
            }
        }
    }
    return exposedObject;
}

这也是为什么 Spring 需要额外增加 singletonFactories 三级缓存的原因,解决 Spring 循环依赖情况下的 Bean 存在动态代理等情况,不然循环注入到别人的 Bean 就是原始的,而不是经过动态代理的!

这里有个值得思考的问题:为什么要包装一层ObjectFactory对象存入三级缓存,说是为了解决Bean对象存在aop代理情况,那么直接生成代理对象半成品Bean放入二级缓存中,这样就可以不用三级缓存了!!!这么一说使用三级缓存的意义在哪里

首先需要明确一点:正常情况下(没有循环依赖),Spring都是在创建好完成品Bean之后才创建对应的代理对象。为了处理循环依赖,Spring有两种选择:

  1. 不管有没有循环依赖,都提前创建好代理对象,并将代理对象放入缓存,出现循环依赖时,其他对象直接就可以取到代理对象并注入。
  2. 不提前创建好代理对象,在出现循环依赖被其他对象注入时,才实时生成代理对象。这样在没有循环依赖的情况下,Bean就可以按着Spring设计原则的步骤来创建。

显然Spring使用了三级缓存,选择第二种方案,这是为啥呢?

原因是:如果要使用二级缓存解决循环依赖,意味着Bean在构造完后就创建代理对象,这样违背了Spring设计原则。Spring结合AOP跟Bean的生命周期,是在Bean创建完全之后通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来完成的,在这个后置处理的postProcessAfterInitialization方法中对初始化后的Bean完成AOP代理。如果出现了循环依赖,那没有办法,只有给Bean先创建代理,但是没有出现循环依赖的情况下,设计之初就是让Bean在生命周期的最后一步完成代理而不是在实例化后就立马完成代理。

经过实例化,初始化、属性赋值等操作之后,bean对象已经是一个完整的实例了,最终会调用DefaultSingletonBeanRegistry的#addSingleton()将完整bean放入一级缓存singletonObjects

protected void addSingleton(String beanName, Object singletonObject) {
   
   
    synchronized (this.singletonObjects) {
   
   
        this.singletonObjects.put(beanName, singletonObject);
        this.singletonFactories.remove(beanName);
        this.earlySingletonObjects.remove(beanName);
        this.registeredSingletons.add(beanName);
    }
}

到这里一个真真正正完整的Bean已经存入Spring容器中,可以随意被使用啦。

这里还涉及到一个比较细节的知识点,也是面试的一个考点:说说BeanFactory、FactoryBean及ObjectFactory三者的作用和区别?

BeanFactory: BeanFactory是IOC容器的核心接口,用于管理Bean的一个工厂接口类,主要功能有实例化、定位、配置应用程序中的对象及建立这些对象间的依赖

FactoryBean: 一般情况下,Spring 通过反射机制利用 bean 的 class 属性指定实现类来实例化 bean 。某些情况下,实例化 bean 过程比较复杂,如果按照传统的方式,则需要提供大量的配置信息,配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring 为此提供了一个 FactoryBean 的工厂类接口,用户可以通过实现该接口定制实例化 bean 的逻辑。FactoryBean在BeanFacotry的实现中有着特殊的处理,如果一个对象实现了FactoryBean 那么通过它get出来的对象实际是
factoryBean.getObject() 得到的对象,如果想得到FactoryBean必须通过在 '&' + beanName 的方式获取。

ObjectFactory: ObjectFactory则只是一个普通的对象工厂接口,从上面可以看到spring对ObjectFactory的应用之一就是,将创建对象 的步骤封装到ObjectFactory中,从而通过ObjectFactory在合适的时机创建合适的bean

4.总结

以上全部就是Spring对单例Bean的循环依赖的解决方案,核心就是使用三级缓存提前曝光Bean对象。两个Bean A,B互相引用循环依赖,Spring的解决过程如下:

  1. 通过构建函数创建A对象(A对象是半成品,还没注入属性和调用init方法)。
  2. A对象需要注入B对象,发现缓存里还没有B对象,将半成品对象A放入半成品缓存
  3. 通过构建函数创建B对象(B对象是半成品,还没注入属性和调用init方法)。
  4. B对象需要注入A对象,从半成品缓存里取到半成品对象A
  5. B对象继续注入其他属性和初始化,之后将完成品B对象放入完成品缓存
  6. A对象继续注入属性,从完成品缓存中取到完成品B对象并注入。
  7. A对象继续注入其他属性和初始化,之后将完成品A对象放入完成品缓存

最后附上一张源码执行流程图:(可自行放大查看)

目录
相关文章
|
6月前
|
人工智能 Java Spring
Spring Boot循环依赖的症状和解决方案
Spring Boot循环依赖的症状和解决方案
|
2月前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
40 4
|
6月前
|
设计模式 Java 开发者
解密Spring:优雅解决依赖循环的神兵利器
解密Spring:优雅解决依赖循环的神兵利器
484 57
|
4月前
|
缓存 Java 开发者
Spring循环依赖问题之Spring循环依赖如何解决
Spring循环依赖问题之Spring循环依赖如何解决
|
4月前
|
缓存 Java Spring
Spring循环依赖问题之Spring不支持构造器内的强依赖注入如何解决
Spring循环依赖问题之Spring不支持构造器内的强依赖注入如何解决
|
4月前
|
Java Spring
Spring循环依赖问题之构造器内的循环依赖如何解决
Spring循环依赖问题之构造器内的循环依赖如何解决
|
3月前
|
前端开发 Java 测试技术
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
|
6月前
|
缓存 Java 开发工具
【spring】如何解决循环依赖
【spring】如何解决循环依赖
209 56
|
4月前
|
JavaScript Java Maven
理解固化的Maven依赖:spring-boot-starter-parent 与 spring-boot-dependencies
理解固化的Maven依赖:spring-boot-starter-parent 与 spring-boot-dependencies
1913 1
|
4月前
|
Java Spring 容器
Spring循环依赖问题之两个不同的Bean A,导致抛出异常如何解决
Spring循环依赖问题之两个不同的Bean A,导致抛出异常如何解决