【Spring面试暴击】循环依赖

77 阅读11分钟

大家好!我是码农界的段子手,今天我们要聊一个让无数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解决这个问题的方案堪称"情感专家",它建立了三级缓存:

  1. 一级缓存(singletonObjects):存放完全成熟的Bean
  2. 二级缓存(earlySingletonObjects):存放提前曝光的半成品Bean
  3. 三级缓存(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的"恋爱攻略"

  1. Boy实例化:先把Boy对象new出来(但还没设属性)

  2. 提前曝光:把Boy对象工厂放入三级缓存

  3. 注入Girl:发现需要Girl,去容器找

  4. Girl实例化:同样把Girl new出来

  5. Girl需要Boy:从三级缓存拿到Boy的早期引用

  6. Girl完成:Girl进入一级缓存

  7. Boy完成注入:Boy拿到完整的Girl引用

  8. 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("女孩,你的代码真美!");
    }
}

在二级缓存方案中:

  1. 原始Boy对象被放入二级缓存
  2. Girl通过二级缓存拿到原始Boy引用
  3. 最后Boy被AOP包装成代理对象
  4. 结果: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、彩蛋:如果坚持用二级缓存?

也不是不行,但需要:

  1. 在实例化后立即生成代理(违背Spring的设计原则,期望是在初始化后生成代理)
  2. 或者放弃某些AOP功能

就像你可以坚持西红柿炒蛋放糖,但可能会被北方同事追着打...

5.8、总结

  1. 普通循环依赖:二级缓存确实够用(但Spring选择了统一处理方案)
  2. 需要AOP的场景:必须三级缓存才能完美解决
  3. 设计哲学: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/@TransactionalAOP注解
    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);

它的核心职责:

  1. 防止重复创建半成品Bean:确保同一Bean在循环依赖中只被创建一次
  2. 性能优化:避免频繁执行ObjectFactory的getObject()方法
  3. 过渡存储:在三级缓存到一级缓存之间的中转站

工作流程示例:

  1. Bean A开始创建 → 放入三级缓存
  2. Bean B依赖A → 通过三级缓存工厂创建代理对象
  3. 代理对象放入二级缓存
  4. 后续依赖直接使用二级缓存中的对象
  5. 初始化完成后移入一级缓存

九、三级缓存 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代理,这时三级缓存又派上用场

十二、从设计模式角度看缓存设计

三级缓存机制本质上是多种设计模式的混合体:

  1. 工厂模式:ObjectFactory的生产能力
  2. 代理模式:处理AOP代理
  3. 外观模式:AbstractBeanFactory统一暴露getBean接口
  4. 备忘录模式:缓存保存对象的不同状态

十三、灵魂拷问:能完全取消二级缓存吗?

理论上可以,但会有这些问题:

  1. 性能下降:重复创建代理对象
  2. 一致性风险:可能产生多个不同的代理实例
  3. 内存泄漏:工厂对象可能被长期持有

Spring的选择就像我们写代码时的trade-off:用空间换时间和稳定性

十四、总结:缓存机制的智慧

  1. 三级缓存是完整解决方案:应对所有场景(包括AOP)
  2. 二级缓存是性能优化:减少工厂方法调用,保证对象一致性
  3. 简单场景可能用不到三级缓存:但没有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();
        // 库存检查逻辑...
    }
}

解决方案

  1. 重构设计(最佳)
  2. 使用@Lazy延迟加载
@Service
public class OrderService {
    @Lazy
    @Autowired
    private InventoryService inventoryService;
    // ...
}
}

3. 使用setter注入代替字段注入

十六、思考题:JVM的类加载会不会循环依赖?

类加载的"父委托机制"其实也有类似的循环依赖问题。比如:

  • 子加载器委托父加载器加载类A
  • 类A又需要加载类B
  • 类B又委托子加载器加载

JVM的解决方案是:打破委托,让子加载器自己尝试加载。这不就是Spring的"提前暴露"思路吗?看来优秀的设计总是相似的!

十七、总结:爱情与代码的哲学

  1. Spring解决循环依赖的核心思想:提前暴露 + 延迟注入
  2. 三级缓存各司其职,像极了恋爱中的不同阶段
  3. 不是所有循环依赖都能解决,构造器注入就是"注孤生"
  4. 设计时应尽量避免循环依赖,就像避免办公室恋情(虽然刺激但容易翻车)

最后送大家一句程序员情话:

while(life.hasLove()) {
    myHeart.beatFor(you);
}
// 我的代码世界,因为有你才不会StackOverflow

怎么样?是不是对循环依赖有了全新的认识?下次面试官再问这个问题,你就可以邪魅一笑:"这要从Spring的恋爱观说起..."