diff --git a/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md b/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md index b7f97be7..a71df39d 100644 --- a/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md +++ b/docs/system-design/framework/spring/spring-knowledge-and-questions-summary.md @@ -632,6 +632,131 @@ public class GlobalExceptionHandler { - **适配器模式** : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 - …… +## Spring 加载 Bean + +### 看过 SpringBoot 的启动流程吗? + +这个问了源码,看过就说看过,没看过就说没看过吧,不过可以了解一下整体的启动流程 + +SpringBoot 是基于 Spring 的,那么 **Spring** 源码中核心的方法就是 **refresh()** 方法,在这个 refresh() 方法中有 12 个方法,通过这 12 个方法完成整个项目中的 Bean 的加载 + +**先说一下 Spring 加载 Bean 的流程:** + +**Spring 整个 Bean 加载的原理:** 先根据路径去拿到路径下的所有 class 文件(字节码文件),再去根据注解或者 xml 文件将指定的 Bean 给注册到 Spring 中去,之后进行实例化,实例化之后需要对该 Bean 里的属性进行填充,那么这里填充属性值的时候就涉及到了 **循环依赖** 的问题,属性填充完毕之后,整个 Bean 就算初始化完成了 + +Spring 中还支持了许多扩展点,比如 **BeanPostProcessor、Aware、@PostConstruct、InitializingBean、DisposableBean** ,通过这些扩展点可以让开发者在 Bean 加载的过程中做一些额外的操作 + +那么整个加载 Bean 的流程主要就是去扫描字节码文件,再通过反射实例化,进行属性填充,完成 Bean 的创建,不过具体到细节流程还是很复杂的,因为还需要初始化很多其他的内容,比如 Bean 工厂、监听器、创建动态代理等等,整个 Bean 加载的流程如下: + +![Spring加载Bean的流程](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/image-20240401150843359.png) + +那么 SpringBoot 启动的时候也会加载 Bean,因此会调用到 Spring 的 **refresh()** 方法, **启动流程:** + +1、SpringBoot 项目启动就是从 **SpringApplication.run()** 方法开始的 + +2、根据应用类型加载对应的 Web 容器,SpringBoot 中 **内嵌了 Web 容器** 的初始化 + +3、加载一些初始化器,也就是加载一些自动配置类,也就是从 **META-INF/spring.factories** 配置文件中进行加载(在定义 starter 的时候,也是在这个文件中定义了自动配置类,这样 SpringBoot 项目启动的时候就会扫描 spring.factories,之后就可以拿到 starter 中定义的自动配置类,根据自动配置类去加载 stater 中对应的 Bean) + +4、之后调用 Spring 的 **refresh()** 方法来进行 Bean 的实例化、初始化流程 + +## Spring 的循环依赖 + +### Spring 的三级缓存介绍下,为啥需要他解决循环依赖? + +先说一下 Spring 中的三级缓存是什么,其实就是三个 Map,如下: + +```java +// 一级缓存:存储完整的 Bean 对象 +private final Map singletonObjects = new ConcurrentHashMap<>(256); +// 二级缓存:存储早期的 Bean 对象,因为有些 Bean 对象可能没有初始化完成,将未初始化完成的 Bean 对象先放在这里 +private final Map earlySingletonObjects = new HashMap<>(16); +// 三级缓存: +private final Map> singletonFactories = new HashMap<>(16); +``` + +Spring 中三级缓存的作用是 **解决循环依赖** + +先说一下 **循环依赖** 问题:比如现在有两个类 **A** 和 **B** ,那么如果 A 类中使用到了 B,B 中也使用到了 A,那么这种情况就是循环依赖: + +```java +class A { + // 使用了 B + private B b; +} +class B { + // 使用了 A + private A a; +} +``` + +循环依赖会出现问题的,当 Spring 去创建 A 的时候,发现 A 依赖了 B,于是去创建 B,发现 B 又依赖了 A,难道还要去创建 A 嘛? + +这显然不合理,因此 Spring 通过 **三级缓存来解决这个循环依赖** 的问题 + +接下来说一下 **Spring 创建 Bean** 的流程: + +1、先去 **一级缓存 singletonObjects** 中获取,存在就返回 + +2、如果不存在或者对象正在创建中,于是去 **二级缓存 earlySingletonObjects** 中获取 + +3、如果还没有获取到,就去 **三级缓存 singletonFactories** 中获取,通过执行 ObjectFacotry 的 getObject() 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中 + +在三级缓存中存储的是 **ObjectFacoty** ,定义为: + +```java +public interface ObjectFactory { + T getObject() throws BeansException; +} +``` + +Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 **addSingletonFactory** 方法,向三级缓存中添加一个 ObjectFactory 对象: + +```java +// AbstractAutowireCapableBeanFactory # doCreateBean # +public abstract class AbstractAutowireCapableBeanFactory ... { + protected Object doCreateBean(...) { + //... + + // 支撑循环依赖:将 ()->getEarlyBeanReference 作为一个 ObjectFactory 对象的 getObject() 方法加入到三级缓存中 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); + } +} +``` + +那么上边在说 Spring 创建 Bean 的流程时说了,如果一级缓存、二级缓存都取不到对象时,会去三级缓存中通过 ObjectFactory 的 getObject 方法获取对象 + +**整个解决循环依赖的流程如下:** + +- 当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A +- 在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 **一二级缓存** 中肯定没有 A +- 那么此时就去三级缓存中调用 getObject() 方法去获取 A 的 **前期暴露的对象** ,也就是调用上边加入的 **getEarlyBeanReference()** 方法,生成一个 A 的 **前期暴露对象** +- 然后就将这个 ObjectFactory 从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,**来支持循环依赖!** + +**最后总结一下 Spring 如何解决三级缓存** + +在三级缓存这一块,主要记一下 Spring 是如何支持循环依赖的即可,也就是如果发生循环依赖的话,就去 **三级缓存 singletonFactories** 中拿到三级缓存中存储的 ObjectFactory 并调用它的 getObject() 方法来获取这个循环依赖对象的前期暴露对象(虽然还没初始化完成,但是可以拿到该对象在堆中的存储地址了),并且将这个前期暴露对象放到二级缓存中,这样在循环依赖时,就不会重复初始化了! + +### @Lazy能解决循环依赖吗? + +@Lazy 注解可以配置在指定的 Bean 上,也可以配置在 SpringBootApplication 上表示全局配置 + +**@Lazy 注解的作用** 就是缩短 Spring IOC 容器的初始化时间,并且在发现循环依赖的时候,也可以通过 @Lazy 来解决 + +@Lazy 解决循环依赖就是靠 **代理** 来解决的,使用 @Lazy 标注的对象会被延迟加载 + +这里举一个例子,比如说有两个 Bean,A 和 B,他们之间发生了循环依赖,那么 A 的构造器上添加 @Lazy 注解之后,**加载的流程如下:** + +- 首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性 +- 由于在 A 上标注了 @Lazy 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性 +- 之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去 + +通过 **@Lazy** 就解决了循环依赖的注入, **关键点** 就在于对 A 中的属性 B 进行注入时,注入的是 B 的代理对象,因此不会循环依赖 + +之前说的发生循环依赖是因为在对 A 中的属性 B 进行注入时,注入的是 B 对象,此时又会去初始化 B 对象,发现 B 又依赖了 A,因此才导致的循环依赖 + +一般是不建议使用循环依赖的,但是如果项目比较复杂,可以使用 @Lazy 解决一部分循环依赖的问题 + ## Spring 事务 关于 Spring 事务的详细介绍,可以看我写的 [Spring 事务详解](https://javaguide.cn/system-design/framework/spring/spring-transaction.html) 这篇文章。