深入剖析:@Lazy为何能破解Spring的“循环依赖”魔咒?

@Lazy之所以能解决循环依赖问题,是因为它改变了Bean的注入时机。它不再是在容器启动和创建Bean时就立即注入依赖的完整实例,而是注入一个“代理对象”。只有当你第一次真正使用这个依赖对象(即调用它的方法)时,Spring才会去创建并初始化那个真正的Bean实例。

这个“延迟”操作,就像给了Spring一个喘息的机会,从而打破了“你等我,我等你”的死循环僵局。

为了彻底理解这一点,我们需要先搞清楚两个概念:什么是循环依赖,以及Spring默认是如何处理它的。

一、什么是循环依赖?

循环依赖(Circular Dependency)很简单,就是两个或多个Bean之间相互依赖,形成了一个闭环。

最常见的形式是两个Bean互相依赖:

  • ServiceA 依赖 ServiceB
  • ServiceB 又反过来依赖 ServiceA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class ServiceA {
private final ServiceB serviceB;

@Autowired
public ServiceA(ServiceB serviceB) { // A的构造函数需要B
this.serviceB = serviceB;
}
}

@Service
public class ServiceB {
private final ServiceA serviceA;

@Autowired
public ServiceB(ServiceA serviceA) { // B的构造函数需要A
this.serviceA = serviceA;
}
}

二、Spring的困境与默认解决方案

当Spring容器启动时,它会按照顺序创建这些Bean。让我们模拟一下上面这个例子的创建过程:

  1. 创建ServiceA:Spring尝试实例化ServiceA
  2. 发现依赖ServiceBServiceA的构造函数需要一个ServiceB的实例。
  3. 查找ServiceB:Spring去容器里找ServiceB。发现还没有,于是准备创建ServiceB
  4. 创建ServiceB:Spring尝试实例化ServiceB
  5. 发现依赖ServiceAServiceB的构造函数需要一个ServiceA的实例。
  6. 查找ServiceA:Spring去容器里找ServiceA。发现ServiceA也正在创建中,还没创建完成。
  7. 陷入死锁:A等B,B等A,谁也无法先完成实例化。程序卡住,最终Spring会抛出 BeanCurrentlyInCreationException 异常。

这个问题看起来无解,但Spring其实内置了一套非常精妙的机制来解决 一部分 循环依赖问题,这套机制就是大名鼎鼎的 “三级缓存”

注意:Spring的“三级缓存”机制只能解决setter注入和字段注入(@Autowired在字段上)的循环依赖。它无法解决我们上面示例中的构造器注入的循环依赖。因为构造器注入要求在对象实例化(调用new)时,所有参数都必须是现成的、完整的实例,无法中途插入一个“半成品”。

三、救世主登场:@Lazy 的魔法

现在,我们来看 @Lazy 如何解决上面那个棘手的 构造器注入 循环依赖。

我们只需要在其中一个依赖注入点加上 @Lazy 注解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
public class ServiceA {
private final ServiceB serviceB;

// 在A的构造函数中,对B的注入使用@Lazy
@Autowired
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}

public void doAStuff() {
System.out.println("ServiceA is doing stuff...");
// 当这行代码第一次被执行时,真正的ServiceB才会被创建
serviceB.doBStuff();
}
}

@Service
public class ServiceB {
private final ServiceA serviceA;

@Autowired
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}

public void doBStuff() {
System.out.println("ServiceB is doing stuff...");
}
}

加上 @Lazy 之后,Spring的创建流程发生了奇妙的变化:

  1. 创建ServiceA:Spring尝试实例化ServiceA
  2. 发现依赖ServiceB(标记为@LazyServiceA的构造函数需要ServiceB。但由于有 @Lazy 注解,Spring知道它不需要一个完整的ServiceB实例
  3. 注入代理对象:Spring会立即创建一个ServiceB代理对象(Proxy)。这个代理对象不是真正的ServiceB,它只是一个“占位符”,看起来像ServiceB,但内部是空的。然后,Spring将这个代理对象注入到ServiceA的构造函数中。
  4. ServiceA创建成功:由于代理对象满足了构造函数的要求,ServiceA顺利地被实例化并初始化完成,然后被放入了Spring容器的缓存中。
  5. 创建ServiceB:现在轮到创建ServiceB了。
  6. 发现依赖ServiceAServiceB的构造函数需要ServiceA
  7. 查找ServiceA:Spring去容器里找ServiceA,此时ServiceA已经是一个完整的、现成的Bean了。
  8. ServiceB创建成功:Spring将完整的ServiceA实例注入到ServiceB中,ServiceB也顺利创建完成。

循环被打破了!

整个过程中,ServiceA持有的其实是一个ServiceB的代理。只有当未来某个时刻,代码第一次调用 serviceB.doBStuff() 时,这个代理对象才会真正地去向Spring容器请求一个完整的ServiceB实例,并把调用委托给它。

总结与最佳实践

@Lazy 注解的作用:

  • Bean的懒加载:如果@Lazy放在 @Component@Bean 定义上,这个Bean默认不会在容器启动时就创建,而是在第一次被其他Bean引用时才创建。
  • 依赖的懒注入:如果@Lazy放在 @Autowired 或构造函数参数上(如我们的例子),它会注入一个代理对象,延迟真实依赖的获取和创建,从而打破构造器注入的循环依赖

最佳实践与思考:

虽然 @Lazy 能解决问题,但出现循环依赖通常是代码设计不良的信号。它暗示着类之间的职责划分可能不清晰,耦合度过高。

因此,当你不得不使用 @Lazy 来解决循环依赖时,应该优先思考:

  1. 能否重构代码? 是不是可以把公共逻辑抽离到第三个服务中,让A和B都去依赖C,从而打破A和B之间的直接循环?
  2. 是否真的需要构造器注入? 如果业务场景允许,改为setter注入,利用Spring的三级缓存自动解决循环依赖。不过,构造器注入能保证依赖的不可变性,是更推荐的方式。

总而言之,@Lazy 是一个强大的工具,是解决构造器循环依赖的“银弹”,但它更像是一个“创可贴”。最好的做法还是通过优秀的设计从根本上避免循环依赖的产生。