1. 引言
在 Spring 中,bean 往往不会独立存在,bean 的相互依赖是极为常见的。在这一过程中,错综复杂的 bean 依赖关系一旦造成了循环依赖,往往十分令人头疼,那么,作为使用者,如果遇到了循环依赖问题,我们应该如何去解决呢?本文我们就来为您详细解读。
2. 什么是循环依赖
2.1 循环依赖的例子
循环依赖很容易理解,简单的来说,就是 A 依赖 B,B 同时又依赖于 A,比如下面的例子:
代码语言:javascript复制@Component
public class CircularDependencyA {
@Autowired
private CircularDependencyB circB;
}
@Component
public class CircularDependencyB {
@Autowired
private CircularDependencyA circA;
}
可能实际情况中要比这个例子复杂,比如 B 依赖于 C,C 依赖于 D。。。。最后这个依赖链条的终点又依赖回了 A,这样的情况不借助工具可能就很难发现了,特殊的,一个 bean 也可能通过这样的依赖链条最后依赖回了自己,这同样也是循环依赖的问题。
在这里说一下,本文的讨论有个前提,那就是被注入的 bean 都是单例的。这很容易理解,如果 A 依赖的 B 对象不是单例的,那么,Spring 就会直接创建一个新的 B 对象,而它发现 B 对象依赖 A 对象,并且也不是单例的,自然也就会直接去创建一个对象,如此反复下去,就陷入了死循环,直接导致溢出了,我们也无从去解决这样的循环依赖问题。
2.2 setter 注入与构造器注入
上面的例子展示了 setter 注入的依赖方式,比如 A 通过 setter 注入的方式依赖 B,Spring 会将 B 的实例通过反射调用 A 的 setter 方法来实现注入。setter 注入的方式如果发生循环依赖,Spring 是可以替我们解决的,这也就是我们通常并不会发现项目中存在的循环依赖的原因。
除了 setter 注入,Spring 还有另一种注入方式,构造器注入:
代码语言:javascript复制@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(CircularDependencyB circB) {
this.circB = circB;
}
}
@Component
public class CircularDependencyB {
private CircularDependencyA circA;
@Autowired
public CircularDependencyB(CircularDependencyA circA) {
this.circA = circA;
}
}
这样的注入方式看起来似乎更容易理解,当 Spring 要创建 A 对象时,必须以 B 对象作为参数,随着 A 对象的创建,A 依赖的 B 对象也就被注入到了 A bean 中,正如上面的例子,它也同样可能存在循环依赖。
不幸的是,这样的循环依赖一旦形成,Spring 启动过程中就会报错:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?
那么,如何来解决循环依赖呢?
3. 循环依赖的解决办法
在 Spring 的设计中,已经预先考虑到了可能的循环依赖问题,并且提供了一系列方法供我们使用。下面就一一来为您介绍。
3.1 重新设计
从项目整体来看,一旦存在一个循环依赖,那么很可能此时已经存在着一个设计问题了,因为很明显,各个模块的责任没有被很好地分层和隔离。
我们最先做的应该是去审视整个项目的层次结构,去追问循环依赖是不是必然产生的。通过重新设计,去规避循环依赖的过程中,可能实际上是去规避了更大的隐患。
当然,在实际场景下,可能当循环依赖出现时,重新设计已经显得有些“何不食肉糜”了,我们需要更加切实可行、立竿见影的解决方法。
3.2 使用 setter 注入
这是解决循环依赖最流行的方式之一,也是 Spring 官方文档建议的:
https://docs.spring.io/spring/docs/current/spring-framework-reference/html/beans.html
正如我们上文所介绍的,BeanCurrentlyInCreationException 的产生往往源于构造器注入方式的循环依赖。把它们改成 setter 注入,就可以利用 Spring 自身的机制来处理循环依赖。
在 Spring 配置中,默认已经开启了 setter 注入的循环依赖解决机制,如果你想关掉它,可以配置:
spring.main.allow-circular-references=false
至于为什么 Spring 会在我们使用 setter 注入时自动地解决循环依赖,以及它是怎么做的, 下一篇文章我们会详细进行介绍。
3.3 使用 @Lazy 注解
@Lazy 注解告诉 Spring 不要立即初始化 bean,而是先创建一个 proxy 对象,以此作为原对象的工厂注入到被依赖的 bean 中去,只有当程序执行时,这个被代理的 bean 第一次被使用时,它才会被完全创建。
代码语言:javascript复制@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(@Lazy CircularDependencyB circB) {
this.circB = circB;
}
}
在这个例子中,CircularDependencyA 对象中实际上注入的是 circB 的代理对象,circB 并没有被创建,这也就意味着在创建 CircularDependencyA 的 bean 对象时,并不会去解析 CircularDependencyB 的构造方式,也就不会发现存在循环依赖的问题。而在代码执行过程中,真正要去创建 CircularDependencyB 对象时,此时在 Spring 上下文中,早已存在了 CircularDependencyA 的 bean 对象实例,CircularDependencyB 依赖的 circA 对象能够直接通过 getSigleton 方法获取到,也就不存在循环依赖的问题了。
3.4 使用 @PostConstruct 注解
@PostConstruct 注解会在 Spring 容器初始化的时候被调用,我们可以在这个过程中,将当前对象的引用传递给我们所依赖的对象,从而避免依赖的对象从 Spring 上下文获取而产生的循环依赖问题。
代码语言:javascript复制@Component
public class CircularDependencyA {
@Autowired
private CircularDependencyB circB;
@PostConstruct
public void init() {
circB.setCircA(this);
}
public CircularDependencyB getCircB() {
return circB;
}
}
代码语言:javascript复制@Component
public class CircularDependencyB {
private CircularDependencyA circA;
private String message = "Hi!";
public void setCircA(CircularDependencyA circA) {
this.circA = circA;
}
public String getMessage() {
return message;
}
}
3.5 通过 Spring 上下文初始化 bean
如果一个 Bean 从 Spring 上下文中获取另一个 Bean,我们就可以手动去设置 Bean 的依赖项,避免 Spring 解析依赖项的过程中产生的循环依赖。
例如:
代码语言:javascript复制@Component
public class CircularDependencyA implements ApplicationContextAware, InitializingBean {
private CircularDependencyB circB;
private ApplicationContext context;
public CircularDependencyB getCircB() {
return circB;
}
@Override
public void afterPropertiesSet() throws Exception {
circB = context.getBean(CircularDependencyB.class);
}
@Override
public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
context = ctx;
}
}
代码语言:javascript复制@Component
public class CircularDependencyB {
private CircularDependencyA circA;
private String message = "Hi!";
@Autowired
public void setCircA(CircularDependencyA circA) {
this.circA = circA;
}
public String getMessage() {
return message;
}
}
4. 总结
本文介绍了在 Spring 使用过程中,避免循环依赖的处理方法。这些方法通过改变 bean 对象的实例化、初始化的时机,避免了循环依赖的产生,它们之间有着微妙的差别。如果在 Spring 使用过程中,你并不关注于 Bean 对象的实例化和初始化的具体细节,那么,使用 setter 注入的方式是首选的解决方案。
当然,循环依赖往往意味着糟糕的设计,尽早发现和重构设计,很可能成为避免系统中隐藏的更大问题的关键。
至于 Spring 是通过什么样的方式来解决 setter 注入时的循环依赖问题的,下一篇文章我们会进行详细讲解,敬请期待。
参考资料
https://www.baeldung.com/circular-dependencies-in-spring
https://medium.com/javarevisited/please-dont-use-circular-dependencies-in-spring-boot-projects-d57a473839d5
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans