用@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时,会陷入典型的"先有鸡还是先有蛋"的困境:
- 创建Boy需要先有Girl
- 创建Girl需要先有Boy
- 创建Boy需要先有Girl...
- ➔ 无限循环警告!
二、Spring的常规解法为何失效?
Spring通过三级缓存解决普通循环依赖:
-
三级缓存架构:
- 一级缓存:存放完整Bean
- 二级缓存:存放早期暴露对象
- 三级缓存:存放ObjectFactory
-
标准处理流程(以setter注入为例):
- 实例化Boy(半成品)→ 放入二级缓存
- 填充属性时需要Girl → 触发Girl的创建
- 实例化Girl(半成品)→ 同样放入二级缓存
- Girl填充属性时从二级缓存拿到Boy的引用
- Girl初始化完成→ 移入一级缓存
- Boy完成初始化
但构造器注入打破了这种默契:
- 必须在实例化阶段就完成依赖注入
- 半成品对象尚未放入缓存
- 导致无法获取对方引用
三、@Lazy的破局之道
3.1 核心思路:时空解耦
通过代理对象将即时依赖转变为延时依赖,打破必须立即满足的依赖链。
3.2 实现原理剖析
当使用@Lazy时,Spring会:
- 创建动态代理对象(CGLIB或JDK Proxy)
- 代理对象持有目标Bean的元数据
- 首次调用代理对象的方法时触发真实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 初始化触发点
代理对象首次被访问时(方法调用、属性访问),通过以下机制触发真实对象的创建:
- 拦截器检查目标对象状态
- 若未初始化则通过BeanFactory获取真实Bean
- 将后续调用委托给真实对象
五、使用时的注意事项
-
作用域限制:
- 只适用于单例Bean
- 原型(prototype)作用域会直接报错
-
代理类型选择:
- 默认使用CGLIB代理
- 可通过@Scope配置proxyMode
-
AOP兼容性:
@Bean @Lazy @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) public MyService myService() { return new MyServiceImpl(); } -
性能考量:
- 代理对象会增加调用开销
- 建议只在必要时使用
六、最佳实践建议
- 优先通过设计避免循环依赖
- 构造器注入时推荐使用@Lazy
- 在@Bean方法参数中使用示例:
@Configuration public class AppConfig { @Bean public ServiceA serviceA(@Lazy ServiceB serviceB) { return new ServiceAImpl(serviceB); } } - 结合单元测试验证:
@Test void testCircularDependency() { try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) { ServiceA serviceA = context.getBean(ServiceA.class); assertThat(serviceA).isInstanceOf(ServiceA.class); } }
七、思考:为何不全部使用@Lazy?
虽然@Lazy能解决循环依赖,但过度使用会:
- 增加内存开销(代理对象)
- 掩盖设计缺陷(本应避免的循环依赖)
- 导致异常延迟暴露(启动时问题推迟到运行时)
Spring的循环依赖处理机制就像精密的齿轮组,@Lazy则是应急的润滑剂。理解其原理,才能在架构设计与框架特性之间找到最佳平衡点。