Spring circular dependency solution

Foreword: A BeanCurrentlyInCreationException occurred in the test environment, causing the backend service to fail to start. At first glance, it was a circular dependency in Spring’s bean management. There are circular dependencies of beans in the project, which is a sign of low code quality. Most people hope that the framework layer will wipe their butts, causing the design of the entire code to get worse and worse, and finally use some weird tricks to make up for the mistakes made.

1. What is circular dependency?

Circular dependency refers to the mutual dependence between two or more beans, forming a closed loop. It directly manifests itself as two service layers calling each other .

The general scenario is that Bean A depends on Bean B, and Bean B also depends on Bean A.
Bean A → Bean B → Bean A

Of course we can also add more dependency levels, such as:
Bean A → Bean B → Bean C → Bean D → Bean E → Bean A

2. Circular dependencies in Spring

When the Spring context loads all beans, it will try to create them in the order of their relationships. For example, if there are no circular dependencies, for example:
Bean A → Bean B → Bean C
Spring will first create Bean C, then create Bean B (and inject Bean C into Bean B), and finally create Bean A (and inject Bean B into Bean A).
However, if we have a circular dependency, the Spring context does not know which bean should be created first because they depend on each other. In this case, Spring will throw a BeanCurrentlyInCreationException when loading the context.

This also happens when we use constructor methods for injection. If you use other types of injection, you should not encounter this problem. Because it is injected when needed, not when the context is loaded.

3. Let’s look at an example

We define two beans and depend on each other (via constructor injection).

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

Now, we write a test configuration class, let’s call it TestConfig, to specify basic package scanning. Assume our Bean is defined in package “com.baeldung.circulardependency”:

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

Finally, we can write a JUnit test to check for circular dependencies. The test method body can be empty because circular dependencies will be detected during context loading.

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

If you run this test you will get the following exception:

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

4. Solution

We’ll use some of the most popular ways to handle this.

4.1 Redesign

When you have a circular dependency, it’s likely that you have a design problem and the responsibilities are not well separated. You should try to redesign components correctly so that their hierarchy is well designed and there is no need for circular dependencies.

If redesigning the component is not possible (there could be many reasons: legacy code, code that has been tested and cannot be modified, not enough time or resources to completely redesign…), but there are some workarounds to solve the problem .

4.2 Using @Lazy

A simple way to solve Spring’s circular dependencies is to use lazy loading on a bean. In other words: this Bean has not been fully initialized. In fact, it is injected into a proxy, which will only be fully initialized when it is used for the first time.
We modify CircularDependencyA and the results are as follows:

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

If you run the test now, you will find that the previous error is no longer there.

4.3 Injection using Setter/Field

One of the most popular solutions is recommended in the Spring documentation, using setter injection.
Simply put, you use setter injection (or field injection) instead of constructor injection for the beans you need to inject. By creating a Bean in this way, its dependencies are not actually injected at this time. They will only be injected when you need them.

Let’s get started. We will add another property in CircularDependencyB and change both of our Class Beans from constructor injection to setter injection:

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

Now, we unit test the modified code:

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

Below is a description of the annotations seen above:
@Bean: In the Spring framework, it marks that a Bean is created and handed over to Spring for management.
@Test: The test will get the CircularDependencyA bean from the Spring context and assert that CircularDependencyB has been injected correctly and check the value of the property.

4.4 Using @PostConstruct

Another way to break the cycle is to use @Autowired on the property to be injected (which is a bean) and annotate it with @PostConstruct in another method that sets the dependency on other methods.

Our Bean will be modified to the following code:

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

Now we run our modified code and find that no exception is thrown and the dependencies are correctly injected.

4.5 Implement ApplicationContextAware and InitializingBean interface

If a bean implements ApplicationContextAware, the bean has access to the Spring context and can obtain other beans from there. Implementing the InitializingBean interface indicates that this bean will do some post-processing operations after all properties are set (the calling sequence is called after the init-method); in this case, we need to manually set the dependencies.

@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;
    }
}
public class CircularDependencyB {
 
    private CircularDependencyA circA;
 
    private String message = "Hi!";
 
    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
 
    public String getMessage() {
        return message;
    }
}

Similarly, we can run the previous test to see if any exceptions are thrown and whether the program results are what we expect.

5. Summary

There are many ways to deal with Spring’s circular dependencies. But the first thing to consider is redesigning your beans so there’s no need for circular dependencies: they’re usually a symptom of a design that can be improved. However, if you do need to have circular dependencies in your project, then you can follow some of the workarounds proposed here.

Reference links:

happy! SpringBoot finally bans circular dependencies

Solve the problem of Spring Boot 2.6 and later versions canceling support for circular dependencies

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java Skill TreeHomepageOverview 139166 people are learning the system