Spring源码学习:三级缓存的必要性

简介: Spring源码学习:三级缓存的必要性

目录

前言

工作中可能会碰到循坏依赖问题,所以了解其Spring设计原理,对于解决问题更加高效。在之前的文章中讲解了Spring的代码过程。这篇文章讲解Spring中三级缓存的必要性。

概述

Spring在属性注入时有两种方式,一种是实例化对象时同个构造器进行注入,这种情况如果产生循环依赖是没用办法解决的。另一种是实例化之后,来到属性填充流程,通过反射完成属性的注入,这种方式也会产生循环依赖问题,Spring中引用了三级缓存用于解决循环依赖。所以三级缓存只能解决特定场景下的循环依赖,一些场景是没办法解决的,如构造器注入、多例等。

正文

Spring的生命周期

在讲循环依赖之前,需要先了解一些Spring中Bean的生命周期,把其流程熟悉清楚才可以更好理解循环依赖场景。

以上是Spring IOC大概流程图,我们可以看到Bean的生命周期为实例化、属性注入、初始化、销毁等过程。

Spring中循环依赖场景

构造器循环依赖:Spring实例化过程中,会计算获取合适的构造函数,如果获取的构造器是一个有参的,此时的入参恰好也有该实例的场景,如实例化A时,构造器需要传入B,这时会先从三级缓存中获取;如果获取不到则会创建B,而创建B的过程中如果恰好使用的构造器是需要传入A的,这时尝试从缓存中获取,由于A还未被创建,所以此时获取不到,再去创建A就形成的闭环,陷入死循环了。如果创建B时,使用的是无参构造器,那么B是可以成功创建出来的,但是在B的属性填充环节又需要用到A,这也会产生循环依赖;构造器循环依赖时,无法解决;


原型模式(多例)循环依赖:Spring在每次从获取Bean时,如果是多例则不会尝试从缓存中获取,每次都会创建一个新的Bean对象进行返回,而在创建时其属性中也有引用Bean本身的情况,如A中有属性B,其两者为原型模式。这样在创建A过程时会尝试获取B,由于是多例的,获取B时又会先创建A,这样就形成闭环,产生循环依赖问题;原型模式(多例)循环依赖时,无法解决。


DependsOn循环依赖:如果使用了DependsOn,则Spring在创建当前Bean之前,会先创建所依赖的Bean。如果依赖的Bean也依赖其本身或依赖的Bean的子孙们也依赖其本身也会产生循环依赖问题,如A依赖B,B依赖A;A依赖B,B依赖C,C依赖A;使用三级缓存可以解决此循环依赖问题。


单例属性填充循环依赖:Spring在完成Bean的实例化之后,会调用populateBean方法,该方法中会对Bean属性进行注入,通过反射的方式使用set注入。这里并不是所有属性都会进行注入,只有进行配置的属性才会进行注入,如使用@Autowire会尝试从BeanFactory中进行获取注入。populateBean过程发生的问题跟上述发生的差不多,使用三级缓存可以解决此循环依赖问题。


单例代理对象循环依赖:当我们使用@Async注解时,会生成一个新的代理对象。此时如果被代理类中属性类也引用了被代理类,则会出行循环依赖问题;使用三级缓存可以解决此循环依赖问题。

@Service
public class TestService1 {
    @Autowired
    private TestService2 testService2;
    public void test1() {
    }
@Service
public class TestService2 {
    @Autowired
    private TestService1 testService1;
    public void test2() {
    }

Spirng中的三级缓存

  protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
      synchronized (this.singletonObjects) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
          ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
          if (singletonFactory != null) {
            singletonObject = singletonFactory.getObject();
            this.earlySingletonObjects.put(beanName, singletonObject);
            this.singletonFactories.remove(beanName);
          }
        }
      }
    }
    return singletonObject;
  }

该方法是Spring尝试从缓存中获取实例化后的对象过程,其中可以看到有三个缓存对象。


singletonObjects:一级缓存,存放成品对象,也就是完成了实例化、属性注入、初始化(如代理对象生成)等过程的完整对象;

earlySingletonObjects:二级缓存,存入实例化后还未进入属性注入,初始化方法调用的半成品对象;

singletonFactories:三级缓存,该缓存是存入ObjectFactory,并不是真正的bean对象,当通过beanName从三级缓存中获取了ObjectFactory的实现类之后,通过其getObject()方法获取真正的对象,返回的对象可能是代理后的对象,也可能是原生对象。

getSingleton方法首先会尝试从一级缓存中获取完整的对象,获取到则返回;如果获取不到则从二级缓存中获取,获取到则返回;如果二级缓存中也获取不到对象,则尝试从三级缓存中获取对象,此时获取到的对象是ObjectFactory对象,需要调用其getObject方法获取真正的对象,获取到之后存入二级缓存,移除三级缓存;下次获取可直接从二级缓存中获取。

Spring一级缓存解决循环依赖

当只有一级缓存时,发生了循环依赖。以A类有属性B,B类有属性A为例;


1.当实例化A之后,将A添加到一级缓存中,后进入属性注入流程;

2.在发现B需要被注入,此时会去一级缓存中查找B,查询不到则进行创建;

3.创建B之后,加入到一级缓存中。在进行属性注入时,发现需要A,此时会去一级缓存中查找A;

4.由于步骤1中一级缓存有A了,所以此时获取到A后进行属性注入,这时的B就是一个完整的对象。

A获取到新创建的B之后,进行注入,此时A也是一个完整的对象了。

通过以上步骤,我们可以得出对于单例模式下,一级缓存是可以解决循环依赖的;但是这种方式是将半成品跟成品对象放在一块了,假设我们对SpringIOC进行拓展,实现新的上下文时,如果存在多线程的情况,当获取到一个半成品,调用其方法时,就会报空指针异常了,所以只使用一级缓存是很不友好的,拓展性太差了。

Spring二级缓存解决循环依赖

由于一级缓存的缺点,半成品跟成品Bean都混在一块,导致拓展性差的问题。使用二级缓存就可以解决一级缓存中的问题。


1.一级缓存存放完整对象、二级缓存存放半成品对象

2.我们在属性注入时,尝试获取所需的Bean,先从一级缓存中查找,查询不到再从二级缓存中进行查询。

通过二级缓存的方式,拓展性有所提升。但是二级缓存也是有所缺陷。


1.A是一个需要被代理的对象,Spring IOC会先创建原始对象,在属性注入之后的初始化环节中才会对其进行代理。

2.创建普通的对象A之后,加入二级缓存中;属性注入时需要B,此时会创建B,B创建之后加入二级缓存。

3.B属性注入时,需要用到A,此时会去二级缓存中查找A,找到后进行注入,并加入到一级缓存中,移除二级缓存。

4.A获取到B之后,进行属性注入。注入完成后进入到初始化环节,调用BeanPostProcessor的postProcessAfterInitialization中生成代理对象EnhanceA;

大家是否发现B对象属性A是一个原生对象,并不是代理后的EnhanceA对象,所以这里就有问题了,二级缓存没办法解决代理的问题。

Spring三级缓存解决循环依赖

基于二级缓存的缺陷,这里Spring IOC引入了三级缓存。三级缓存存入的是一个FactoryObject,并不是真正的Bean对象。

public interface FactoryBean<T> {
  String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
  @Nullable
  T getObject() throws Exception;
  @Nullable
  Class<?> getObjectType();
  default boolean isSingleton() {
    return true;
  }
}


我们可以看到FactoryBean有getObject方法,在三级缓存中获取到FactoryBean类型对象后,通过调用其getObject方法返回真正的Bean对象

  1. 创建A之后,创建一个匿名内部类的FactoryBean存入三级缓存中。
      addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, 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;
  }

从匿名内部类的代码实现,首先从BeanFactory工厂中获取所有的BeanPostProcessor ,如果SmartInstantiationAwareBeanPostProcessor 类型的则调用其getEarlyBeanReference方法。而是否会被代理,就看SmartInstantiationAwareBeanPostProcessor 中是否有加载代理生成的工具类(在Spring IOC的准备工作中创建的)。

创建B之后,加入三级缓存。从一级缓存中查询,查不到从二级缓存中查询,查不到从三级缓存中查询。查询到之后,根据beanName名称获取到FactoryBean对象,并调用getObject方法,最终会得到真正的bean对象。该bean对象可能是代理对象也可能不是,要看具体的配置。从三级缓存中获取到对象之后,将该对象加入到二级缓存中,移除三级缓存,因为每次调用三级缓存可能开销是很大的。

3.B 获取到代理对象A之后进行属性注入,此时B就是一个完整的对象了,将二级缓存、三级缓存中的半成品移除,并将完整的对象加入到一级缓存中。


4、A获取到创建的B进行属性注入后,加入一级缓存中,移除二级、三级缓存。


通过以上步骤我们可以看到就能够解决二级缓存的缺陷了;可能有读者会有疑惑:

为什么不直接创建代理对象之后直接放入二级缓存呢?


如果直接放入二级缓存中也是没有问题的,但是需要考虑到代理的创建过程是及其耗时的,所以这里相当于采用懒加载的方法,当需要用到时再去进行创建,避免容器启动时,大量对象的创建,导致内存泄露等问题。


三级缓存中调用了代理对象,在初始化流程中还会进行代理吗?


在第一次创建代理之后会放入代理缓存中,当下次进行代理时,会尝试从缓存中获取,获取到了直接返回即可;

总结

循环依赖的解决方式有多种,Spring IOC是一个框架,所在在设计时会考虑拓展性,性能等各种问题,所以也是其应用广泛的原因之一吧。


目录
相关文章
|
6天前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
|
6天前
|
XML 缓存 Java
手写Spring源码(简化版)
Spring包下的类、手写@ComponentScan注解、@Component注解、@Autowired注解、@Scope注解、手写BeanDefinition、BeanNameAware、InitializingBean、BeanPostProcessor 、手写AnnotationConfigApplicationContext
手写Spring源码(简化版)
|
6天前
|
缓存 安全 Java
Spring框架中Bean是如何加载的?从底层源码入手,详细解读Bean的创建流程
从底层源码入手,通过代码示例,追踪AnnotationConfigApplicationContext加载配置类、启动Spring容器的整个流程,并对IOC、BeanDefinition、PostProcesser等相关概念进行解释
Spring框架中Bean是如何加载的?从底层源码入手,详细解读Bean的创建流程
|
21天前
|
小程序 前端开发 Java
SpringBoot+uniapp+uview打造H5+小程序+APP入门学习的聊天小项目
JavaDog Chat v1.0.0 是一款基于 SpringBoot、MybatisPlus 和 uniapp 的简易聊天软件,兼容 H5、小程序和 APP,提供丰富的注释和简洁代码,适合初学者。主要功能包括登录注册、消息发送、好友管理及群组交流。
45 0
SpringBoot+uniapp+uview打造H5+小程序+APP入门学习的聊天小项目
|
23天前
|
人工智能 前端开发 Java
【实操】Spring Cloud Alibaba AI,阿里AI这不得玩一下(含前后端源码)
本文介绍了如何使用 **Spring Cloud Alibaba AI** 构建基于 Spring Boot 和 uni-app 的聊天机器人应用。主要内容包括:Spring Cloud Alibaba AI 的概念与功能,使用前的准备工作(如 JDK 17+、Spring Boot 3.0+ 及通义 API-KEY),详细实操步骤(涵盖前后端开发工具、组件选择、功能分析及关键代码示例)。最终展示了如何成功实现具备基本聊天功能的 AI 应用,帮助读者快速搭建智能聊天系统并探索更多高级功能。
168 2
【实操】Spring Cloud Alibaba AI,阿里AI这不得玩一下(含前后端源码)
|
30天前
|
缓存 前端开发 JavaScript
前后端分离 SpringBoot+Vue商城买卖系统通杀版本。大家可以参考学习一下
这篇文章介绍了一个使用SpringBoot+Vue开发的前后端分离商城系统,包括技术架构、开发环境、实现的功能以及项目截图,并展示了普通用户和商家端的功能界面。
前后端分离 SpringBoot+Vue商城买卖系统通杀版本。大家可以参考学习一下
|
1月前
|
缓存 NoSQL Java
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
Spring Cache 是 Spring 提供的简易缓存方案,支持本地与 Redis 缓存。通过添加 `spring-boot-starter-data-redis` 和 `spring-boot-starter-cache` 依赖,并使用 `@EnableCaching` 开启缓存功能。JetCache 由阿里开源,功能更丰富,支持多级缓存和异步 API,通过引入 `jetcache-starter-redis` 依赖并配置 YAML 文件启用。Layering Cache 则提供分层缓存机制,需引入 `layering-cache-starter` 依赖并使用特定注解实现缓存逻辑。
243 1
SpringBoot的三种缓存技术(Spring Cache、Layering Cache 框架、Alibaba JetCache 框架)
|
2月前
|
Java 数据格式 微服务
2024最新首发,全网最全 Spring Boot 学习宝典(附思维导图)
📚 《滚雪球学Spring Boot》是由CSDN博主bug菌创作的全面Spring Boot教程。作者是全栈开发专家,在多个技术社区如CSDN、掘金、InfoQ、51CTO等担任博客专家,并拥有超过20万的全网粉丝。该教程分为入门篇和进阶篇,每篇包含详细的教学步骤,涵盖Spring Boot的基础和高级主题。
171 4
2024最新首发,全网最全 Spring Boot 学习宝典(附思维导图)
|
19天前
|
缓存 Java Spring
Spring缓存实践指南:从入门到精通的全方位攻略!
【8月更文挑战第31天】在现代Web应用开发中,性能优化至关重要。Spring框架提供的缓存机制可以帮助开发者轻松实现数据缓存,提升应用响应速度并减少服务器负载。通过简单的配置和注解,如`@Cacheable`、`@CachePut`和`@CacheEvict`,可以将缓存功能无缝集成到Spring应用中。例如,在配置文件中启用缓存支持并通过`@Cacheable`注解标记方法即可实现缓存。此外,合理设计缓存策略也很重要,需考虑数据变动频率及缓存大小等因素。总之,Spring缓存机制为提升应用性能提供了一种简便快捷的方式。
25 0
|
2月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的在线学习过程管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的在线学习过程管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的在线学习过程管理系统的详细设计和实现(源码+lw+部署文档+讲解等)