spring cycle dependency problem

Posted by headbangerbuggy on Wed, 11 Mar 2020 10:49:06 +0100

1. The cause of circular dependency - injected through the constructor

Let's define two interdependent bean s (injected through the constructor):

@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 can write a Configuration class for the test, let's call it TestConfig, which specifies the underlying package to scan for the component. Suppose our bean is defined in the package "com.baeldung.circulardependency":

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

Finally, we can write a JUnit test to check the loop dependency. The test can be empty because cyclic dependencies are 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 try to run this test, the following exception occurs:

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

2. Solutions

2.1 use @ Lazy

An easy way to break the loop is to have Spring lazily initialize one of the beans. That is: instead of fully initializing the bean, it creates a proxy to inject it into another bean. The injected bean is only fully created when it is first needed.

To try this operation with our code, you can change the CircularDependencyA to the following

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

If you run the test now, you will see that this error will not occur.

2.2 using Setter / Field Injection

One of the most popular solutions, also proposed by Spring documentation, is to use setter injection.

Simply put, if you change the way your beans are connected, use setter injection (or live injection) instead of constructor injection - which does solve the problem. Spring then creates beans, but does not inject dependencies until it needs to.

Let's do this - let's change our class to use setter injection and add another field (message) to CircularDependencyB so we can do the appropriate unit tests:

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

2.2 principle explanation

Timing of cycle dependence
Bean instantiation mainly consists of three steps, as shown in the figure:

The problem is in the process of the first and second steps, which is to populate the properties / methods

How Spring solves

  • Spring uses a three-level cache to solve the circular dependency problem of a single instance. When it calls recursively, it finds that the Bean is still in the process of creation, which is called circular dependency
  • The Bean of the singleton mode is saved in the following data structure:
/** First level cache: used to store fully initialized bean s**/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** Second level cache: store the original bean object (properties have not been filled) to solve the circular dependency */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);

/** Three level cache: store bean factory objects to solve circular dependency */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/**
bean Acquisition process of: first obtain from level 1, then obtain from level 2 and level 3 after failure

Creating state: the object has been new but all properties are null waiting for init
*/

The process of detecting circular dependency is as follows:

  • A needs B in the creation process, so a puts itself in the three-level buffer to instantiate B

  • When instantiating B, it was found that A was needed, so B checked the first level cache first, no, then the second level cache, or no, then the third level cache, and found it!

    • Then put the A in the level 3 cache into the level 2 cache, and delete the A in the level 3 cache
    • B successfully initializes and puts itself in the first level cache (at this time, A in B is still in the creating state)
  • Then come back and create A. at this time, B has finished creating. Get B directly from the first level cache, and then finish creating and put yourself into the first level cache
    This solves the problem of circular dependence

// Source code described above
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
	//B check the first level cache first
    Object singletonObject = this.singletonObjects.get(beanName);
     //No, check the L2 cache again,
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            singletonObject = this.earlySingletonObjects.get(beanName);
            //No, we still haven't
            if (singletonObject == null && allowEarlyReference) {
                ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                if (singletonFactory != null) {
                    singletonObject = singletonFactory.getObject();
                    //Then put the A in the level 3 cache into the level 2 cache, and delete the A in the level 3 cache
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    //And delete A in Level 3 cache
                    this.singletonFactories.remove(beanName);
                }
            }
        }
    }
    //B. after successful initialization, put yourself into the first level cache
    return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

The above corresponds to the setter construction injection in the xml configuration file, and scope = "singleton", while scope = "prototype", the Spring container does not cache, so it is unable to expose a Bean created in advance.

  • scope="singleton",
    When the scope of a bean is set to singleton, only one shared bean instance will exist in the Spring IOC container, and all requests for beans, as long as the id matches the bean definition, will only return the same instance of the bean. In other words, when a bean definition is set to singleton scope, the Spring IOC container will only create a unique instance of the bean definition. This single instance will be stored in the singleton cache, and all subsequent requests and references for this bean will return the cached object instance. Note that the singleton scope and the single instance in the GOF design pattern are completely different. The singleton design pattern represents a ClassLoader There is only one class, and singleton here means that a container corresponds to a bean. That is to say, when a bean is identified as singleton, only one bean will exist in spring's IOC container.

  • scope="prototype"
    For the bean deployed in the prototype scope, every request (inject it into another bean, or call the container's getBean() method programmatically) will generate a new bean instance, which is equivalent to a new operation. For the bean deployed in the prototype scope, it is very important that Spring cannot be used for a prototype The whole life cycle of a bean is responsible. After the container initializes, configures, decorates or assembles a prototype instance, it gives it to the client, and then ignores the prototype instance. No matter what scope, the container will call the initialization life cycle callback method of all objects, and for prototype, any configured analysis life cycle callback method will not be called. It is the responsibility of the client code to clear the objects of the prototype scope and release the expensive resources held by any prototype bean. (a possible way for the Spring container to release resources occupied by a singleton scope bean is by using the bean's postprocessor, which holds a reference to the bean to be cleared.)

Published 28 original articles, won praise and 919 visitors
Private letter follow

Topics: Spring Junit xml