Detailed explanation of circular dependency resolution in Spring

Posted by BooRadLey on Tue, 25 Jan 2022 14:58:25 +0100

catalogue

1 what is circular dependency?

1.1 constructor loop dependency

1.2 field attribute injection cyclic dependency

1.3 field attribute injection loop dependency (prototype)

2. Circular dependency processing

2.1 constructor loop dependency (unresolved)

2.2 # setter cyclic dependency (can be solved)

2.3 dependency handling of prototype scope (unable to be solved)

3 how does spring solve circular dependency?

Simple circular dependency (no AOP)

Combined with the cyclic dependency of AOP

4 interview answers

1 what is circular dependency?

Circular dependency is circular reference, which means that two or more bean s hold each other. For example, TestA references TestB, and TestB references TestA, and finally form a closed loop.

Note: circular dependency does not refer to circular calls.

Literally, A depends on B, and B also depends on A, as shown below

Circular call: refers to the ring call between methods. Circular call has no solution. Unless there is an end condition, it is an endless loop, which will eventually lead to memory overflow exception.

There are two Spring container circular dependencies:

  1. Constructor loop dependency
  2. setter method cyclic dependency

This is what it looks like at the code level

1.1 constructor loop dependency

@Service
public class A {  
    public A(B b) {  }
}

@Service
public class B {  
    public B(C c) {  
    }
}

@Service
public class C {  
    public C(A a) {  }
}

Result: the project failed to start and a cycle was found

1.2 field attribute injection cyclic dependency

@Service
public class A1 {  
    @Autowired  
    private B1 b1;
}

@Service
public class B1 {  
    @Autowired  
    public C1 c1;
}

@Service
public class C1 {  
    @Autowired  public A1 a1;
}

Result: the project was started successfully

1.3 field attribute injection loop dependency (prototype)

@Service
@Scope("prototype")
public class A1 {  
    @Autowired  
    private B1 b1;
}

@Service
@Scope("prototype")
public class B1 {  
    @Autowired  
    public C1 c1;
}

@Service
@Scope("prototype")
public class C1 {  
    @Autowired  public A1 a1;
}

Result: the project failed to start and a cycle was found.

Phenomenon summary: similarly, for the scenario of circular dependency, constructor injection and prototype type attribute injection will fail to initialize the Bean. Because @ Service is singleton by default, attribute injection of singleton can be successful

2. Circular dependency processing

2.1 constructor loop dependency (unresolved)

Represents a circular dependency formed by constructor injection. This dependency has no solution. Forced dependency can only throw an exception (BeanCreationException);

The Spring container places each bean identifier being created in a "currently created bean pool", and the bean identifier will always remain in this pool during the creation process. Therefore, if you find yourself already in the pool during the creation of beans, you will throw a beancurrentyincreationexception to indicate circular dependency; The created beans will be cleared from the "currently created bean pool".

Let's confirm the above theory through a piece of code.

Create application The XML configuration file is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <bean id="testA" class="com.chenpt.springFrameWork.TestA">
        <constructor-arg name="testB" ref="testB"></constructor-arg>
    </bean>
 
    <bean id="testB" class="com.chenpt.springFrameWork.TestB">
        <constructor-arg name="testA" ref="testA"></constructor-arg>
    </bean>
 
</beans>

Client test case:

public class MainTest {
    public static void main(String[] args){
        ApplicationContext context = new FileSystemXmlApplicationContext("classpath:spring/applicationContext.xml");
 
    }
}

The execution result is shown in the figure below (only some errors are truncated)

2.2 # setter cyclic dependency (can be solved)

It refers to the cyclic dependency formed by setter injection.

Solution: the Spring container exposes the beans that have just completed the constructor injection but have not completed other steps (such as setter injection) in advance. Moreover, it can only solve the bean circular dependency of the singleton scope. By exposing a singleton factory method in advance, other beans can reference the bean.

Code example:

First, you need to remove the parameters injected by the constructor.

//bean1
public class TestA {
    private TestB testB;
 
    TestA(){}
 
    public TestB getTestB() {
        return testB;
    }
 
    public void setTestB(TestB testB) {
        this.testB = testB;
    }
}
//bean2
public class TestB {
    private TestA testA;
 
    TestB(){}
 
    public TestA getTestA() {
        return testA;
    }
 
    public void setTestA(TestA testA) {
        this.testA = testA;
    }
}

xml example:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <bean id="testA" class="com.chenpt.springFrameWork.TestA" scope="singleton">
        <property name="testB" ref="testB"/>
    </bean>
 
    <bean id="testB" class="com.chenpt.springFrameWork.TestB" scope="singleton">
        <property name="testA" ref="testA"/>
    </bean>
 
</beans>

Client execution (self demonstration, no output error)

2.3 dependency handling of prototype scope (unable to be solved)

For prototype scope beans, the spring container cannot complete dependency injection because the spring container does not cache prototype scope beans, so it cannot expose a bean being created in advance.

The example code is as follows:

xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
 
    <bean id="testA" class="com.chenpt.springFrameWork.TestA" scope="prototype">
        <property name="testB" ref="testB"/>
    </bean>
 
    <bean id="testB" class="com.chenpt.springFrameWork.TestB" scope="prototype">
        <property name="testA" ref="testA"/>
    </bean>
 
</beans>

Client example

public class MainTest {
    public static void main(String[] args){
        ApplicationContext context = new FileSystemXmlApplicationContext("classpath:spring/applicationContext.xml");
 
        TestB testB = context.getBean("testB",TestB.class);
    }
}

The execution result throws an exception

For the above scope analysis

beanFactory not only has the responsibility of ioc, but also has object lifecycle management.

Scope is used to declare the restricted scene where the objects in the container should be located or the survival time of the object, that is, the container generates and assembles these objects before the objects enter their corresponding scope. After the objects are no longer restricted by the scope, the container usually destroys these objects.

How many scope types does the spring container provide?

  • Singleton: there is only one instance in the spring container, and all object references will share this instance. (Note: do not confuse with singleton mode)
  • prototype: the container generates a new object instance to the requester every time.
  • Request (limited to web applications): create a new request processor object for each http request for the current request. When the request ends, the instance life cycle ends.
  • Session (restricted to web applications): create a new UserPreference object instance for each independent session.
  • Global session (restricted to web applications): only the application is meaningful in portlet based web applications. It maps to the global session of the portlet. If this type of scope is used in an ordinary servlet based web application, the container will treat it as an ordinary session type scope.

The following mainly describes the solution of circular dependency in the second case

Step 1: beanA initializes, records its initialization status, and exposes a singleton engineering method in advance, so that other beans can reference the bean (after reading this sentence, you still have doubts. It doesn't matter. Continue reading)

Step 2: beanA has a dependency on beanB, so start initializing beanB.

Step 3: in the process of initializing beanB, it is found that beanB depends on beanA, so beanA is initialized again. At this time, it is found that beanA is already initializing, and the program finds the existing circular dependency, Then get the reference of beanA through the singleton engineering method exposed in step 1 (note that beanA only completes the constructor injection at this time, but to complete other steps), so that beanB gets the reference of beanA, completes the injection and completes the initialization, so that beanA can get the reference of beanB, and beanA completes the initialization.

When spring loads beans, first initialize the beans (call the constructor), and then fill in the properties. In the middle of these two steps, spring records the state of the bean once, that is to say, spring will record the reference to the bean that has only completed the initialization of the constructor through a variable. Understanding this is very important for the later understanding of the source code.

3 how does spring solve circular dependency?

The solution of circular dependency should be discussed in two cases

  1. Simple circular dependency (no AOP)
  2. Combined with the circular dependency of AOP

Simple circular dependency (no AOP)

First of all, we need to know that Spring creates beans according to natural sorting by default, so in the first step, Spring will create A.

At the same time, we should know that Spring has three steps in the process of creating beans

  1. Instantiation, corresponding method: createBeanInstance method in AbstractAutowireCapableBeanFactory

  2. Attribute injection, corresponding method: populateBean method of AbstractAutowireCapableBeanFactory

  3. Initialization, corresponding method: initializeBean of AbstractAutowireCapableBeanFactory

  1. Instantiation is simply understood as new creating an object
  2. Property injection: fill in properties for the new object in the instantiation
  3. Initialize, execute the method in the aware interface, initialize the method, and complete the AOP agent

We can see from the above figure that although an uninitialized A object will be injected into B in advance when creating B, the reference of the A object injected into B is always used in the process of creating A, and then A will be initialized according to this reference, so there is no problem.

Combined with the circular dependency of AOP

  1. Why inject a proxy object when injecting B?

A: when we AOP proxy a, it means that what we want to get from the container is the object after a proxy, not a itself. Therefore, when we inject a as a dependency, we should also inject its proxy object

  1. It is A object at the time of initialization, so where does Spring put the proxy object into the container?

After initialization, Spring calls the getSingleton method again. The parameters passed in this time are different. false can be understood as disabling the L3 cache. As mentioned in the previous figure, when injecting A into B, the factory in the L3 cache has been taken out, and an object obtained from the factory has been put into the L2 cache, Therefore, the time for the getSingleton method here is to obtain the A object after the proxy from the L2 cache. exposedObject == bean can be considered to be valid unless you have to replace the Bean in the normal process in the post processor in the initialization stage, such as adding A post processor:

@Component
public class MyPostProcessor implements BeanPostProcessor {
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		if (beanName.equals("a")) {
			return new A();
		}
		return bean;
	}
}

However, please don't do this coquettish operation, which will only increase your troubles!

  1. During initialization, the A object itself is initialized, and the proxy objects in the container and injected into B are proxy objects. Won't there be A problem?

A: No, this is because no matter the proxy class generated by cglib proxy or jdk dynamic proxy, it holds a reference to the target class. When calling the method of the proxy object, it will actually call the method of the target object. A completes the initialization, which is equivalent to that of the proxy object itself

  1. Why should L3 cache use factories instead of direct references? In other words, why do you need this L3 cache? Can't you expose a reference directly through L2 cache?

A: the purpose of this factory is to delay the proxy of the object generated in the instantiation stage. The proxy object will be generated in advance only when the circular dependency really occurs. Otherwise, only a factory will be created and put into the L3 cache, but the object will not be created through this factory

4 interview answers

”How does Spring solve circular dependency? “

A: Spring solves the circular dependency through the three-level cache. The first level cache is singleton objects, the second level cache is earlySingletonObjects, and the third level cache is singleton factories. When circular references occur to classes a and B, after a completes instantiation, it uses the instantiated object to create an object factory and add it to the L3 cache. If a is represented by AOP, the object obtained through this factory is the object after a is represented by AOP. If a is not represented by AOP, the object obtained by this factory is the object instantiated by A. When a performs attribute injection, it will create B, and B depends on A. therefore, when creating B, it will call getBean(a) to obtain the required dependencies. At this time, getBean(a) will be obtained from the cache. The first step is to obtain the factory in the L3 cache first; The second step is to call the getObject method of the object factory to obtain the corresponding object. After obtaining the object, inject it into B. Then B will complete its life cycle process, including initialization, post processor, etc. When B is created, B will be re injected into a, and a will complete its whole life cycle. At this point, the circular dependency ends!

Interviewer: "why use L3 cache? Can L2 cache solve circular dependency? “

A: if the L2 cache is used to solve the circular dependency, it means that all beans must complete the AOP proxy after instantiation, which is contrary to the principle of Spring design. At the beginning of the design, Spring uses the post processor AnnotationAwareAspectJAutoProxyCreator to complete the AOP proxy at the last step of the Bean life cycle, Instead of AOP proxy immediately after instantiation.