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

2022-12-21 17:43:22 浏览数 (1)

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

0 人点赞