什么是循环依赖以及解决方式

1.什么是循环依赖?

它发生在bean A依赖于另一个bean B时,bean B依赖于bean A:

豆A→豆B→豆A

当然,我们可以有更多的暗示:

豆A→豆B→豆C→豆D→豆E→豆A

2.春天会发生什么

当Spring上下文加载所有bean时,它会尝试按照它们完全工作所需的顺序创建bean。例如,如果我们没有循环依赖,如下例所示:

豆A→豆B→豆C.

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

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

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

3.一个快速示例

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

1

2

3

4

5

6

7

8

9

10

@Component

public class CircularDependencyA {

 

    private CircularDependencyB circB;

 

    @Autowired

    public CircularDependencyA(CircularDependencyB circB) {

        this.circB = circB;

    }

}

1

2

3

4

5

6

7

8

9

10

@Component

public class CircularDependencyB {

 

    private CircularDependencyA circA;

 

    @Autowired

    public CircularDependencyB(CircularDependencyA circA) {

        this.circA = circA;

    }

}

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

1

2

3

4

@Configuration

@ComponentScan(basePackages = { "com.baeldung.circulardependency" })

public class TestConfig {

}

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

1

2

3

4

5

6

7

8

9

@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更改为以下内容:

1

2

3

4

5

6

7

8

9

10

@Component

public class CircularDependencyA {

 

    private CircularDependencyB circB;

 

    @Autowired

    public CircularDependencyA(@Lazy CircularDependencyB circB) {

        this.circB = circB;

    }

}

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

4.3。使用Setter / Field Injection

最流行的解决方法之一,也是Spring文档提出的,是使用setter注入。

简单地说,如果你改变你的bean的连接方式,使用setter注入(或现场注入)而不是构造函数注入 - 这确实解决了这个问题。这样Spring就会创建bean,但是在需要之前不会注入依赖项。

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@Component

public class CircularDependencyA {

 

    private CircularDependencyB circB;

 

    @Autowired

    public void setCircB(CircularDependencyB circB) {

        this.circB = circB;

    }

 

    public CircularDependencyB getCircB() {

        return circB;

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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

    }

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

@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已正确注入,检查其message属性的值。

4.4。使用@PostConstruct

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

@Component

public class CircularDependencyA {

 

    @Autowired

    private CircularDependencyB circB;

 

    @PostConstruct

    public void init() {

        circB.setCircA(this);

    }

 

    public CircularDependencyB getCircB() {

        return circB;

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

@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。实现ApplicationContextAwareInitializingBean

如果其中一个bean实现ApplicationContextAware,则bean可以访问Spring上下文,并可以从那里提取其他bean。实现InitializingBean我们指出这个bean必须在设置了所有属性后执行一些操作; 在这种情况下,我们想手动设置我们的依赖项。

我们的bean的代码是:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

@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,这样就不需要循环依赖:它们通常是可以改进的设计的症状。

但是如果你的项目中绝对需要循环依赖,那么你可以遵循这里建议的一些解决方法。

优选的方法是使用二次注射。但是还有其他选择,通常基于阻止Spring管理bean的初始化和注入,以及使用一种策略或另一种策略自己做。

你可以找到上面显示的豆这里,和单元测试在这里

  • 46
    点赞
  • 172
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
### 回答1: 循环依赖指两个或多个模块相互依赖,且彼此之间形成了一个闭环的依赖关系。在编译、构建或运行程序时,循环依赖可能会导致无法正确加载、编译或执行代码。 解决循环依赖的方法包括: 1. 重构代码结构:将依赖关系打破,使得各个模块之间不再相互依赖。 2. 引入中间件:引入一个新的模块或组件,作为两个或多个模块之间的中间层,从而避免直接相互依赖。 3. 延迟加载:延迟加载某些模块,直到真正需要使用它们时再进行加载,以避免循环依赖。 4. 依赖注入:通过依赖注入框架,将依赖的对象注入到需要使用它们的模块中,从而避免循环依赖。 需要注意的是,解决循环依赖问题并不是一件简单的事情,需要结合实际情况进行具体分析和处理。 ### 回答2: 循环依赖是指在一个系统或者模块间存在相互依赖的情况,其中一个模块依赖于另一个模块,而该模块又依赖于第一个模块,形成了一个循环的依赖关系。 循环依赖会导致以下问题:首先,造成死锁,因为两个模块都在等待对方完成;其次,增加了开发和维护的复杂性,因为难以准确地确定模块间的先后顺序;最后,降低了系统的灵活性,因为任何一个模块的变更都可能会影响到其他所有依赖于它的模块。 要解决循环依赖问题,可以采取以下几种方法: 1. 重构代码结构:重新组织代码结构,通过拆分或合并模块来消除循环依赖。这个过程中,需要仔细分析模块之间的依赖关系,找出冗余的依赖,减少它们之间的耦合度。 2. 引入中间层:在循环依赖的模块之间引入中间层,将循环依赖改为单向依赖。这样,就能够确保模块间的依赖关系按照正确的顺序进行。 3. 使用回调函数:将模块之间的相互调用改为通过回调函数来实现。这样,模块的调用顺序不再依赖于相互之间的依赖关系,可以更加灵活地控制程序的执行。 4. 引入观察者模式:将循环依赖的模块之间引入观察者模式,通过事件的发布和订阅机制来解耦模块间的依赖关系。 总之,解决循环依赖问题需要深入分析模块之间的依赖关系,并采取适当的措施来降低模块间的耦合度,从而达到消除循环依赖的目的。 ### 回答3: 循环依赖是指两个或多个对象之间互相依赖,形成了一个闭环,导致程序无法正常执行或产生错误的情况。 循环依赖通常出现在对象之间的相互引用上。举例来说,对象A引用了对象B,而对象B又引用了对象A,它们之间形成了一个循环依赖。在这种情况下,当我们在创建或使用这些对象时,程序可能会陷入无限循环,或者无法正确地处理对象间的依赖关系。 要解决循环依赖问题,可以考虑以下几种方法: 1. 重构代码结构:分析循环依赖的原因,重新设计代码结构,将相互依赖的部分提取到一个单独的模块或类中,减少或避免循环依赖的发生。 2. 使用接口或抽象类:通过引入接口或抽象类,将具体实现类抽象出来,降低了依赖的耦合度,从而可以在不产生循环依赖的情况下正常使用对象。 3. 引入中间层或事件驱动机制:通过引入中间层或事件驱动机制,将对象之间的直接依赖转变为间接依赖,避免了循环依赖的发生。例如,使用消息队列或事件总线来解耦对象之间的依赖关系。 4. 使用依赖注入框架:依赖注入框架可以帮助管理对象之间的依赖关系,通过外部容器来管理对象的创建和注入,从而减少了手动管理依赖的复杂性,避免了循环依赖的问题。 总之,解决循环依赖问题需要对代码结构进行合理设计,降低依赖的耦合度,引入合适的设计模式或框架,从而实现代码的灵活性和可维护性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值