spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖 (上)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: spring5源码系列--循环依赖 之 手写代码模拟spring循环依赖

本次目标


1. 手写spring循环依赖的整个过程

2. spring怎么解决循环依赖

3. 为什么要二级缓存和三级缓存

4. spring有没有解决构造函数的循环依赖

5. spring有没有解决多例下的循环依赖.


一.  什么是循环依赖?



如下图所示:

1187916-20201104101251598-269899241.png


A类依赖了B类, 同时B类有依赖了A类. 这就是循环依赖, 形成了一个闭环


1187916-20201104101327208-179153318.png


如上图: A依赖了B, B同时依赖了A和C , C依赖了A. 这也是循环依赖. , 形成了一个闭环

 

那么, 如果出现循环依赖, spring是如何解决循环依赖问题的呢?


二. 模拟循环依赖



2.1 复现循环依赖


我们定义三个类:


1. 新增类InstanceA

package com.lxl.www.circulardependencies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class InstanceA {
    @Autowired
    private InstanceB instanceB;
    public InstanceA() {
        System.out.println("调用 instanceA的构造函数");
    }
    public InstanceA(InstanceB instanceB) {
        this.instanceB = instanceB;
    }
    public void say(){
        System.out.println( "I am A");
    }
    public InstanceB getInstanceB() {
        return instanceB;
    }
    public void setInstanceB(InstanceB instanceB) {
        this.instanceB = instanceB;
    }
}

这是InstanceA, 里面引用了InstanceB.

 

2. 新增类instanceB

package com.lxl.www.circulardependencies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class InstanceB {
    @Autowired
    private InstanceA instanceA;
    public InstanceB() {
        System.out.println("调用 instanceB的构造函数");
    }
    public InstanceA getInstanceA() {
        return instanceA;
    }
    public void setInstanceA(InstanceA instanceA) {
        this.instanceA = instanceA;
    }
}

这是InstanceB, 在里面有引用了InstanceA


3:模拟spring是如何创建Bean的


这个在前面已经说过了, 首先会加载配置类的后置处理器, 将其解析后放入到beanDefinitionMap中. 然后加载配置类, 也将其解析后放入beanDefinitionMap中. 最后解析配置类. 我们这里直接简化掉前两步, 将两个类放入beanDefinitionMap中. 主要模拟第三步解析配置类. 在解析的过程中, 获取bean的时候会出现循环依赖的问题循环依赖.


第一步: 将两个类放入到beanDefinitionMap中

public class MainStart {
    private static Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();     /**
     * 读取bean定义, 当然在spring中肯定是根据配置 动态扫描注册的
     *
     * InstanceA和InstanceB都有注解@Component, 所以, 在spring扫描读取配置类的时候, 会把他们两个扫描到BeanDefinitionMap中.
     * 这里, 我们省略这一步, 直接将instanceA和instanceB放到BeanDefinitionMap中.
     */
    public static void loadBeanDefinitions(){
        RootBeanDefinition aBeanDefinition = new RootBeanDefinition(InstanceA.class);
        RootBeanDefinition bBeanDefinition = new RootBeanDefinition(InstanceB.class);
        beanDefinitionMap.put("instanceA", aBeanDefinition);
        beanDefinitionMap.put("instanceB", bBeanDefinition);
    }
    public static void main(String[] args) throws Exception {
        // 第一步: 扫描配置类, 读取bean定义
        loadBeanDefinitions();
......
}

上面的代码结构很简单, 再看一下注释应该就能明白了. 这里就是模拟spring将配置类解析放入到beanDefinitionMap的过程.

 

第二步: 循环创建bean


首先,我们已经知道, 创建bean一共有三个步骤: 实例化, 属性赋值, 初始化.

1187916-20201106051846487-1478345934.png

而在属性赋值的时候, 会判断是否引用了其他的Bean, 如果引用了, 那么需要构建此Bean. 下面来看一下代码

/**
     * 获取bean, 根据beanName获取
     */
    public static Object getBean(String beanName) throws Exception {/**
         * 第一步: 实例化
         * 我们这里是模拟, 采用反射的方式进行实例化. 调用的也是最简单的无参构造函数
         */
        RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionMap.get(beanName);
        Class<?> beanClass = beanDefinition.getBeanClass();
        // 调用无参的构造函数进行实例化
        Object instanceBean = beanClass.newInstance();
       /**
         *  第二步: 属性赋值
         *  instanceA这类类里面有一个属性, InstanceB. 所以, 先拿到 instanceB, 然后在判断属性头上有没有Autowired注解.
         *  注意: 这里我们只是判断有没有Autowired注解. spring中还会判断有没有@Resource注解. @Resource注解还有两种方式, 一种是name, 一种是type
         */
        Field[] declaredFields = beanClass.getDeclaredFields();
        for (Field declaredField: declaredFields) {
            // 判断每一个属性是否有@Autowired注解
            Autowired annotation = declaredField.getAnnotation(Autowired.class);
            if (annotation != null) {
                // 设置这个属性是可访问的
                declaredField.setAccessible(true);
                // 那么这个时候还要构建这个属性的bean.
                /*
                 * 获取属性的名字
                 * 真实情况, spring这里会判断, 是根据名字, 还是类型, 还是构造函数来获取类.
                 * 我们这里模拟, 所以简单一些, 直接根据名字获取.
                 */
                String name = declaredField.getName();
                /**
                 * 这样, 在这里我们就拿到了 instanceB 的 bean
                 */
                Object fileObject = getBean(name);
                // 为属性设置类型
                declaredField.set(instanceBean, fileObject);
            }
        }
        /**
         * 第三步: 初始化
         * 初始化就是设置类的init-method.这个可以设置也可以不设置. 我们这里就不设置了
         */
        return instanceBean;
    }

我们看到如上代码.


第一步: 实例化: 使用反射的方式, 根据beanName查找构建一个实例bean.


第二步: 属性赋值: 判断属性中是否有@Autowired属性, 如果有这个属性, 那么需要构建bean. 我们发现在为InstanceA赋值的时候, 里面引用了InstanceB, 所以去创建InstanceB, 而创建InstanceB的时候, 发现里面又有InstanceA, 于是又去创建A. 然后以此类推,继续判断. 就形成了死循环. 无法走出这个环. 这就是循环依赖


第三步: 初始化: 调用init-method, 这个方法不是必须有, 所以,我们这里不模拟了

看看如下图所示

1187916-20201106053543069-1122277414.png


红色部分就形成了循环依赖.


4: 增加一级缓存, 解决循环依赖的问题.


我们知道上面进行了循环依赖了. 其实, 我们的目标很简单, 如果一个类创建过了, 那么就请不要在创建了.


所以, 我们增加一级缓存

// 一级缓存
    private static Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
   /**
     * 获取bean, 根据beanName获取
     */
    public static Object getBean(String beanName) throws Exception {
        // 增加一个出口. 判断实体类是否已经被加载过了
        Object singleton = getSingleton(beanName);
        if (singleton != null) {
            return singleton;
        }
        /**
         * 第一步: 实例化
         * 我们这里是模拟, 采用反射的方式进行实例化. 调用的也是最简单的无参构造函数
         */
        RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionMap.get(beanName);
        Class<?> beanClass = beanDefinition.getBeanClass();
        // 调用无参的构造函数进行实例化
        Object instanceBean = beanClass.newInstance();
        /**
         * 第二步: 放入到一级缓存
         */
        singletonObjects.put(beanName, instanceBean);
        /**
         *  第三步: 属性赋值
         *  instanceA这类类里面有一个属性, InstanceB. 所以, 先拿到 instanceB, 然后在判断属性头上有没有Autowired注解.
         *  注意: 这里我们只是判断有没有Autowired注解. spring中还会判断有没有@Resource注解. @Resource注解还有两种方式, 一种是name, 一种是type
          */
        Field[] declaredFields = beanClass.getDeclaredFields();
        for (Field declaredField: declaredFields) {
            // 判断每一个属性是否有@Autowired注解
            Autowired annotation = declaredField.getAnnotation(Autowired.class);
            if (annotation != null) {
                // 设置这个属性是可访问的
                declaredField.setAccessible(true);
                // 那么这个时候还要构建这个属性的bean.
                /*
                 * 获取属性的名字
                 * 真实情况, spring这里会判断, 是根据名字, 还是类型, 还是构造函数来获取类.
                 * 我们这里模拟, 所以简单一些, 直接根据名字获取.
                 */
                String name = declaredField.getName();
                /**
                 * 这样, 在这里我们就拿到了 instanceB 的 bean
                 */
                Object fileObject = getBean(name);
                // 为属性设置类型
                declaredField.set(instanceBean, fileObject);
            }
        }
        /**
         * 第四步: 初始化
         * 初始化就是设置类的init-method.这个可以设置也可以不设置. 我们这里就不设置了
         */
        return instanceBean;
    }

还是上面的获取bean的流程, 不一样的是, 这里增加了以及缓存. 当我们获取到bean实例以后, 将其放入到缓存中. 下次再需要创建之前, 先去缓存里判断,是否已经有了, 如果没有, 那么再创建.


这样就给创建bean增加了一个出口. 不会循环创建了.

 1187916-20201106055519319-196622798.png

如上图所示, 在@Autowired的时候, 增加了一个出口. 判断即将要创建的类是否已经存在, 如果存在了, 那么就直接返回, 不在创建


虽然使用了一级缓存解决了循环依赖的问题, 但要是在多线程下, 这个依赖可能就会出现问题.


比如: 有两个线程, 同时创建instanceA 和instanceB, instanceA和instanceB都引用了instanceC. 他们同步进行, 都去创建instanceC. 首先A去创建, A在实例化instanceC以后就将其放入到一级缓存了, 这时候, B去一级缓存里拿. 此时拿到的instanceC是不完整的. 后面的属性赋值, 初始化都还没有执行呢. 所以, 我们增加二级缓存来解决这个问题.

 

5. 增加二级缓存, 区分完整的bean和纯净的bean.

public class MainStart {
    private static Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
    // 一级缓存
    private static Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
    // 二级缓存
    private static Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
    /**
     * 读取bean定义, 当然在spring中肯定是根据配置 动态扫描注册的
     *
     * InstanceA和InstanceB都有注解@Component, 所以, 在spring扫描读取配置类的时候, 会把他们两个扫描到BeanDefinitionMap中.
     * 这里, 我们省略这一步, 直接将instanceA和instanceB放到BeanDefinitionMap中.
     */
    public static void loadBeanDefinitions(){
        RootBeanDefinition aBeanDefinition = new RootBeanDefinition(InstanceA.class);
        RootBeanDefinition bBeanDefinition = new RootBeanDefinition(InstanceB.class);
        beanDefinitionMap.put("instanceA", aBeanDefinition);
        beanDefinitionMap.put("instanceB", bBeanDefinition);
    }
    public static void main(String[] args) throws Exception {
        // 第一步: 扫描配置类, 读取bean定义
        loadBeanDefinitions();
        // 第二步: 循环创建bean
        for (String key: beanDefinitionMap.keySet()) {
            // 第一次: key是instanceA, 所以先创建A类
            getBean(key);
        }
        // 测试: 看是否能执行成功
        InstanceA instanceA = (InstanceA) getBean("instanceA");
        instanceA.say();
    }
    /**
     * 获取bean, 根据beanName获取
     */
    public static Object getBean(String beanName) throws Exception {
        // 增加一个出口. 判断实体类是否已经被加载过了
        Object singleton = getSingleton(beanName);
        if (singleton != null) {
            return singleton;
        }
        /**
         * 第一步: 实例化
         * 我们这里是模拟, 采用反射的方式进行实例化. 调用的也是最简单的无参构造函数
         */
        RootBeanDefinition beanDefinition = (RootBeanDefinition) beanDefinitionMap.get(beanName);
        Class<?> beanClass = beanDefinition.getBeanClass();
        // 调用无参的构造函数进行实例化
        Object instanceBean = beanClass.newInstance();
        /**
         * 第二步: 放入到二级缓存
         */
        earlySingletonObjects.put(beanName, instanceBean);
        /**
         *  第三步: 属性赋值
         *  instanceA这类类里面有一个属性, InstanceB. 所以, 先拿到 instanceB, 然后在判断属性头上有没有Autowired注解.
         *  注意: 这里我们只是判断有没有Autowired注解. spring中还会判断有没有@Resource注解. @Resource注解还有两种方式, 一种是name, 一种是type
          */
        Field[] declaredFields = beanClass.getDeclaredFields();
        for (Field declaredField: declaredFields) {
            // 判断每一个属性是否有@Autowired注解
            Autowired annotation = declaredField.getAnnotation(Autowired.class);
            if (annotation != null) {
                // 设置这个属性是可访问的
                declaredField.setAccessible(true);
                // 那么这个时候还要构建这个属性的bean.
                /*
                 * 获取属性的名字
                 * 真实情况, spring这里会判断, 是根据名字, 还是类型, 还是构造函数来获取类.
                 * 我们这里模拟, 所以简单一些, 直接根据名字获取.
                 */
                String name = declaredField.getName();
                /**
                 * 这样, 在这里我们就拿到了 instanceB 的 bean
                 */
                Object fileObject = getBean(name);
                // 为属性设置类型
                declaredField.set(instanceBean, fileObject);
            }
        }
        /**
         * 第四步: 初始化
         * 初始化就是设置类的init-method.这个可以设置也可以不设置. 我们这里就不设置了
         */
        /**
         * 第二步: 放入到一级缓存
         */
        singletonObjects.put(beanName, instanceBean);
        return instanceBean;
    }
    /**
     * 判断是否是循环引用的出口.
     * @param beanName
     * @return
     */
    private static Object getSingleton(String beanName) {
        // 先去一级缓存里拿,如果一级缓存没有拿到,去二级缓存里拿
        if (singletonObjects.containsKey(beanName)) {
            return singletonObjects.get(beanName);
        } else if (earlySingletonObjects.containsKey(beanName)){
            return earlySingletonObjects.get(beanName);
        } else {
            return null;
        }
    }
}

如上图所示,增加了一个二级缓存. 首先, 构建出instanceBean以后, 直接将其放入到二级缓存中. 这时只是一个纯净的bean, 里面还没有给属性赋值, 初始化. 在给属性赋值完成, 初始化完成以后, 在将其放入到一级缓存中.


我们判断缓存中是否有某个实例bean的时候, 先去一级缓存中判断是否有完整的bean, 如果没有, 就去二级缓存中判断有没有实例化过这个bean.

总结: 一级缓存和二级缓存的作用

 

一级缓存: 解决循环依赖的问题


二级缓存: 在创建实例bean和放入到一级缓存之间还有一段间隙. 如果在这之间从一级缓存拿实例, 肯定是返回null的. 为了避免这个问题, 增加了二级缓存.

 

我们都知道spring中有一级缓存, 二级缓存, 三级缓存. 一级缓存和二级缓存的作用我们知道了, 那么三级缓存有什么用呢?

相关文章
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
110 2
|
13天前
|
监控 JavaScript 数据可视化
建筑施工一体化信息管理平台源码,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
智慧工地云平台是专为建筑施工领域打造的一体化信息管理平台,利用大数据、云计算、物联网等技术,实现施工区域各系统数据汇总与可视化管理。平台涵盖人员、设备、物料、环境等关键因素的实时监控与数据分析,提供远程指挥、决策支持等功能,提升工作效率,促进产业信息化发展。系统由PC端、APP移动端及项目、监管、数据屏三大平台组成,支持微服务架构,采用Java、Spring Cloud、Vue等技术开发。
|
2月前
|
缓存 架构师 Java
图解 Spring 循环依赖,一文吃透!
Spring 循环依赖如何解决,是大厂面试高频,本文详细解析,建议收藏。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
图解 Spring 循环依赖,一文吃透!
|
1月前
|
存储 缓存 Java
Spring面试必问:手写Spring IoC 循环依赖底层源码剖析
在Spring框架中,IoC(Inversion of Control,控制反转)是一个核心概念,它允许容器管理对象的生命周期和依赖关系。然而,在实际应用中,我们可能会遇到对象间的循环依赖问题。本文将深入探讨Spring如何解决IoC中的循环依赖问题,并通过手写源码的方式,让你对其底层原理有一个全新的认识。
65 2
|
Java 关系型数据库 MySQL
06_spring_ 依赖注入| 学习笔记
快速学习 06_spring_ 依赖注入
115 0
|
3月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
295 2
|
5天前
|
XML JavaScript Java
SpringBoot集成Shiro权限+Jwt认证
本文主要描述如何快速基于SpringBoot 2.5.X版本集成Shiro+JWT框架,让大家快速实现无状态登陆和接口权限认证主体框架,具体业务细节未实现,大家按照实际项目补充。
38 11
|
7天前
|
缓存 安全 Java
Spring Boot 3 集成 Spring Security + JWT
本文详细介绍了如何使用Spring Boot 3和Spring Security集成JWT,实现前后端分离的安全认证概述了从入门到引入数据库,再到使用JWT的完整流程。列举了项目中用到的关键依赖,如MyBatis-Plus、Hutool等。简要提及了系统配置表、部门表、字典表等表结构。使用Hutool-jwt工具类进行JWT校验。配置忽略路径、禁用CSRF、添加JWT校验过滤器等。实现登录接口,返回token等信息。
134 12
|
27天前
|
Java 数据库连接 Maven
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
自动装配是现在面试中常考的一道面试题。本文基于最新的 SpringBoot 3.3.3 版本的源码来分析自动装配的原理,并在文未说明了SpringBoot2和SpringBoot3的自动装配源码中区别,以及面试回答的拿分核心话术。
最新版 | 深入剖析SpringBoot3源码——分析自动装配原理(面试常考)
|
13天前
|
Java 测试技术 应用服务中间件
Spring Boot 如何测试打包部署
本文介绍了 Spring Boot 项目的开发、调试、打包及投产上线的全流程。主要内容包括: 1. **单元测试**:通过添加 `spring-boot-starter-test` 包,使用 `@RunWith(SpringRunner.class)` 和 `@SpringBootTest` 注解进行测试类开发。 2. **集成测试**:支持热部署,通过添加 `spring-boot-devtools` 实现代码修改后自动重启。 3. **投产上线**:提供两种部署方案,一是打包成 jar 包直接运行,二是打包成 war 包部署到 Tomcat 服务器。
40 10