Spring 三级缓存解决循环依赖
架构图
Spring Bean的生命周期:实例化 -> 属性注入 -> (……) -> 初始化
三级缓存
在 Spring 框架中,解决循环依赖问题使用的是 三级缓存 机制。
这种机制主要存在于 DefaultSingletonBeanRegistry 类中,专门用于解决 Singleton(单例) 作用域下的循环依赖。
三级缓存的定义
这三级缓存其实是三个不同的 Map 结构:
- 一级缓存(singletonObjects):
- 存放的是完全初始化好的成品 Bean。
- 平时我们从容器中获取的 Bean 绝大部分都来自这里。
- 二级缓存(earlySingletonObjects):
- 存放的是半成品 Bean(尚未填充属性、未执行初始化方法的原始对象)。
- 主要用于解决循环依赖中的引用问题,确保同一个 Bean 在循环依赖中只被创建一次。
- 三级缓存(singletonFactories):
- 存放的是 ObjectFactory(工厂对象)。
- 它的作用是生成 Bean 的早期引用。如果该 Bean 需要被 AOP 代理,三级缓存会负责返回代理对象,而不是原始对象。
解决循环依赖的流程
假设有 A 和 B 互相依赖(A 依赖 B,B 依赖 A):
- 创建 A: 实例化 A,将 A 的
ObjectFactory放入三级缓存。 - 填充 A 的属性: 发现需要 B,去容器中找 B。
- 创建 B: 实例化 B,将 B 的
ObjectFactory放入三级缓存。 - 填充 B 的属性: 发现需要 A。
- B 去一级缓存找 A(没有)。
- B 去二级缓存找 A(没有)。
- B 去三级缓存找到了 A 的工厂,调用工厂获取 A 的引用(此时如果是 AOP,返回的就是代理后的 A)。
- 将 A 放入二级缓存,并从三级缓存中移除。
- 完成 B: B 拿到 A 的引用后,顺利完成初始化,进入一级缓存。
- 完成 A: A 拿到 B 的引用后,也顺利完成初始化,进入一级缓存。
核心知识点
- 为什么不能只用一级缓存?
- 一级缓存存放的是成品。如果不分级,容器可能会把还没初始化完的“半成品”暴露给用户,导致空指针异常或其他不可预知的错误。
- 为什么一定要有三级缓存(ObjectFactory)?二级不行吗?
- 核心原因是为了处理 AOP(面向切面编程)。
- 如果只有二级缓存,意味着所有 Bean 在实例化后必须立刻创建代理对象。但 Spring 的设计原则是尽可能在 Bean 初始化完成后再创建代理。
- 三级缓存相当于一个“延迟执行”,只有在出现循环依赖时,才通过三级缓存提前触发代理对象的创建。
注意: Spring 只能解决 A是 setter 注入 或 字段注入 的循环依赖。对于 构造器注入 的循环依赖,Spring 无法解决(因为在调用构造函数阶段,连对象还没实例化,根本没法放进缓存),此时会抛出 BeanCurrentlyInCreationException。
创建代理的时机
Spring 的设计逻辑是:在 Bean 生命周期的最后一步(初始化后)创建代理;但如果发生了循环依赖,就不得不“提前”创建代理。
1. 正常情况:初始化后创建代理
在没有循环依赖的情况下,Spring 遵循标准的生命周期:
- 实例化(CreateBeanInstance)
- 属性填充(PopulateBean)
- 初始化(InitializingBean / init-method)
- 初始化后(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 集合) 来做记录。
- 在三级缓存触发时: 如果创建了代理,会将原始 Bean 的 CacheKey 存入
earlyProxyReferences。 - 在初始化后的正常 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")。
- 特点: 适合集成第三方库,因为你不需要修改源码去加注解或实现接口。
具体会做哪些事?(典型场景)
-
参数校验:
检查必要的属性是否已经注入。例如,一个发送邮件的 Service,在初始化时检查 SMTP_SERVER 变量是否为空,如果为空直接报错,防止程序带病运行。
-
资源准备:
- 建立数据库连接。
- 打开文件 IO 流。
- 启动定时任务(Quartz 或 Spring Task)。
-
预热缓存:
从数据库加载一些热点数据到本地内存 Map 或 Redis 中,确保系统上线后能立即响应。
-
与外部系统握手:
比如向注册中心(Nacos/Eureka)注册当前服务,或者建立长连接。
初始化与构造器的区别
这是一个常见的误区:为什么不直接在构造器(Constructor)里写这些逻辑?
| 维度 | 构造器 (Constructor) | 初始化方法 (init-method) |
|---|---|---|
| 属性状态 | 属性尚未注入(都是 null) |
属性已全部注入完成 |
| 代理对象 | 此时拿不到代理对象 | 可能已经拿到了(如果是循环依赖) |
| 目的 | 创建对象实例 | 准备业务环境 |
举个例子:
如果你在构造器里调用 this.dao.save(),会报 NullPointerException,因为此时 dao 还没被 Spring 注入进来;但如果你在 afterPropertiesSet() 里调用,就是安全的。
总结
初始化阶段是 Bean 从“一个普通的 Java 对象”转变为“一个具备业务能力的 Spring 组件”的最后一道工序。
