Spring - 循环依赖

原文地址:https://www.baeldung.com/circular-dependencies-in-spring

1. 什么是循环依赖

当一个bean A依赖于另一个bean B,并且bean B也依赖于bean A时,就会发生这种情况:

Bean A → Bean B → Bean A

当然了,也可以有很多bean:

Bean A → Bean B → Bean C → Bean D → Bean E → Bean A

2. Spring中发生了什么

当Spring上下文加载所有bean时,它将尝试按照它们完全工作所需的顺序创建bean。

例如,如果我们没有循环依赖关系,例如以下情况:

Bean A → Bean B → Bean C

Spring将创建bean C,然后创建bean B(并将bean C注入到其中),然后创建bean A(并将bean B注入到其中)。

但是,当具有循环依赖关系时,Spring无法决定应首先创建哪个bean,因为它们相互依赖。在这些情况下,Spring在加载上下文时将引发BeanCurrentlyInCreationException。

使用构造函数注入时,它可能会在Spring中发生。如果使用其他类型的注入,则不会发现此问题,因为依赖项将在需要时注入,而不是在上下文加载时注入。

3. 一个简单的例子

让我们定义两个相互依赖的bean(通过构造函数注入):

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

现在,我们可以为测试编写一个Configuration类,将其称为TestConfig,它指定要扫描组件的基本程序包。假设我们的bean是在“ com.baeldung.circulardependency”包中定义的:

@Configuration
@ComponentScan(basePackages = { "com.baeldung.circulardependency" })
public class TestConfig {
}

最后,我们可以编写一个JUnit测试来检查循环依赖。该测试可以为空,因为在上下文加载期间将检测到循环依赖关系。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {
 
    @Test
    public void givenCircularDependency_whenConstructorInjection_thenItFails() {
        // Empty test; we just want the context to load
    }
}

如果您尝试运行此测试,则会收到以下异常:

BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA':
Requested bean is currently in creation: Is there an unresolvable circular reference?

4. 解决方法

我们将展示一些最流行的方法来解决此问题。

4.1. 重新设计

如果您有循环依赖关系,则可能是您遇到了设计问题,并且职责没有很好地分开。您应该尝试正确地重新设计组件,以使它们的层次结构设计得很好,并且不需要循环依赖项。

如果您不能重新设计组件(可能有很多原因:遗留代码,已经过测试且无法修改的代码,没有足够的时间或资源来进行完全重新设计……),则可以尝试一些变通办法。

4.2. 使用@Lazy

打破这个循环的一个简单方法是使用Spring惰性地初始化其中一个bean。也就是说:不是完全初始化bean,而是创建一个代理将其注入到另一个bean中。只有在第一次需要时,才会完全创建注入的bean。

要在您的代码中尝试此操作,可以将CircularDependencyA更改为以下内容:

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}

如果现在运行测试,您将看到这次不会发生错误。

4.3. 使用Setter/Field注入

最受欢迎的解决方法之一,也是Spring文档提出的建议,就是使用setter注入。

简单地说,如果您改变了装配bean的方式,使用setter注入(或字段注入)而不是构造函数注入——这确实解决了问题。通过这种方式,Spring创建了bean,但是依赖项直到需要时才被注入。

让我们这样做-让我们改变我们的类使用setter注入,并将添加另一个字段(消息)到CircularDependencyB,这样我们可以做一个适当的单元测试:

@Component
public class CircularDependencyA {
 
    private CircularDependencyB circB;
 
    @Autowired
    public void setCircB(CircularDependencyB circB) {
        this.circB = circB;
    }
 
    public CircularDependencyB getCircB() {
        return circB;
    }
}
@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;
    }
}

现在我们必须对单元测试进行一些更改:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {
 
    @Autowired
    ApplicationContext context;
 
    @Bean
    public CircularDependencyA getCircularDependencyA() {
        return new CircularDependencyA();
    }
 
    @Bean
    public CircularDependencyB getCircularDependencyB() {
        return new CircularDependencyB();
    }
 
    @Test
    public void givenCircularDependency_whenSetterInjection_thenItWorks() {
        CircularDependencyA circA = context.getBean(CircularDependencyA.class);
 
        Assert.assertEquals("Hi!", circA.getCircB().getMessage());
    }
}

以下说明了上面看到的注释:
@Bean:告诉Spring框架,必须使用这些方法来检索要注入的bean的实现。
@Test:测试将从上下文中获取CircularDependencyA bean,并断言其CircularDependencyB已被正确注入,并检查其消息属性的值。

4.4. 使用@PostContruct

打破这个循环的另一种方法是在其中一个bean上使用@Autowired注入一个依赖项,然后使用一个带有@PostConstruct注解的方法来设置另一个依赖项。

我们的bean可能具有以下代码:

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

我们可以运行与之前相同的测试,因此我们可以检查循环依赖项异常是否仍然没有被抛出,依赖项是否被正确地注入。

4.5. 实现ApplicationContextAware和InitializingBean

如果其中一个bean实现了ApplicationContext,该bean就可以访问Spring上下文,并可以从其中提取另一个bean。实现InitializingBean我们指出这个bean在设置完所有属性之后必须执行一些操作;在本例中,我们希望手动设置依赖项。

我们的bean的代码为:

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

同样,我们可以运行先前的测试,并查看未引发异常,并且该测试按预期运行。

5. 结论

在Spring中有许多方法可以处理循环依赖项。首先要考虑的是重新设计bean,这样就不需要使用循环依赖项:它们通常是可以改进的设计的症状。

但是,如果您绝对需要在项目中使用循环依赖项,您可以遵循这里建议的一些变通方法。

首选的方法是使用setter注入。但是还有其他的选择,通常是基于停止Spring管理bean的初始化和注入,并使用一种或另一种策略自己来完成。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容