大家好!我是码农界的段子手,今天我们要聊一个让无数Spring开发者又爱又恨的话题——循环依赖。这就像你暗恋的人刚好也暗恋你,但你们都不敢表白,结果陷入了无限循环的尴尬...
一、面试现场:当面试官突然邪魅一笑
面试官(推了推并不存在的眼镜):"小伙子,知道Spring是怎么解决循环依赖的吗?"
我(表面镇定,内心慌得一批):"这个...大概就是...先给个半成品?"
面试官(露出姨母笑):"哦?展开说说?"
二、循环依赖的"死亡华尔兹"
先看个典型例子:
@Service
public class Boy {
@Autowired
private Girl girl;
public void sayLove() {
System.out.println("女孩,你的代码真美!");
}
}
@Service
public class Girl {
@Autowired
private Boy boy;
public void response() {
System.out.println("男孩,你的Bug真多!");
}
}
这就像两个傲娇的程序员互相等待对方先提交代码,结果项目永远无法启动
三、Spring的三级缓存:爱情调解员
Spring解决这个问题的方案堪称"情感专家",它建立了三级缓存:
- 一级缓存(singletonObjects):存放完全成熟的Bean
- 二级缓存(earlySingletonObjects):存放提前曝光的半成品Bean
- 三级缓存(singletonFactories):存放Bean工厂
// 伪代码展示Spring的解决思路
public class DefaultSingletonBeanRegistry {
// 一级缓存:成品区
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();
// 二级缓存:半成品展示区
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>();
// 三级缓存:对象工厂区
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>();
}
四、Spring的"恋爱攻略"
-
Boy实例化:先把Boy对象new出来(但还没设属性)
-
提前曝光:把Boy对象工厂放入三级缓存
-
注入Girl:发现需要Girl,去容器找
-
Girl实例化:同样把Girl new出来
-
Girl需要Boy:从三级缓存拿到Boy的早期引用
-
Girl完成:Girl进入一级缓存
-
Boy完成注入:Boy拿到完整的Girl引用
-
Boy完成:Boy也进入一级缓存
这个过程就像:
- Boy先表白:"我喜欢你"(三级缓存)
- Girl回应:"我也喜欢你,但你要先证明自己"(属性注入)
- Boy努力变优秀(完成初始化)
- 最后幸福地在一起(一级缓存)
五、灵魂拷问:为什么不能只用二级缓存?
面试官(突然发难):"既然三级缓存这么麻烦,为什么不用二级缓存搞定?"
我(灵光一闪):"因为AOP!代理对象需要在后期生成,三级缓存通过ObjectFactory实现了延迟处理。"
举个:
@Component
public class A {
@Autowired
private B b;
@Async // 这里会生成代理对象
public void method() {}
}
如果不使用三级缓存,提前暴露的可能是不带AOP功能的原始对象,就像你网恋奔现发现对方没用美颜滤镜...悲剧啊!
5.2、硬核实验:手动实现二级缓存方案
让我们写个简化版的Spring容器验证一下:
public class MyMiniSpring {
// 一级缓存:成品
private Map<String, Object> singletonObjects = new HashMap<>();
// 二级缓存:半成品
private Map<String, Object> earlySingletonObjects = new HashMap<>();
public <T> T getBean(Class<T> clazz) throws Exception {
String beanName = clazz.getSimpleName().toLowerCase();
// 1. 先查一级缓存
if (singletonObjects.containsKey(beanName)) {
return (T) singletonObjects.get(beanName);
}
// 2. 查二级缓存
if (earlySingletonObjects.containsKey(beanName)) {
return (T) earlySingletonObjects.get(beanName);
}
// 3. 创建Bean实例
Object bean = clazz.getDeclaredConstructor().newInstance();
earlySingletonObjects.put(beanName, bean); // 半成品放入二级缓存
// 4. 属性填充(模拟依赖注入)
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
Object dependency = getBean(field.getType());
field.setAccessible(true);
field.set(bean, dependency);
}
}
// 5. 完成初始化
singletonObjects.put(beanName, bean);
earlySingletonObjects.remove(beanName);
return (T) bean;
}
}
测试我们的Boy-Girl案例:
public class Test {
public static void main(String[] args) throws Exception {
MyMiniSpring context = new MyMiniSpring();
Boy boy = context.getBean(Boy.class); // 成功!
Girl girl = context.getBean(Girl.class);
boy.sayLove(); // 输出:女孩,你的代码真美!
girl.response();// 输出:男孩,你的Bug真多!
}
}
惊不惊喜?意不意外?二级缓存确实能解决普通循环依赖!
5.3、但是(永远有个但是)...
当我们的Bean需要AOP代理时,问题就来了:
@Service
public class Boy {
@Autowired
private Girl girl;
@Async // 需要生成代理
public void sayLove() {
System.out.println("女孩,你的代码真美!");
}
}
在二级缓存方案中:
- 原始Boy对象被放入二级缓存
- Girl通过二级缓存拿到原始Boy引用
- 最后Boy被AOP包装成代理对象
- 结果:Girl持有的是原始Boy,而其他Bean拿到的是代理Boy → 精神分裂了!
5.4、三级缓存的魔法:ObjectFactory
Spring的三级缓存精妙之处在于:
// Spring真实源码片段
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory); // 关键在这里!
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
当需要提前暴露引用时:
- 二级缓存方案:直接暴露原始对象
- 三级缓存方案:暴露一个能生成最终对象的工厂
就像:
- 二级缓存:给你我的素颜照(风险:可能被P图)
- 三级缓存:给你我的美颜相机APP(保证最终看到的都是处理后的效果)
5.5、终极对决:二级缓存 vs 三级缓存
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 二级缓存 | 实现简单 | 无法正确处理AOP代理 | 无AOP的简单项目 |
| 三级缓存 | 支持代理对象的延迟生成 | 实现复杂 | 需要AOP的企业级应用 |
5.6、Spring官方怎么说?
在Spring源码的
DefaultSingletonBeanRegistry类中有段注释:
/**
* 三级缓存分工:
* 1. singletonObjects:存储完全初始化的bean
* 2. earlySingletonObjects:存储原始bean(尚未填充属性)
* 3. singletonFactories:存储生成bean的工厂(可以处理代理)
*
* 这样设计主要是为了解决循环引用同时支持AOP代理。
*/
5.7、彩蛋:如果坚持用二级缓存?
也不是不行,但需要:
- 在实例化后立即生成代理(违背Spring的设计原则,期望是在初始化后生成代理)
- 或者放弃某些AOP功能
就像你可以坚持西红柿炒蛋放糖,但可能会被北方同事追着打...
5.8、总结
- 普通循环依赖:二级缓存确实够用(但Spring选择了统一处理方案)
- 需要AOP的场景:必须三级缓存才能完美解决
- 设计哲学:Spring选择用更复杂的三级缓存,是为了保证功能扩展性
最后送大家一张图理解三级缓存:
[三级缓存工作流程图]
1. 创建Bean → 放入三级缓存(工厂)
2. 依赖注入时 → 通过工厂获取早期引用
3. 若需要代理 → 工厂负责生成代理对象
4. 最终成品 → 存入一级缓存
六、循环依赖的"禁忌之恋"
不是所有循环依赖Spring都能解决:
❌ 构造器注入的循环依赖:
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; }
}
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; }
}
这种就像两个人都说"你先表白我再表白",结果永远没结果,Spring直接抛出
BeanCurrentlyInCreationException表示:"你们还是做朋友吧"。
七、不需要三级缓存的5种典型场景
7.1. 普通Bean没有AOP需求时
@Service
public class NormalService {
// 没有@Async/@Transactional等AOP注解
public void normalMethod() {}
}
就像素颜出门不用带化妆品,Spring直接使用二级缓存就够了
7.2. 使用构造器注入时
@Service
public class ConstructorService {
private final DependencyService dependency;
// 构造器注入直接报错,缓存都没机会用
public ConstructorService(DependencyService dependency) {
this.dependency = dependency;
}
}
这就像两个人互相等着对方先表白,结果谁都开不了口
7.3. 原型(prototype)作用域的Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
public class PrototypeService {
@Autowired
private OtherService otherService;
}
Spring:"每次都是新的对象,我缓存它干啥?"
7.4. 显式关闭循环依赖支持
// 在Spring Boot配置中
spring.main.allow-circular-references=false
相当于在说:"禁止套娃!"
7.5. 使用@Lazy延迟加载
@Service
public class LazyService {
@Lazy
@Autowired
private AnotherService anotherService;
}
就像拖延症患者:"明天再说吧",结果永远不需要解决
八、二级缓存的真实作用:Spring的"临时停车场"
二级缓存(earlySingletonObjects)本质上是个临时过渡存储:
// Spring源码中的二级缓存定义
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
它的核心职责:
- 防止重复创建半成品Bean:确保同一Bean在循环依赖中只被创建一次
- 性能优化:避免频繁执行ObjectFactory的getObject()方法
- 过渡存储:在三级缓存到一级缓存之间的中转站
工作流程示例:
- Bean A开始创建 → 放入三级缓存
- Bean B依赖A → 通过三级缓存工厂创建代理对象
- 代理对象放入二级缓存
- 后续依赖直接使用二级缓存中的对象
- 初始化完成后移入一级缓存
九、三级缓存 vs 二级缓存 工作流程图解
graph TD
A[开始创建Bean X] --> B{是否需要提前暴露?}
B -->|是| C[放入三级缓存: singletonFactories]
B -->|否| D[正常初始化]
C --> E[依赖注入时需要X]
E --> F[从三级缓存获取ObjectFactory]
F --> G[执行getObject()]
G --> H{是否需要AOP?}
H -->|是| I[创建代理对象]
H -->|否| J[返回原始对象]
I --> K[放入二级缓存: earlySingletonObjects]
J --> K
K --> L[返回对象引用]
L --> M[继续属性填充]
M --> N[初始化完成]
N --> O[移入一级缓存: singletonObjects]
O --> P[清理二三级缓存]
十、性能视角:为什么需要二级缓存?
看个性能对比实验:
// 模拟没有二级缓存的情况
public Object getEarlyBeanReference(String beanName) {
// 每次都要执行工厂方法
ObjectFactory<?> factory = this.singletonFactories.get(beanName);
return factory.getObject(); // 可能涉及复杂的代理创建逻辑
}
// 有二级缓存时
public Object getEarlyBeanReference(String beanName) {
Object object = this.earlySingletonObjects.get(beanName);
if (object == null) {
// 只第一次需要执行工厂
object = getEarlyBeanReference(beanName);
this.earlySingletonObjects.put(beanName, object);
}
return object;
}
性能影响:
- 没有二级缓存:每次依赖注入都要执行工厂方法(可能很耗时)
- 有二级缓存:只有第一次需要执行工厂,后续直接取缓存
十一、实战中的特殊案例
案例1:AOP切面自身的循环依赖
@Aspect
@Component
public class MyAspect {
@Autowired
private MyService service; // 切面依赖Service
@Around("execution(* com..*(..))")
public Object around(ProceedingJoinPoint pjp) {
// 切面逻辑
}
}
@Service
public class MyService {
@Autowired
private MyAspect aspect; // Service又依赖切面
}
这种情况下,三级缓存是唯一的救命稻草!
案例2:@Configuration类中的循环
@Configuration
public class MyConfig {
@Bean
public A a() { return new A(b()); }
@Bean
public B b() { return new B(a()); }
}
Spring特别处理配置类,实际上会使用CGLIB代理,这时三级缓存又派上用场
十二、从设计模式角度看缓存设计
三级缓存机制本质上是多种设计模式的混合体:
- 工厂模式:ObjectFactory的生产能力
- 代理模式:处理AOP代理
- 外观模式:AbstractBeanFactory统一暴露getBean接口
- 备忘录模式:缓存保存对象的不同状态
十三、灵魂拷问:能完全取消二级缓存吗?
理论上可以,但会有这些问题:
- 性能下降:重复创建代理对象
- 一致性风险:可能产生多个不同的代理实例
- 内存泄漏:工厂对象可能被长期持有
Spring的选择就像我们写代码时的trade-off:用空间换时间和稳定性
十四、总结:缓存机制的智慧
- 三级缓存是完整解决方案:应对所有场景(包括AOP)
- 二级缓存是性能优化:减少工厂方法调用,保证对象一致性
- 简单场景可能用不到三级缓存:但没有AOP就像没有调料的泡面—能吃饱但不够香
最后送大家一张缓存使用决策图:
是否需要处理循环依赖?
├─ 否 → 直接使用一级缓存
└─ 是 → 是否需要AOP代理?
├─ 否 → 二级缓存足够
十五、实战中的"爱情三十六计"
场景:订单服务依赖库存服务,库存服务又需要订单服务检查历史记录
@Service
public class OrderService {
@Autowired
private InventoryService inventoryService;
public void createOrder() {
// 检查库存
inventoryService.checkStock();
// 创建订单逻辑...
}
}
@Service
public class InventoryService {
@Autowired
private OrderService orderService;
public void checkStock() {
// 检查历史订单记录
orderService.getOrderHistory();
// 库存检查逻辑...
}
}
解决方案:
- 重构设计(最佳)
- 使用@Lazy延迟加载
@Service
public class OrderService {
@Lazy
@Autowired
private InventoryService inventoryService;
// ...
}
}
3. 使用setter注入代替字段注入
十六、思考题:JVM的类加载会不会循环依赖?
类加载的"父委托机制"其实也有类似的循环依赖问题。比如:
- 子加载器委托父加载器加载类A
- 类A又需要加载类B
- 类B又委托子加载器加载
JVM的解决方案是:打破委托,让子加载器自己尝试加载。这不就是Spring的"提前暴露"思路吗?看来优秀的设计总是相似的!
十七、总结:爱情与代码的哲学
- Spring解决循环依赖的核心思想:提前暴露 + 延迟注入
- 三级缓存各司其职,像极了恋爱中的不同阶段
- 不是所有循环依赖都能解决,构造器注入就是"注孤生"
- 设计时应尽量避免循环依赖,就像避免办公室恋情(虽然刺激但容易翻车)
最后送大家一句程序员情话:
while(life.hasLove()) {
myHeart.beatFor(you);
}
// 我的代码世界,因为有你才不会StackOverflow
怎么样?是不是对循环依赖有了全新的认识?下次面试官再问这个问题,你就可以邪魅一笑:"这要从Spring的恋爱观说起..."