Spring Interview Requirements-Circular Dependency

1. What is circular dependency

1.1 Overview

Circular dependency is the interdependence between different objects. For example, A-like constructors depend on B instances, B-like constructors depend on A instances, so Spring needs to inject B instances when initializing A instances, then B instances need to be taken from containers, then B instances will be created, but B instances need to depend on A instances when creating B instances. There is a circular dependency. (Or circular dependency between multiple objects A depends on B | B depends on C | C depends on A, etc.)

1.2 Code Example

General Class A/B

/*
 *  Two common constructors of class A and B, A depend on instance B, and B depend on instance A
 */
public class A {
	private A a;
	public A(B b) {
		this.a = a;
	}
}

public class B {
	private A a;
	public B(A a){
		this.a = a;
	}
}

Spring Profile

<?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">

	
    <!-- stay Spring Configuration in configuration file A B-->
    
	<bean id="a" class="com.shwsh.A">
        <!--Constructor Injection b-->
		<constructor-arg ref="b"></constructor-arg>
	</bean>

	<bean id="b" class="com.shwsh.B">
        <!--Constructor Injection a-->
		<constructor-arg ref="a"></constructor-arg>
	</bean>

</beans>

test

public class Main {
	public static void main(String[] args) {
        /*
         *  Initialize the container, since A and B are singleton s by default, they are created when the container starts
         */
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");  

	}
}

Run Results

Report errors

//Spring detected a circular dependency.
Is there an unresolvable circular reference

2. What kinds of circular dependencies do Spring have

Spring has three circular dependencies

1. Prototype Circular Dependency

That is, prototype pattern object A (that is, scope = prototype bean) loops on a singleton or prototype pattern B

This circular dependency Spring cannot be resolved and can only throw exceptions.

2. Singleton Construction Method Cyclic Dependency

That is, the 1.2 code sample, Spring can't solve, it can only throw exceptions

3. Single set Injection Cyclic Dependency

Spring solves this problem by exposing an early object in advance through a three-level cache (explained in more detail below).

3. How the Spring source level checks for circular dependencies

Diagram

3.1 Singleton Construction Method Injection Cyclic Dependency Detection

Through a Set collection, the beanName that is currently being created ** (incomplete creation and initialization)** is recorded.

protected void beforeSingletonCreation(String beanName) {
		if (!this.inCreationCheckExclusions.contains(beanName) && 
            /*
             *1.This condition holds true, that is, set.add() fails, indicating that the beans corresponding to the current beanName are being created, indicating that a circular dependency is currently occurring and throwing an exception is sufficient
             *2.The condition is not valid, indicating that the beanName corresponding to the current beanName is not being created and that the current beanName will be 
             *  And add to set collection.
             */
            !this.singletonsCurrentlyInCreation.add(beanName)) {
			throw new BeanCurrentlyInCreationException(beanName);
		}
	}

// singletonsCurrentlyInCreation is a Set collection
private final Set<String> singletonsCurrentlyInCreation =
      Collections.newSetFromMap(new ConcurrentHashMap<>(16));

When the beans are created and all initializations are completed, the corresponding beanName is removed from the set collection

	protected void afterSingletonCreation(String beanName) {
		if (!this.inCreationCheckExclusions.contains(beanName) && 
            //Remove the beanName created successfully from the set collection
            !this.singletonsCurrentlyInCreation.remove(beanName)) {
			throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
		}
	}

3.2 Prototype Pattern Cyclic Dependency Detection

Diagram

The detection here is similar to the 3.1 singleton construction method injection, but here is a ThreadLocal collection that stores the beanName currently being created.

    try {
        //beanName that records the prototype object being created by the current thread
        beforePrototypeCreation(beanName);
        //create object
        prototypeInstance = createBean(beanName, mbd, args);
    } finally {
        //Remove from collection being created (HashSet)
        afterPrototypeCreation(beanName);
    }

beforePrototypeCreation

	protected void beforePrototypeCreation(String beanName) {
        //Get objects inside ThreadLocal
		Object curVal = this.prototypesCurrentlyInCreation.get();
        //curVal == null, indicating that the internal value has not been initialized, just save a String
		if (curVal == null) {
			this.prototypesCurrentlyInCreation.set(beanName);
        //curVal has a value of String, at which point you need to continue adding the current beanName to construct a Set collection
		} else if (curVal instanceof String) {
			Set<String> beanNameSet = new HashSet<>(2);
			beanNameSet.add((String) curVal);
			beanNameSet.add(beanName);
			this.prototypesCurrentlyInCreation.set(beanNameSet);
        //curVal is already a Set collection, just add it
		} else {
			Set<String> beanNameSet = (Set<String>) curVal;
			beanNameSet.add(beanName);
		}
	}

afterPrototypeCreation

	protected void afterPrototypeCreation(String beanName) {
		Object curVal = this.prototypesCurrentlyInCreation.get();
		if (curVal instanceof String) {
      		//curVal is a String that directly calls the remove() method of ThreadLocal
            //Kill ThreadLocal associated with the current thread.
			this.prototypesCurrentlyInCreation.remove();
		} else if (curVal instanceof Set) {
			Set<String> beanNameSet = (Set<String>) curVal;
            //Remove the current beanName
			beanNameSet.remove(beanName);
			if (beanNameSet.isEmpty()) {
				this.prototypesCurrentlyInCreation.remove();
			}
		}
	}

Detection process for prototype circular dependency

In one part of the getBean() method execution ** (before creating a single instance and a prototype instance)**, the following code is executed to determine if a prototype circular dependency is currently generated

			/*
			 *  Decision on circular dependency of prototype patterns (Spring cannot handle, only throw exceptions)
			 *   (That is, prototype pattern object A depends on a B(B may or may not be prototype), B depends on A)
			 *     1.The current thread stores a string "A" in the Set collection in ThreadLoal to indicate that an A object is currently being created
			 *     2.The object A created at this time is an earlier object
			 *     3.Processing the dependency of A, found that A depends on objects of type B, triggering the logic of getBean(B)
			 *     4.Create early instances of B from the B construction method
			 *     5.Processing dependency on B, found dependency on A
			 *     6.Then proceed to the logic of getBean("A"), but at this point there is an A string in the set collection of ThreadLocal, indicating that `Currently A is being created, indicating that 				          	  							                   			                                   A circular dependency occurs, and an exception is thrown directly
			 * */

		     //Returns true, indicating that the beans corresponding to the current beanName are being created, indicating that a circular dependency direct throw exception has occurred
			if (isPrototypeCurrentlyInCreation(beanName)) {
				throw new BeanCurrentlyInCreationException(beanName);
			}

isPrototypeCurrentlyInCreation

	    /*
		 * prototypesCurrentlyInCreation(ThreadLocal)
		 *  Get the internal set collection or string and determine if the beanName exists. Existence indicates a circular dependency has occurred
         */
	protected boolean isPrototypeCurrentlyInCreation(String beanName) {
		//Gets the String or Set collection inside ThreadLoacl
		Object curVal = this.prototypesCurrentlyInCreation.get();
		
        /*
         * 1.The current ThreadLocal still has a String in it, so compare it with the current beanName
         * 2.The current ThreadLocal stores a Set collection, so call the contains() method
         */
        return (curVal != null &&
				(curVal.equals(beanName) || (curVal instanceof Set && ((Set<?>) curVal).contains(beanName))));
	}

3.3 Summary

The core of Spring's detection of circular dependency is to use a Set collection to store the beanName of the beans currently being created. Assuming A and B circular dependency, when creating an A instance, discover the dependency B, store the beanNameA in the Set collection, then execute the process of getBean(B), store the beanNameB in the Set collection, and then B relies on A to go to the process of getBean(A). However, if beanNameA is found already in the Set collection, a circular dependency occurs, throwing an exception directly.

4. How Spring solves the cyclic dependency of a single Set injection

Level 3 cache for 4.1 Spring

The core of Spring's solution to the cyclical dependency of single Set injection is to use a third-level cache to wrap the early objects created (incomplete subsequent injection and initialization) into an ObjectFactory and place them in the third-level cache.

	/*
	*  Level 1 cache, key -> beanName: single instance object created by value (through full lifecycle)
	*  When a single bean object is created (a complete object), it is placed in the first level cache, the next time getBean()
	*  Get it directly from the cache.
	* */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/*
	*  Level 3 caching, where the current bean instance is wrapped as an ObjectFactory object and placed in the level 3 cache when a single-instance bean has just been created (initialization has not yet been completed, and so on).
	*          
	* */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

	/*
	* Secondary Cache: Used to save objects that have been instantiated but have not been initialized
	* key -> beanName : value -> Bean Example 
	* When a single-instance bean is being created, it is placed in the third-level cache first, and then getBean() is called by other threads.
	* The bean object is retrieved from the third-level cache, and then the bean is removed from the third-level cache and placed in the second-level cache.
	* */
	private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

4.2 Source - Place in Level 3 cache when creating a single bean

//At this point, the bean is an early object that has just been created through reflection, and injection and initialization of the postprocessor have not been completed.
final Object bean = instanceWrapper.getWrappedInstance();
····

// Do you need to expose earlier: Singleton &Allow circular dependencies &Currently bean s are being created to detect circular dependencies
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
      isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
   if (logger.isDebugEnabled()) {
      //Print Log
   }
   // To avoid late circular dependency, you can add the ObjectFactory that created the instance to the factory before the bean initialization is complete
   addSingletonFactory(beanName,
         () -> getEarlyBeanReference(beanName, mbd, bean));
}

//----------------------------------------------------------------------------

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) {
			//Place in Level 3 Cache
            this.singletonFactories.put(beanName, singletonFactory);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }
}

4.3 Source-getBean() try to get it from the cache first

		/*
		 * getBean Second step: try to get objects from the cache
		 */
		Object sharedInstance = getSingleton(beanName);

getSingleton()

This resolves the circular dependency by trying to get it from the cache before creating the object. If it is a single set injection, it will get the early object from the third level cache, complete the injection, and then go through the whole creation process.

   /*
	*  @param allowEarlyReference Allow early references 
	* Final Return Value Case
	*  1.Found in Level 1 cache, returned
	*  2.Level 1 cache not found, Level 2 cache found return
	*  3.There are no first-level or second-level caches, found in the third-level cache (ObjectFactory), put the objects in the third-level cache to the second-level cache, and return 
	*/
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {

		//Get from Level 1 Cache
		Object singletonObject = this.singletonObjects.get(beanName);
		/*
		*  The following if conditions are met: (Level 1 cache does not exist and the current bean object is being created)
		*  Spring The beanName of the beans being created is added to a Set collection.
		*
		*  How many possibilities are there for a singletionObject == null condition to be true?
		*  1.Single Instance Really Not Created
		*  2.Single instance is being created and a circular dependency is currently occurring
		*
		*  What is circular dependency? A depends on B, B depends on A.
 		*  How many circular dependencies does a single instance have?
		*  1.Construction method cyclic dependency (no solution)
		*  2.set Injection Cyclic Dependency (Solution, Level 3 Cache Solution)
		*
		*  How the third-level cache solves the circular dependency caused by set injection.
		*   Example A -> B, B - A.
		*   1.Suppose Spring instantiates A first, gets the construction method of A, and then reflects back to create the early object of A.
		*     This early object was wrapped as an ObjectFactory object and placed in the third level cache
		*	2.Processing dependency data for A, check that A is dependent on B, so go to the logic of getBean(B)
		*   3.Get the construction method of B and reflect to create the early instance object, and also wrap B as ObjectFactory in the third level cache
		*   4.Processing B's dependent data, we find that B is dependent on A, so we will go to getBean(A) again to get the A object.
		*   5.So the program then goes to the current getSingleton() method
		*   6.if you go here, both judgments will be true (1.A is not in the first level cache 2.A is being created)
		* */
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			//Get from secondary cache
			singletonObject = this.earlySingletonObjects.get(beanName);
			//Second-level caches are also not viewed in third-level caches.
			if (singletonObject == null && allowEarlyReference) {
				//Locking
				synchronized (this.singletonObjects) {

					/*
					*  Spring Why do we need a Level 3 cache instead of just a Level 2 cache?
					*  AOP,By dynamic proxy.
					*  3 What is the purpose of Level 1 caching here?
					*  The third level cache holds the ObjectFactory, which holds native object references, and the getObject() method considers returning
					*  The getObject() method determines if the current early instance needs to be enhanced, if needed.
					*  Dynamic proxy enhancements must be completed ahead of time
					*  Returns the proxy object, otherwise returns the native object.
					* */
                     
					//Attempt to continue fetching from first level cache
					singletonObject = this.singletonObjects.get(beanName);
					//Level 1 cache does not exist
					if (singletonObject == null) {
						//Try again to get from the secondary cache
						singletonObject = this.earlySingletonObjects.get(beanName);
						//Neither does the secondary cache exist
						if (singletonObject == null) {
							//Get from Level 3 Cache
							ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
							//Level 3 cache is not NULL
							if (singletonFactory != null) {
								//Get the early object in the wrapped ObjectFactory
								singletonObject = singletonFactory.getObject();
								//Place early objects in a secondary cache
								this.earlySingletonObjects.put(beanName, singletonObject);
								//Remove the ObjectFactory corresponding to this beanName from the third level cache
								this.singletonFactories.remove(beanName);
							}
						}
					}
				}
			}
		}
		return singletonObject;
	}

4.4 Summary

This also shows why prototype pattern cyclic dependency and case construction method cyclic dependency cannot be resolved.

  • 1. Early objects created by prototype mode are not placed in the third-level cache, so they cannot be resolved by the above method.
  • 2. The singleton construction method circular dependency occurs before the early objects have been created, that is, the early objects can not be created at all, nor can they be solved by the above way.

Keywords: Java Spring Interview source code ioc

Added by gfoot on Thu, 30 Dec 2021 18:06:39 +0200