用@Lazy破解Spring循环依赖:一个缓兵之计的智慧

159 阅读3分钟

用@Lazy破解Spring循环依赖:一个"缓兵之计"的智慧

一、当依赖关系陷入死循环

假设我们有两个热恋中的Bean:

@Component
public class Boy {
    private final Girl lover;
    
    public Boy(Girl lover) { // 构造器注入
        this.lover = lover;
    }
}

@Component
public class Girl {
    private final Boy lover;
    
    public Girl(Boy lover) { // 构造器注入
        this.lover = lover;
    }
}

当Spring尝试创建这两个Bean时,会陷入典型的"先有鸡还是先有蛋"的困境:

  1. 创建Boy需要先有Girl
  2. 创建Girl需要先有Boy
  3. 创建Boy需要先有Girl...
  4. ➔ 无限循环警告!

二、Spring的常规解法为何失效?

Spring通过三级缓存解决普通循环依赖:

  1. 三级缓存架构

    • 一级缓存:存放完整Bean
    • 二级缓存:存放早期暴露对象
    • 三级缓存:存放ObjectFactory
  2. 标准处理流程(以setter注入为例):

    • 实例化Boy(半成品)→ 放入二级缓存
    • 填充属性时需要Girl → 触发Girl的创建
    • 实例化Girl(半成品)→ 同样放入二级缓存
    • Girl填充属性时从二级缓存拿到Boy的引用
    • Girl初始化完成→ 移入一级缓存
    • Boy完成初始化

构造器注入打破了这种默契:

  • 必须在实例化阶段就完成依赖注入
  • 半成品对象尚未放入缓存
  • 导致无法获取对方引用

三、@Lazy的破局之道

3.1 核心思路:时空解耦

通过代理对象将即时依赖转变为延时依赖,打破必须立即满足的依赖链。

3.2 实现原理剖析

当使用@Lazy时,Spring会:

  1. 创建动态代理对象(CGLIB或JDK Proxy)
  2. 代理对象持有目标Bean的元数据
  3. 首次调用代理对象的方法时触发真实Bean的初始化
@Component
public class Boy {
    private final Girl lover;
    
    public Boy(@Lazy Girl lover) { // 关键注解
        this.lover = lover;
    }
}

3.3 时序图解析

sequenceDiagram
    participant SpringContainer
    participant Boy
    participant GirlProxy
    participant RealGirl
    
    SpringContainer->>Boy: 开始初始化
    Boy->>SpringContainer: 需要Girl实例
    SpringContainer->>GirlProxy: 生成代理对象
    GirlProxy-->>Boy: 返回代理
    Boy->>SpringContainer: 完成初始化
    SpringContainer->>RealGirl: 开始真正初始化
    RealGirl->>SpringContainer: 需要Boy实例
    SpringContainer->>Boy: 返回已初始化的Boy
    RealGirl-->>SpringContainer: 完成初始化
    GirlProxy->>RealGirl: 代理调用转接

四、深入代理机制

4.1 代理对象的本质

生成的代理类伪代码示意:

public class GirlProxy extends Girl {
    private TargetSource targetSource;
    
    public void realMethod() {
        if (target == null) {
            target = getBeanFromContext(); // 延迟初始化
        }
        return target.realMethod();
    }
}

4.2 初始化触发点

代理对象首次被访问时(方法调用、属性访问),通过以下机制触发真实对象的创建:

  1. 拦截器检查目标对象状态
  2. 若未初始化则通过BeanFactory获取真实Bean
  3. 将后续调用委托给真实对象

五、使用时的注意事项

  1. 作用域限制

    • 只适用于单例Bean
    • 原型(prototype)作用域会直接报错
  2. 代理类型选择

    • 默认使用CGLIB代理
    • 可通过@Scope配置proxyMode
  3. AOP兼容性

    @Bean
    @Lazy
    @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
    public MyService myService() {
        return new MyServiceImpl();
    }
    
  4. 性能考量

    • 代理对象会增加调用开销
    • 建议只在必要时使用

六、最佳实践建议

  1. 优先通过设计避免循环依赖
  2. 构造器注入时推荐使用@Lazy
  3. 在@Bean方法参数中使用示例
    @Configuration
    public class AppConfig {
        @Bean
        public ServiceA serviceA(@Lazy ServiceB serviceB) {
            return new ServiceAImpl(serviceB);
        }
    }
    
  4. 结合单元测试验证
    @Test
    void testCircularDependency() {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            ServiceA serviceA = context.getBean(ServiceA.class);
            assertThat(serviceA).isInstanceOf(ServiceA.class);
        }
    }
    

七、思考:为何不全部使用@Lazy?

虽然@Lazy能解决循环依赖,但过度使用会:

  1. 增加内存开销(代理对象)
  2. 掩盖设计缺陷(本应避免的循环依赖)
  3. 导致异常延迟暴露(启动时问题推迟到运行时)

Spring的循环依赖处理机制就像精密的齿轮组,@Lazy则是应急的润滑剂。理解其原理,才能在架构设计与框架特性之间找到最佳平衡点。