• Spring中的循环依赖解决方案


    前言:测试环境突发BeanCurrentlyInCreationException,导致后端服务启动失败,一看就是Spring的Bean管理中循环依赖。项目中存在Bean的循环依赖,是代码质量低下的表现。多数人寄希望于框架层来给擦屁股,造成了整个代码的设计越来越糟,最后用一些奇技淫巧来填补犯下的错误。

    1、什么是循环依赖?

    循环依赖指的是两个或者多个bean之间相互依赖,形成一个闭环。直接表现为两个service层互相调用对方

    一般场景是一个Bean A依赖Bean B,而Bean B也依赖Bean A.
    Bean A → Bean B → Bean A

    当然我们也可以添加更多的依赖层次,比如:
    Bean A → Bean B → Bean C → Bean D → Bean E → Bean A


    2、Spring中的循环依赖

    当Spring上下文在加载所有的bean时,他会尝试按照他们他们关联关系的顺序进行创建。比如,如果不存在循环依赖时,例如:
    Bean A → Bean B → Bean C
    Spring会先创建Bean C,再创建Bean B(并将Bean C注入到Bean B中),最后再创建Bean A(并将Bean B注入到Bean A中)。
    但是,如果我们存在循环依赖,Spring上下文不知道应该先创建哪个Bean,因为它们依赖于彼此。在这种情况下,Spring会在加载上下文时,抛出一个BeanCurrentlyInCreationException。

    当我们使用构造方法进行注入时,也会遇到这种情况。如果您使用其它类型的注入,你应该不会遇到这个问题。因为它是在需要时才会被注入,而不是上下文加载被要求注入。


    3、让我们看一个例子

    我们定义两个Bean并且互相依赖(通过构造函数注入)。

    1. @Component
    2. public class CircularDependencyA {
    3. private CircularDependencyB circB;
    4. @Autowired
    5. public CircularDependencyA(CircularDependencyB circB) {
    6. this.circB = circB;
    7. }
    8. }
    1. @Component
    2. public class CircularDependencyB {
    3. private CircularDependencyA circA;
    4. @Autowired
    5. public CircularDependencyB(CircularDependencyA circA) {
    6. this.circA = circA;
    7. }
    8. }

    现在,我们写一个测试配置类,姑且称之为TestConfig,指定基本包扫描。假设我们的Bean在包“com.baeldung.circulardependency”中定义:

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

    最后,我们可以写一个JUnit测试,以检查循环依赖。该测试方法体可以是空的,因为循环依赖将上下文加载期间被检测到。

    1. @RunWith(SpringJUnit4ClassRunner.class)
    2. @ContextConfiguration(classes = { TestConfig.class })
    3. public class CircularDependencyTest {
    4. @Test
    5. public void givenCircularDependency_whenConstructorInjection_thenItFails() {
    6. // Empty test; we just want the context to load
    7. }
    8. }

    如果您运行这个测试,你会得到以下异常:

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

    4、解决方法

    我们将使用一些最流行的方式来处理这个问题。

    4.1 重新设计

    当你有一个循环依赖,很可能你有一个设计问题并且各责任没有得到很好的分离。你应该尽量正确地重新设计组件,以便它们的层次是精心设计的,也没有必要循环依赖。

    如果不能重新设计组件(可能有很多的原因:遗留代码,已经被测试并不能修改代码,没有足够的时间或资源来完全重新设计......),但有一些变通方法来解决这个问题。

    4.2 使用 @Lazy

    解决Spring 循环依赖的一个简单方法就是对一个Bean使用延时加载。也就是说:这个Bean并没有完全的初始化完,实际上他注入的是一个代理,只有当他首次被使用的时候才会被完全的初始化。
    我们对CircularDependencyA 进行修改,结果如下:

    1. @Component
    2. public class CircularDependencyA {
    3. private CircularDependencyB circB;
    4. @Autowired
    5. public CircularDependencyA(@Lazy CircularDependencyB circB) {
    6. this.circB = circB;
    7. }
    8. }

    如果你现在运行测试,你会发现之前的错误不存在了。

    4.3 使用 Setter/Field 注入

    其中最流行的解决方法,就是Spring文档中建议,使用setter注入。
    简单地说,你对你须要注入的bean是使用setter注入(或字段注入),而不是构造函数注入。通过这种方式创建Bean,实际上它此时的依赖并没有被注入,只有在你须要的时候他才会被注入进来。

    让我们开始动手干吧。我们将在CircularDependencyB 中添加另一个属性,并将我们两个Class Bean从构造方法注入改为setter方法注入:

    1. @Component
    2. public class CircularDependencyA {
    3. private CircularDependencyB circB;
    4. @Autowired
    5. public void setCircB(CircularDependencyB circB) {
    6. this.circB = circB;
    7. }
    8. public CircularDependencyB getCircB() {
    9. return circB;
    10. }
    11. }
    1. @Component
    2. public class CircularDependencyB {
    3. private CircularDependencyA circA;
    4. private String message = "Hi!";
    5. @Autowired
    6. public void setCircA(CircularDependencyA circA) {
    7. this.circA = circA;
    8. }
    9. public String getMessage() {
    10. return message;
    11. }
    12. }

    现在,我们对修改后的代码进单元测试:

    1. @RunWith(SpringJUnit4ClassRunner.class)
    2. @ContextConfiguration(classes = { TestConfig.class })
    3. public class CircularDependencyTest {
    4. @Autowired
    5. ApplicationContext context;
    6. @Bean
    7. public CircularDependencyA getCircularDependencyA() {
    8. return new CircularDependencyA();
    9. }
    10. @Bean
    11. public CircularDependencyB getCircularDependencyB() {
    12. return new CircularDependencyB();
    13. }
    14. @Test
    15. public void givenCircularDependency_whenSetterInjection_thenItWorks() {
    16. CircularDependencyA circA = context.getBean(CircularDependencyA.class);
    17. Assert.assertEquals("Hi!", circA.getCircB().getMessage());
    18. }
    19. }

    下面对上面看到的注解进行说明:
    @Bean:在Spring框架中,标志着他被创建一个Bean并交给Spring管理
    @Test:测试将得到从Spring上下文中获取CircularDependencyA bean并断言CircularDependencyB已被正确注入,并检查该属性的值。

    4.4 使用 @PostConstruct

    打破循环的另一种方式是,在要注入的属性(该属性是一个bean)上使用 @Autowired ,并使用@PostConstruct 标注在另一个方法,且该方法里设置对其他的依赖。

    我们的Bean将修改成下面的代码:

    1. @Component
    2. public class CircularDependencyA {
    3. @Autowired
    4. private CircularDependencyB circB;
    5. @PostConstruct
    6. public void init() {
    7. circB.setCircA(this);
    8. }
    9. public CircularDependencyB getCircB() {
    10. return circB;
    11. }
    12. }
    1. @Component
    2. public class CircularDependencyB {
    3. private CircularDependencyA circA;
    4. private String message = "Hi!";
    5. public void setCircA(CircularDependencyA circA) {
    6. this.circA = circA;
    7. }
    8. public String getMessage() {
    9. return message;
    10. }
    11. }

    现在我们运行我们修改后的代码,发现并没有抛出异常,并且依赖正确注入进来。

    4.5 实现ApplicationContextAware and InitializingBean接口

    如果一个Bean实现了ApplicationContextAware,该Bean可以访问Spring上下文,并可以从那里获取到其他的bean。实现InitializingBean接口,表明这个bean在所有的属性设置完后做一些后置处理操作(调用的顺序为init-method后调用);在这种情况下,我们需要手动设置依赖。

    1. @Component
    2. public class CircularDependencyA implements ApplicationContextAware, InitializingBean {
    3. private CircularDependencyB circB;
    4. private ApplicationContext context;
    5. public CircularDependencyB getCircB() {
    6. return circB;
    7. }
    8. @Override
    9. public void afterPropertiesSet() throws Exception {
    10. circB = context.getBean(CircularDependencyB.class);
    11. }
    12. @Override
    13. public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
    14. context = ctx;
    15. }
    16. }
    1. public class CircularDependencyB {
    2. private CircularDependencyA circA;
    3. private String message = "Hi!";
    4. @Autowired
    5. public void setCircA(CircularDependencyA circA) {
    6. this.circA = circA;
    7. }
    8. public String getMessage() {
    9. return message;
    10. }
    11. }

    同样,我们可以运行之前的测试,看看有没有异常抛出,程序结果是否是我们所期望的那样。


    5、总结

    有很多种方法来应对Spring的循环依赖。但考虑的第一件事就是重新设计你的bean,所以没有必要循环依赖:他们通常是可以提高设计的一种症状。 但是,如果你在你的项目中确实是需要有循环依赖,那么你可以遵循一些这里提出的解决方法。


    参考链接:

    痛快!SpringBoot终于禁掉了循环依赖

    解决Spring Boot 2.6及之后版本取消了循环依赖的支持的问题

  • 相关阅读:
    食堂点餐小程序,智慧食堂小程序,食堂预约点餐小程序毕设作品
    2020最新Java面试题
    关于#matlab#的问题:我看到了gp-ols算法,请问gp我可以理解为是线性拟合吗,还是利用树结构寻优
    线上故障突突突?如何紧急诊断、排查与恢复
    cs224n-2022-assignment1
    LeetCode //C - 210. Course Schedule II
    linux 使用crontab 创建定时任务
    Python自动化办公(一) —— 根据PDF文件批量创建Word文档
    【JavaScript复习十二】数组内置对象方法二
    订单服务-----功能实现逻辑
  • 原文地址:https://blog.csdn.net/CSDN2497242041/article/details/134290500