Spring循环依赖解决方案

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

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对象放入完成品缓存

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

目录
相关文章
|
2月前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo:微服务通信的高效解决方案
【10月更文挑战第15天】随着信息技术的发展,微服务架构成为企业应用开发的主流。Spring Cloud Dubbo结合了Dubbo的高性能RPC和Spring Cloud的生态系统,提供高效、稳定的微服务通信解决方案。它支持多种通信协议,具备服务注册与发现、负载均衡及容错机制,简化了服务调用的复杂性,使开发者能更专注于业务逻辑的实现。
72 2
|
15天前
|
Java Nacos Sentinel
Spring Cloud Alibaba:一站式微服务解决方案
Spring Cloud Alibaba(简称SCA) 是一个基于 Spring Cloud 构建的开源微服务框架,专为解决分布式系统中的服务治理、配置管理、服务发现、消息总线等问题而设计。
153 13
Spring Cloud Alibaba:一站式微服务解决方案
|
1月前
|
缓存 架构师 Java
图解 Spring 循环依赖,一文吃透!
Spring 循环依赖如何解决,是大厂面试高频,本文详细解析,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
图解 Spring 循环依赖,一文吃透!
|
19天前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
39 2
|
19天前
|
安全 Java API
实现跨域请求:Spring Boot后端的解决方案
本文介绍了在Spring Boot中处理跨域请求的三种方法:使用`@CrossOrigin`注解、全局配置以及自定义过滤器。每种方法都适用于不同的场景和需求,帮助开发者灵活地解决跨域问题,确保前后端交互顺畅与安全。
|
3月前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
231 24
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
|
2月前
|
缓存 Java Spring
源码解读:Spring如何解决构造器注入的循环依赖?
本文详细探讨了Spring框架中的循环依赖问题,包括构造器注入和字段注入两种情况,并重点分析了构造器注入循环依赖的解决方案。文章通过具体示例展示了循环依赖的错误信息及常见场景,提出了三种解决方法:重构代码、使用字段依赖注入以及使用`@Lazy`注解。其中,`@Lazy`注解通过延迟初始化和动态代理机制有效解决了循环依赖问题。作者建议优先使用`@Lazy`注解,并提供了详细的源码解析和调试截图,帮助读者深入理解其实现机制。
71 1
|
2月前
|
Java 关系型数据库 开发工具
idea创建不了spring2.X版本,无法使用JDK8,最低支持JDK17 , 如何用idea创建spring2.X版本,使用JDK8解决方案
本文提供了解决方案,如何在IDEA中创建Spring 2.X版本的项目并使用JDK8,尽管Spring 2.X已停止维护且IDEA不再直接支持,通过修改pom.xml或使用阿里云的国内源来创建项目。
140 0
idea创建不了spring2.X版本,无法使用JDK8,最低支持JDK17 , 如何用idea创建spring2.X版本,使用JDK8解决方案
|
3月前
|
缓存 Java Spring
手写Spring Ioc 循环依赖底层源码剖析
在Spring框架中,IoC(控制反转)是一个核心特性,它通过依赖注入(DI)实现了对象间的解耦。然而,在实际开发中,循环依赖是一个常见的问题。
46 4
|
3月前
|
Java 开发工具 对象存储
简化配置管理:Spring Cloud Config与Netflix OSS中的动态配置解决方案
简化配置管理:Spring Cloud Config与Netflix OSS中的动态配置解决方案
57 2