架构图

架构图

Spring Bean的生命周期:实例化 -> 属性注入 -> (……) -> 初始化

三级缓存

在 Spring 框架中,解决循环依赖问题使用的是 三级缓存 机制。

这种机制主要存在于 DefaultSingletonBeanRegistry 类中,专门用于解决 Singleton(单例) 作用域下的循环依赖。


三级缓存的定义

这三级缓存其实是三个不同的 Map 结构:

  1. 一级缓存(singletonObjects):
    • 存放的是完全初始化好的成品 Bean。
    • 平时我们从容器中获取的 Bean 绝大部分都来自这里。
  2. 二级缓存(earlySingletonObjects):
    • 存放的是半成品 Bean(尚未填充属性、未执行初始化方法的原始对象)。
    • 主要用于解决循环依赖中的引用问题,确保同一个 Bean 在循环依赖中只被创建一次。
  3. 三级缓存(singletonFactories):
    • 存放的是 ObjectFactory(工厂对象)
    • 它的作用是生成 Bean 的早期引用。如果该 Bean 需要被 AOP 代理,三级缓存会负责返回代理对象,而不是原始对象。

解决循环依赖的流程

假设有 A 和 B 互相依赖(A 依赖 B,B 依赖 A):

  1. 创建 A: 实例化 A,将 A 的 ObjectFactory 放入三级缓存
  2. 填充 A 的属性: 发现需要 B,去容器中找 B。
  3. 创建 B: 实例化 B,将 B 的 ObjectFactory 放入三级缓存
  4. 填充 B 的属性: 发现需要 A。
    • B 去一级缓存找 A(没有)。
    • B 去二级缓存找 A(没有)。
    • B 去三级缓存找到了 A 的工厂,调用工厂获取 A 的引用(此时如果是 AOP,返回的就是代理后的 A)。
    • 将 A 放入二级缓存,并从三级缓存中移除。
  5. 完成 B: B 拿到 A 的引用后,顺利完成初始化,进入一级缓存
  6. 完成 A: A 拿到 B 的引用后,也顺利完成初始化,进入一级缓存

核心知识点

  • 为什么不能只用一级缓存?
    • 一级缓存存放的是成品。如果不分级,容器可能会把还没初始化完的“半成品”暴露给用户,导致空指针异常或其他不可预知的错误。
  • 为什么一定要有三级缓存(ObjectFactory)?二级不行吗?
    • 核心原因是为了处理 AOP(面向切面编程)
    • 如果只有二级缓存,意味着所有 Bean 在实例化后必须立刻创建代理对象。但 Spring 的设计原则是尽可能在 Bean 初始化完成后再创建代理。
    • 三级缓存相当于一个“延迟执行”,只有在出现循环依赖时,才通过三级缓存提前触发代理对象的创建。

注意: Spring 只能解决 A是 setter 注入字段注入 的循环依赖。对于 构造器注入 的循环依赖,Spring 无法解决(因为在调用构造函数阶段,连对象还没实例化,根本没法放进缓存),此时会抛出 BeanCurrentlyInCreationException

创建代理的时机

Spring 的设计逻辑是:在 Bean 生命周期的最后一步(初始化后)创建代理;但如果发生了循环依赖,就不得不“提前”创建代理。


1. 正常情况:初始化后创建代理

在没有循环依赖的情况下,Spring 遵循标准的生命周期:

  1. 实例化(CreateBeanInstance)
  2. 属性填充(PopulateBean)
  3. 初始化(InitializingBean / init-method)
  4. 初始化后(PostProcessAfterInitialization):在这个阶段,AOP 开始介入,将原始对象包装成代理对象并返回。

2. 循环依赖情况:提前创建代理

当 A 依赖 B,B 又依赖 A 时,A 还在属性填充阶段(第 2 步),B 就已经问 A 要引用了。这时候 A 必须提前把自己的代理对象交出去。

这就是 三级缓存(ObjectFactory) 发挥作用的地方:

  • 三级缓存存的是什么? 它存的是一个 Lambda 表达式(工厂),这个工厂里封装了 getEarlyBeanReference 方法。
  • 触发时机: 当 B 向三级缓存请求 A 时,这个工厂方法会被调用。
  • 执行逻辑: 它会检查 A 是否需要被代理。如果需要,它会提前执行 AOP 逻辑,创建一个代理对象 A。
  • 结果: B 拿到的是 A 的代理对象,而 A 的代理对象内部持有了 A 的原始对象。

3. 既然提前创建了,后面还会再创一次吗?

这是一个关键细节。为了防止代理对象被重复创建,Spring 使用了一个 earlyProxyReferences(Map 集合) 来做记录。

  1. 在三级缓存触发时: 如果创建了代理,会将原始 Bean 的 CacheKey 存入 earlyProxyReferences
  2. 在初始化后的正常 AOP 阶段: AOP 后置处理器会检查:“这个 Bean 是不是已经在 earlyProxyReferences 里了?”
    • 如果是,说明已经提前创建过了,直接返回原始对象,不再重复创建代理。
    • 如果不是,说明没有循环依赖,按照正常流程在此处创建代理。

总结

Spring 并没有改变“代理包装原始对象”的本质,它只是通过 三级缓存 灵活地调整了创建代理的 时机

  • 无冲突: 初始化后再代理(最理想,符合设计原则)。
  • 有冲突: 实例化后、填充前提前代理(为了解决循环依赖的妥协)。

初始化阶段会做些什么

在 Spring Bean 的生命周期中,“初始化”是一个关键阶段。它的核心作用是:在 Bean 的属性注入完成后,给开发者一个机会去执行一些自定义的逻辑。

比如:检查配置是否合法、开启资源(数据库连接池、线程池)、预加载缓存等。


初始化阶段的执行顺序

Spring 提供了多种方式来实现初始化逻辑,它们的执行顺序如下:

1. @PostConstruct 注解(常用)

这是 JSR-250 规范定义的注解。只要在方法上加上它,Spring 容器在填充完属性后就会调用该方法。

  • 特点: 耦合度低,最推荐使用。

2. InitializingBean 接口

如果你的类实现了这个接口,就必须重写 afterPropertiesSet() 方法。

  • 逻辑: 顾名思义,就是在属性设置完成后(After Properties Set)执行。
  • 缺点: 你的业务代码会和 Spring 的接口耦合。

3. 自定义的 init-method

在 XML 配置中指定 init-method="xxx",或者在 Java 配置中使用 @Bean(initMethod = "xxx")

  • 特点: 适合集成第三方库,因为你不需要修改源码去加注解或实现接口。

具体会做哪些事?(典型场景)

  1. 参数校验:

    检查必要的属性是否已经注入。例如,一个发送邮件的 Service,在初始化时检查 SMTP_SERVER 变量是否为空,如果为空直接报错,防止程序带病运行。

  2. 资源准备:

    • 建立数据库连接。
    • 打开文件 IO 流。
    • 启动定时任务(Quartz 或 Spring Task)。
  3. 预热缓存:

    从数据库加载一些热点数据到本地内存 Map 或 Redis 中,确保系统上线后能立即响应。

  4. 与外部系统握手:

    比如向注册中心(Nacos/Eureka)注册当前服务,或者建立长连接。


初始化与构造器的区别

这是一个常见的误区:为什么不直接在构造器(Constructor)里写这些逻辑?

维度 构造器 (Constructor) 初始化方法 (init-method)
属性状态 属性尚未注入(都是 null 属性已全部注入完成
代理对象 此时拿不到代理对象 可能已经拿到了(如果是循环依赖)
目的 创建对象实例 准备业务环境

举个例子:

如果你在构造器里调用 this.dao.save(),会报 NullPointerException,因为此时 dao 还没被 Spring 注入进来;但如果你在 afterPropertiesSet() 里调用,就是安全的。


总结

初始化阶段是 Bean 从“一个普通的 Java 对象”转变为“一个具备业务能力的 Spring 组件”的最后一道工序。