如何在 Spring 中解决 bean 的循环依赖

2022-09-12 17:25:40   最后更新: 2022-09-12 17:25:40   访问数量:44




 

在 Spring 中,bean 往往不会独立存在,bean 的相互依赖是极为常见的。在这一过程中,错综复杂的 bean 依赖关系往往很容易会造成循环依赖。可是在实际情况中,我们却很少遇到 bean 循环依赖造成的报错,以至于我们往往会忽略这类情况。很显然,是 Spring 默默为我们解决了循环依赖的问题,那么,它是如何解决的?它是万能的吗?本文我们就来解读一下。

 

 

 

 

2.1 循环依赖的例子

 

循环依赖很容易理解,简单的来说,就是 A 依赖 B,B 同时又依赖于 A,比如下面的例子:

 

@Component public class CircularDependencyA { @Autowired private CircularDependencyB circB; } @Component public class CircularDependencyB { @Autowired private CircularDependencyA circA; }

 

 

可能实际情况中要比这个例子复杂,比如 B 依赖于 C,C 依赖于 D。。。。最后这个依赖链条的终点又依赖回了 A,这样的情况不借助工具可能就很难发现了,特殊的,一个 bean 也可能通过这样的依赖链条最后依赖回了自己,这同样也是循环依赖的问题。

 

但是这有个前提,那就是被注入的是单例 bean,Spring 才能够有可能去解决循环依赖的问题。这很容易理解,如果 A 依赖的 B 对象不是单例的,那么,Spring 就会直接创建一个新的 B 对象,而它发现 B 对象依赖 A 对象,并且也不是单例的,自然也就会直接去创建一个  对象,如此反复下去,就陷入了死循环,直接导致溢出了。

 

2.2 setter 注入与构造器注入

 

上面的例子展示了 setter 注入的依赖方式,比如 A 通过 setter 注入的方式依赖 B,Spring 会将 B 的实例通过反射调用 A 的 setter 方法来实现注入。setter 注入的方式如果发生循环依赖,Spring 是可以替我们解决的,这也就是我们没有发现项目中存在的循环依赖的原因。

 

除了 setter 注入,Spring 还有另一种注入方式,构造器注入:

 

@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?

 

那么,如何来解决循环依赖呢?

 

 

在 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 第一次被使用时,它才会被完全创建。

 

@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 上下文获取而产生的循环依赖问题。

 

@Component public class CircularDependencyA { @Autowired private CircularDependencyB circB; @PostConstruct public void init() { circB.setCircA(this); } public CircularDependencyB getCircB() { return circB; } }

 

 

@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 解析依赖项的过程中产生的循环依赖。

 

例如:

 

@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; } }

 

 

@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; } }

 

 

 

本文介绍了在 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

 

 

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

Spring 从入门到实战






spring      ioc      依赖注入      circular dependencies      循环依赖     


京ICP备2021035038号