当前位置: 首页 > article >正文

Spring 框架基础知识

目录

Spring 框架中用到了哪些设计模式?

Spring 的循环依赖

Spring 循环依赖了解吗,怎么解决?

@Lazy 能解决循环依赖吗?

SpringBoot 允许循环依赖发生么?


Spring 框架中用到了哪些设计模式?

关于下面这些设计模式的详细介绍,可以看我写的 Spring 中的设计模式详解 这篇文章。

  • 工厂设计模式 : Spring 使用工厂模式通过 BeanFactoryApplicationContext 创建 bean 对象。
  • 代理设计模式 : Spring AOP 功能的实现。
  • 单例设计模式 : Spring 中的 Bean 默认都是单例的。
  • 模板方法模式 : Spring 中 jdbcTemplatehibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller
  • ……

Spring 的循环依赖

Spring 循环依赖了解吗,怎么解决?

循环依赖是指 Bean 对象循环引用,是两个或多个 Bean 之间相互持有对方的引用,例如 CircularDependencyA → CircularDependencyB → CircularDependencyA。

@Component
public class CircularDependencyA {
    @Autowired
    private CircularDependencyB circB;
}

@Component
public class CircularDependencyB {
    @Autowired
    private CircularDependencyA circA;
}

单个对象的自我依赖也会出现循环依赖,但这种概率极低,属于是代码编写错误。

@Component
public class CircularDependencyA {
    @Autowired
    private CircularDependencyA circA;
}

Spring 框架通过使用三级缓存来解决这个问题,确保即使在循环依赖的情况下也能正确创建 Bean。

Spring 中的三级缓存其实就是三个 Map,如下:

// 一级缓存
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

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

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

简单来说,Spring 的三级缓存包括:

  1. 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池,为“Spring 的单例属性”⽽⽣。一般情况我们获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
  2. 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中ObjectFactory产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()都是会产生新的代理对象的。
  3. 三级缓存(singletonFactories):存放ObjectFactoryObjectFactorygetObject()方法(最终调用的是getEarlyBeanReference()方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。

接下来说一下 Spring 创建 Bean 的流程:

  1. 先去 一级缓存 singletonObjects 中获取,存在就返回;
  2. 如果不存在或者对象正在创建中,于是去 二级缓存 earlySingletonObjects 中获取;
  3. 如果还没有获取到,就去 三级缓存 singletonFactories 中获取,通过执行 ObjectFacotrygetObject() 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。

在三级缓存中存储的是 ObjectFacoty

public interface ObjectFactory<T> {
    T getObject() throws BeansException;
}

Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 addSingletonFactory 方法,向三级缓存中添加一个 ObjectFactory 对象:

// AbstractAutowireCapableBeanFactory # doCreateBean #
public abstract class AbstractAutowireCapableBeanFactory ... {
	protected Object doCreateBean(...) {
        //...

        // 支撑循环依赖:将 ()->getEarlyBeanReference 作为一个 ObjectFactory 对象的 getObject() 方法加入到三级缓存中
		addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }
}

那么上边在说 Spring 创建 Bean 的流程时说了,如果一级缓存、二级缓存都取不到对象时,会去三级缓存中通过 ObjectFactory 的 getObject 方法获取对象。

class A {
    // 使用了 B
    private B b;
}
class B {
    // 使用了 A
    private A a;
}

以上面的循环依赖代码为例,整个解决循环依赖的流程如下:

  • 当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A;
  • 在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 一二级缓存 中肯定没有 A;
  • 那么此时就去三级缓存中调用 getObject() 方法去获取 A 的 前期暴露的对象 ,也就是调用上边加入的 getEarlyBeanReference() 方法,生成一个 A 的 前期暴露对象
  • 然后就将这个 ObjectFactory 从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,来支持循环依赖。

只用两级缓存够吗? 在没有 AOP 的情况下,确实可以只使用一级和三级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,二级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。

最后总结一下 Spring 如何解决三级缓存

在三级缓存这一块,主要记一下 Spring 是如何支持循环依赖的即可,也就是如果发生循环依赖的话,就去 三级缓存 singletonFactories 中拿到三级缓存中存储的 ObjectFactory 并调用它的 getObject() 方法来获取这个循环依赖对象的前期暴露对象(虽然还没初始化完成,但是可以拿到该对象在堆中的存储地址了),并且将这个前期暴露对象放到二级缓存中,这样在循环依赖时,就不会重复初始化了!

不过,这种机制也有一些缺点,比如增加了内存开销(需要维护三级缓存,也就是三个 Map),降低了性能(需要进行多次检查和转换)。并且,还有少部分情况是不支持循环依赖的,比如非单例的 bean 和@Async注解的 bean 无法支持循环依赖。

@Lazy 能解决循环依赖吗?

@Lazy 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。

Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。

配置文件配置全局懒加载:

#默认false
spring.main.lazy-initialization=true

编码的方式设置全局懒加载:

SpringApplication springApplication=new SpringApplication(Start.class);
springApplication.setLazyInitialization(false);
springApplication.run(args);

如非必要,尽量不要用全局懒加载。全局懒加载会让 Bean 第一次使用的时候加载会变慢,并且它会延迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。

如果一个 Bean 没有被标记为懒加载,那么它会在 Spring IoC 容器启动的过程中被创建和初始化。如果一个 Bean 被标记为懒加载,那么它不会在 Spring IoC 容器启动时立即实例化,而是在第一次被请求时才创建。这可以帮助减少应用启动时的初始化时间,也可以用来解决循环依赖问题。

循环依赖问题是如何通过@Lazy 解决的呢?这里举一个例子,比如说有两个 Bean,A 和 B,他们之间发生了循环依赖,那么 A 的构造器上添加 @Lazy 注解之后(延迟 Bean B 的实例化),加载的流程如下:

  • 首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性;
  • 由于在 A 上标注了 @Lazy 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性;
  • 之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。

从上面的加载流程可以看出: @Lazy 解决循环依赖的关键点在于代理对象的使用。

  • 没有 @Lazy 的情况下:在 Spring 容器初始化 A 时会立即尝试创建 B,而在创建 B 的过程中又会尝试创建 A,最终导致循环依赖(即无限递归,最终抛出异常)。
  • 使用 @Lazy 的情况下:Spring 不会立即创建 B,而是会注入一个 B 的代理对象。由于此时 B 仍未被真正初始化,A 的初始化可以顺利完成。等到 A 实例实际调用 B 的方法时,代理对象才会触发 B 的真正初始化。

@Lazy 能够在一定程度上打破循环依赖链,允许 Spring 容器顺利地完成 Bean 的创建和注入。但这并不是一个根本性的解决方案,尤其是在构造函数注入、复杂的多级依赖等场景中,@Lazy 无法有效地解决问题。因此,最佳实践仍然是尽量避免设计上的循环依赖。

SpringBoot 允许循环依赖发生么?

SpringBoot 2.6.x 以前是默认允许循环依赖的,也就是说你的代码出现了循环依赖问题,一般情况下也不会报错。SpringBoot 2.6.x 以后官方不再推荐编写存在循环依赖的代码,建议开发者自己写代码的时候去减少不必要的互相依赖。这其实也是我们最应该去做的,循环依赖本身就是一种设计缺陷,我们不应该过度依赖 Spring 而忽视了编码的规范和质量,说不定未来某个 SpringBoot 版本就彻底禁止循环依赖的代码了。

SpringBoot 2.6.x 以后,如果你不想重构循环依赖的代码的话,也可以采用下面这些方法:

  • 在全局配置文件中设置允许循环依赖存在:spring.main.allow-circular-references=true。最简单粗暴的方式,不太推荐。
  • 在导致循环依赖的 Bean 上添加 @Lazy 注解,这是一种比较推荐的方式。@Lazy 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。
  • ……


http://www.kler.cn/a/456450.html

相关文章:

  • 代码解析:安卓VHAL的AIDL参考实现
  • 【AI大模型】探索GPT模型的奥秘:引领自然语言处理的新纪元
  • 从 Elastic 迁移到 Easysearch 指引
  • 排序算法:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序
  • OpenAI 模型发展汇总
  • linux-软硬链接
  • 【设计模式学习笔记】1. 设计模式概述
  • 系统设计及解决方案
  • EndtoEnd Object Detection with Transformers
  • BOOST 库在缺陷检测领域的应用与发展前景
  • 1、redis的基础知识和类型
  • Docker部署neo4j
  • JDBC(Tomcat)
  • 深入探索哈夫曼编码与二叉树的遍历
  • 三、STM32MP257系列之定制Yocto Machine
  • 《PHP MySQL 插入数据》
  • Pytorch | 利用VA-I-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击
  • SD ComfyUI工作流 对人物图像进行抠图并替换背景
  • numpy的repeat和pytorch的repeat区别
  • CSS实现一个自定义的滚动条
  • 虚幻引擎反射机制
  • LabVIEW故障诊断中的无故障数据怎么办
  • C语言性能优化:从基础到高级的全面指南
  • python wxauto库实现微信自动化发送信息、回复、添加好友等
  • 五十一:HPACK如何减少HTTP头部的大小?
  • 条款20 当std::shared_ptr 可能悬空的时候使用std::weak_ptr