首页 > 其他分享 >Spring 如何解决循环依赖

Spring 如何解决循环依赖

时间:2024-01-18 11:25:22浏览次数:25  
标签:缓存 实例 Spring bean Bean 依赖 循环

目录

前言

什么是循环依赖?

循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。

  • 第一种情况:自己依赖自己的直接依赖

    image

  • 第二种情况:两个对象之间的直接依赖

    image

  • 第三种情况:多个对象之间的间接依赖

    image

Spring 创建 Bean 主要流程

实例化 Bean

// org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#doCreateBean
instanceWrapper = createBeanInstance(beanName, mbd, args);

主要是通过反射调用默认构造函数创建 Bean 实例,此时 Bean 的属性都还是默认值 null。被注解 @Bean 标记的方法就是此阶段被调用的。

填充 Bean 属性

// org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#doCreateBean
populateBean(beanName, mbd, instanceWrapper);

这一步主要是对 Bean 的依赖属性进行填充,对 @Value、@Autowired、@Resource 注解标注的属性注入对象引用。

调用 Bean 初始化方法

// org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java#doCreateBean
exposedObject = initializeBean(beanName, exposedObject, mbd);

调用配置指定中的 init 方法,例如,如果 xml 文件指定 Bean 的 init-method 方法或注解 @Bean(initMethod = "initMethod") 指定的方法。

BeanPostProcessor 接口拓展点

在 Bean 创建的流程中 Spring 提供了多个 BeanPostProcessor 接口方便开发者对 Bean 进行自定义调整和加工。

有以下几种 BeanPostProcessor 接口比较常用:

  • postProcessMergedBeanDefinition:可对 BeanDefinition 添加额外的自定义配置;

  • getEarlyBeanReference:返回早期暴露的 Bean 引用,一个典型的例子是循环依赖时如果有动态代理,需要在此先返回代理实例;

  • postProcessAfterInstantiation:在 populateBean 前用户可以手动注入一些属性;

  • postProcessProperties:对属性进行注入,例如配置文件加密信息在此解密后注入;

  • postProcessBeforeInitialization:属性注入后的一些额外操作;

  • postProcessAfterInitialization:实例完成创建的最后一步,这里也是一些 BPP 进行 AOP 代理的时机。

最后,对 Bean 的生命流程进行一个流程图的总结:

image

Spring 的动态代理(AOP)就是通过 BeanPostProcessor 实现的(图中的 3.4 步),其中 AbstractAutoProxyCreator 是十分典型的自动代理类,它实现了 SmartInstantiationAwareBeanPostProcessor 接口,并重写了 getEarlyBeanReference 和 postProcessAfterInitialization 两个方法实现代理的逻辑,这样完成对原始 Bean 进行增强,生成新 Bean 对象,将增强后的新 Bean 对象注入到属性依赖中。

Spring 解决循环依赖的方法

三级缓存

// org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
    // ...
    /** Cache of singleton objects: bean name to bean instance. */
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    /** Cache of singleton factories: bean name to ObjectFactory. */
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

    /** Cache of early singleton objects: bean name to bean instance. */
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
    // ...
}

三级缓存:

  • singletonObjects:一级缓存

    主要存放的是已经完成实例化、属性填充和初始化所有步骤的单例 Bean 实例,这样的 Bean 能够直接提供给用户使用,我们称之为终态 Bean 或叫成熟 Bean。

  • earlySingletonObjects:二级缓存

    主要存放的已经完成初始化,但属性还没自动赋值的 Bean,这些 Bean 还不能提供用户使用,只是用于提前暴露的 Bean 实例,我们把这样的 Bean 称之为临时 Bean 或早期的 Bean(半成品 Bean)。

  • singletonFactories:三级缓存

    存放的是 ObjectFactory 的匿名内部类实例,调用 ObjectFactory.getObject() 最终会调用 getEarlyBeanReference 方法,该方法可以获取提前暴露的单例 Bean 引用。

流程

spring 在创建 Bean A 的时候,会先去一级缓存(singletonObjects)查找,如果一级缓存没有,则再从二级缓存(earlySingletonObjects)中获取,如果二级缓存也没有,则再从三级缓存(singletonFactories)中获取。

如果还获取不到,则实例化一个 A,然后放入三级缓存,然后填充属性,此刻发现依赖 B,于是创建 B,同样的经过上述步骤,由于每级缓存都获取不到,于是实例化 B,然后填充属性,发现依赖 A,然后依次去每级缓存中获取,由于三级缓存中已经有一个 A,于是 B 可以顺利注入依赖,并被正确的初始化,然后递归返回,于是 A 也可以被正确的初始化了。

通过上述流程,可以看出 bean 都是需要先可以被实例化才可以的,所以,这也就是为什么构造器依赖可能会失败的原因。

例如,Bean A 的构造器依赖 B,而实例化 A 需要先调用 A 的构造函数,发现依赖 B,那么,需要再去初始化 B,但是,B 也依赖 A,不管 B 是通过构造器注入还是 setter 注入,此时,由于 A 没有被实例化,没有放入三级缓存,所以, B 无法被初始化,所以,spring 会直接报错。反之,如果 A 通过 setter 注入的话,那么,则可以通过构造函数先实例化,放入缓存,然后再填充属性,这样的话不管 B 是通过 setter 还是构造器注入 A,都能在缓存中获取到,于是可以初始化。

Spring 如何解决循环依赖

在 Spring 中,只有同时满足以下两点才能解决循环依赖的问题:

  • 依赖的 Bean 必须都是单例

  • 依赖注入的方式,必须不全是构造器注入,且 beanName 字母序在前的不能是构造器注入

    spring 的 bean 加载顺序:默认情况下,是按照文件完整路径递归查找的,按路径 + 文件名排序,排在前面的先加载。

为什么必须是单例

如果循环依赖的 Bean 是原型模式,会直接抛错,其源码如下:

// org/springframework/beans/factory/support/AbstractBeanFactory.java
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
    // ...

    // Fail if we're already creating this bean instance:
    // We're assumably within a circular reference.
    if (isPrototypeCurrentlyInCreation(beanName)) {
        throw new BeanCurrentlyInCreationException(beanName);
    }

    // ...
}

为什么无法支持原型对象呢?

因为原型模式都需要创建新的对象,不能用以前的对象。

如果 Bean A 和 Bean B 都是原型模式的话,那么,

  • 创建 A1 需要创建一个 B1;

  • 创建 B1 的时候要创建一个 A2;

  • 创建 A2 又要创建一个 B2;

  • 创建 B2 又要创建一个 A3;

  • 创建 A3 又要创建一个 B3

  • ...

就会陷入死循环。

如果是单例的话,创建 A 需要创建 B,而创建的 B 需要的还是之前的个 A。

为什么不能全是构造器注入

在 Spring 中创建 Bean 分三步:

  • 实例化:createBeanInstance,就是 new 了一个对象

  • 属性注入:populateBean, 就是 set 了一些属性值

  • 初始化:initializeBean,执行一些 aware 接口中的方法,initMethod、AOP 代理等

如果全是构造器注入,例如,A的构造器 A(B b),那就表明在 new 的时候,就需要得到 B,此时需要 new B(A a) 。

但是,B 也是要在构造的时候注入 A ,即 B(A a),这时候, B 需要在一个 map 中找到不完整的 A ,就会发现找不到,从而导致 Bean 创建失败。

为什么循环依赖需要三级缓存,二级不够吗

思考:如果在实例化 Bean A 之后,在二级 map 里面保存这个 A,然后继续属性注入。发现 A 依赖 B,所以要创建 Bean B,这时候, B 就能从二级缓存得到 A ,完成 B 的建立之后, Bean A 似乎也能完成实例化。

很明显,如果仅仅只是为了解决循环依赖,二级缓存够了,根本就不必要三级缓存。但是,如果我们希望对添加到三级缓存中的实例对象进行增强(例如,AOP),直接用实例对象是行不通的。

但是我们都知道对象如果有代理的话,那么,我们希望直接拿到的是代理对象。

也就是说如果 A 需要被代理,那么, B 依赖的 A 是已经被代理的 A,所以,我们不能返回 A 给 B,而是返回 A 的代理对象给 B。

其核心的代码实现如下:

// org/springframework/beans/factory/support/AbstractBeanFactory.java
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
    // ...

    // Eagerly cache singletons to be able to resolve circular references
    // even when triggered by lifecycle interfaces like BeanFactoryAware.
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
            isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // ...
}

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    // 判断是否有后置处理器
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        // Bean需要被AOP代理
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
            // 单例 Bean 创建完毕,将bean放入一级缓存
            this.singletonFactories.put(beanName, singletonFactory);
            // 清理二级、三级缓存
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

三级工厂的作用就是判断这个对象是否需要代理,如果否则直接返回,如果是则返回代理对象。

为什么代理对象没有放在二级缓存中

通常代理对象的生成是基于后置处理器,是在被代理的对象初始化后期调用生成的,所以,如果我们提早代理了其实是违背了 Bean 定义的生命周期。所以, Spring 先在一个三级缓存放置一个工厂,如果产生循环依赖,那么,就调用这个工厂提早得到代理对象。

如果没产生依赖,这个工厂根本不会被调用,所以,Bean 的生命周期就是对的。

循环依赖的场景

spring 中出现循环依赖主要有以下场景:

image

单例的 setter 注入

  • EmployeeService
public class EmployeeService {
    @Autowired
    private CompanyService companyService;

    @Override
    public String toString() {
        return "EmployeeService";
    }
}
  • CompanyService
@Service
public class CompanyService {
    @Autowired
    private EmployeeService employeeService;

    @Override
    public String toString() {
        return "CompanyService";
    }
}

这是一个经典的循环依赖,但是它能正常运行,得益于 spring 的内部机制,让我们根本无法感知它有问题,因为 spring 默默帮我们解决了。

spring内部有三级缓存:

  • singletonObjects:一级缓存,用于保存实例化、注入、初始化完成的bean实例

  • earlySingletonObjects:二级缓存,用于保存实例化完成的bean实例

  • singletonFactories:三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

spring 解决循环依赖的流程如下:

image

多例的 setter 注入

构造器注入

单例的代理对象setter注入

这种注入方式其实也比较常用,例如,使用 @Async 注解的场景,会通过 AOP 自动生成代理对象。

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}

@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

这种情况下的循环依赖,spring 就无法解决,就会导致启动报错。

它的启动过程如下:

image

bean初始化完成之后,后面还有一步去检查:第二级缓存 和 原始对象 是否相等。

如果这时候把TestService1改个名字,改成 TestService6,其他的都不变,就可以解决启动依赖报错的问题了:

@Service
publicclass TestService6 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}

这种情况下,TestService2 会比 TestService6 先加载,这种情况下就不会有问题

image

DependsOn循环依赖

出现循环依赖如何解决

image

生成代理对象产生的循环依赖

  • 使用 @Lazy 注解,延迟加载

  • 使用 @DependsOn 注解,指定加载先后关系

  • 修改文件名称,改变循环依赖类的加载顺序

使用 @DependsOn 产生的循环依赖

多例循环依赖

  • 把 bean 改成单例

构造器循环依赖

  • 使用 @Lazy 注解

总结

Spring 处理循环依赖的方式:

  • 有构造器注入,不一定会产生问题,具体得看是否都是构造器注和 BeanName 的字母序;

  • 如果单纯为了打破循环依赖,不需要三级缓存,两级就够了;

  • 三级缓存是否为延迟代理的创建,尽量不打破 Bean 的生命周期。


参考:

标签:缓存,实例,Spring,bean,Bean,依赖,循环
From: https://www.cnblogs.com/larry1024/p/17775288.html

相关文章

  • SpringBoot中操作Bean的生命周期的方法
    SpringBoot中操作Bean的生命周期的方法路人路人甲Java2024-01-1719:17发表于上海引言在SpringBoot应用中,管理和操作Bean的生命周期是一项关键的任务。这不仅涉及到如何创建和销毁Bean,还包括如何在应用的生命周期中对Bean进行精细控制。Spring框架提供了多种机制来......
  • spring boot 3.2.1 dremio jdbc jprofiler 集成
    jprofiler可以直接与idea集成,对于分析一些实际需要debug但是不好复现的问题还是比较方便的,以下是一个简单的与dremio集成的,springboot使用了3.2(jdk需要17)同时也会包含一些启动说明安装idea插件直接plugins的市场中搜索安装就可以了,之后就是配置了idea启动配置因......
  • Sa-Token介绍与SpringBoot环境下使用
    个人博客:无奈何杨(wnhyang)个人语雀:wnhyang共享语雀:在线知识共享Github:wnhyang-Overview官网:Sa-Token一个轻量级Java权限认证框架,让鉴权变得简单、优雅!介绍Sa-Token是一个轻量级Java权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话......
  • 每个Go程序员必犯之错之切片循环错误
    每个Go程序员必犯之错原创 晁岳攀(鸟窝) 鸟窝聊技术 2023-12-1808:48 发表于北京 听全文说起每个程序员必犯的错误,那还得是"循环变量"这个错误了,就连Go的开发者都犯过这个错误,这个错误在Go的FAQ中也有提到Whathappenswithclosuresrunningasgoroutines?[......
  • 使用ChatGPT解决在Spring AOP中@Pointcut中的execution如何指定Controller的所有方法
    背景使用ChatGPT解决工作中遇到的问题,https://xinghuo.xfyun.cn/desk切指定类在SpringAOP中,@Pointcut注解用于定义切点表达式,而execution属性用于指定切点表达式的具体匹配规则。要指定Controller的所有方法,可以使用以下方法:使用类名和方法名进行精确匹配。例如,如果要匹配名......
  • Spring 事务的概念
    ①什么是事务数据库事务(transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。②事务的特性A:原子性(Atomicity)一个事务(transaction)中的所......
  • springMVC重定向和转发区别
    请求转发是浏览器一次发出请求,获取一次相应,重定向是二次。请求地址栏未变,转发地址栏变请求获取用户提交的数据,重定向不可以获取用户提交数据,但可以获取第二次由浏览器携带的数据请求转发是在服务器端内部完成的,它将请求从一个Servlet转发到另一个Servlet或JSP页面,浏览器......
  • springMVC执行流程是啥
    用户发送请求,前端控制器DIspathServlet 2.DispathcherServlet收到请求调用HanderMappingc处理映射器3.处理映射器找到具体的处理器,根据xml配置注解查找返回给dispathServlet4.DispathServlet调用HandlerAdapter处理器找到Coltrller5.controller执行完毕返回modleAndView.......
  • springMvc如何解决请求中文乱码问题
    方式一:解决get请求中文乱码问题  每次请求前用encode对url进行编码方式二:在应用服务器上配置URL编码格式,在tomcat配置文件server.xml增加encodeURL编码格式,然后重启解决post请求方式一:使用spring提供的编码过器 在web.xml文件配置编码过lu器,增加一下配置: <web-ap......
  • 多模块之间的循环依赖:java: Annotation processing is not supported for module cycl
    问题描述java:Annotationprocessingisnotsupportedformodulecycles.Pleaseensurethatallmodulesfromcycle[BDCloud-business,BDCloud-admin]areexcludedfromannotationprocessing  本质:BDCloud-admin模块为主启动模块,其包含了BDCloud-business模块;但在......