How does the Spring source code solve the cyclic dependency of beans

Posted by yelvington on Fri, 18 Feb 2022 20:58:07 +0100

First of all, we need to understand that spring supports circular dependency only when the scop is a single instance bean. Beans with a scope of prototype type are not supported. A new instance will be created every time it is called. Spring will first instantiate various attribute dependencies of the bean when instantiating the bean. If testa and testb are prototype types and interdependent, new TestA will appear. When new TestA, new TestB will appear first, and then new TestA will appear.

The instantiation process in which two singleton testa and testb depend on each other:

  1. The Spring container creates a singleton "testA" bean. First, create the bean according to the parameterless constructor, expose an "ObjectFactory" to return a bean that is exposed in advance, put the "testA" identifier into the "currently created bean pool", and then inject "testB" into the setter.
  2. The Spring container creates a singleton "testB" bean. First, create the bean according to the parameterless constructor, expose an "ObjectFactory" to return a bean that is exposed in advance, put the "testB" identifier into the "currently created bean pool", and then inject "testA" into the setter, At this time, because a "testA" bean under creation has been exposed in advance by exposing "ObjectFactory", inject directly to complete the creation of testB, inject into testA, and then complete the creation of testA.

Implementation method in source code:

First, let's take a look at the three most important map s in the process of creating beans

The following three maps are from DefaultSingletonBeanRegistry
Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

  • singletonObjects: used to save the relationship between BeanName and bean instance creation. bean name > bean instance.
  • singletonFactories: used to save the relationship between BeanName and the factory that created the bean. bean name > ObjectFactory.
  • earlySingletonObjects: it is also the relationship between saving BeanName and creating bean instances. The difference from singletonObjects is that when a singleton bean is placed here, it can be obtained through the getBean method when the bean is still in the process of creation. Its purpose is to detect circular references.

Summary: the latter two maps are actually used to assist the first Map to cache Bean instances. After completion, the data will be cleared in the latter two maps.

Test code:

// 1. Introduce dependency. The springboot project only needs this dependency to test
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
// 2. Two test classes
@Component
public class TestA {
    @Autowired
    private TestB testB;
}
@Component
public class TestB {
    @Autowired
    private TestA testA;
}
Note: all the following methods are only partial interceptions of the source code. I put the important logic here. When reading, you can open several classes mentioned in the article in the IDE in advance, and mark breakpoints at the corresponding methods for direct debugging, so that the instantiation process of bean s can be seen at a glance.
1. getBean method in abstractbeanfactory class
public <T> T getBean(String name, @Nullable Class<T> requiredType, @Nullable Object... args)
		throws BeansException {
	return doGetBean(name, requiredType, args, false);
}
2. doGetBean method in abstractbeanfactory class
// 
// 2.1 get the instance Bean from the cache. It must not be available for the first time. It is null
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
	beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}else{
	// Create bean instance.
	if (mbd.isSingleton()) {
		// 2.2 get instances in cache
		sharedInstance = getSingleton(beanName, () -> {
			try {
				// 2.3 call the method of creating Bean instance
				return createBean(beanName, mbd, args);
			}	
		});
		beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
	}
}
3. The getSingleton method in defaultsingletonbeanregistry class is called in 3.1 here
// 3.1
@Override
@Nullable
public Object getSingleton(String beanName) {
	return getSingleton(beanName, true);
}
// 3.2
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
	// Quick check for existing instance without full singleton lock
	Object singletonObject = this.singletonObjects.get(beanName);
	if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
		singletonObject = this.earlySingletonObjects.get(beanName);
		if (singletonObject == null && allowEarlyReference) {
			synchronized (this.singletonObjects) {
				// Consistent creation of early reference within full singleton lock
				singletonObject = this.singletonObjects.get(beanName);
				if (singletonObject == null) {
					singletonObject = this.earlySingletonObjects.get(beanName);
					if (singletonObject == null) {
						ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
						if (singletonFactory != null) {
							singletonObject = singletonFactory.getObject();
							this.earlySingletonObjects.put(beanName, singletonObject);
							this.singletonFactories.remove(beanName);
						}
					}
				}
			}
		}
	}
	return singletonObject;
}

3.1 and 3.2 will be obtained repeatedly later. For the first time, because issingletoncurrentyincreation (beanname) returns false, it returns null

4. getSingleton method in defaultsingletonbeanregistry class to obtain ObjectFactory. 2.2 is called here
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
	synchronized (this.singletonObjects) {
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null) {
			beforeSingletonCreation(beanName);
			boolean newSingleton = false;
			try {
				// 4.0.1
				singletonObject = singletonFactory.getObject();
				newSingleton = true;
			}
			finally {
				afterSingletonCreation(beanName);
			}
			if (newSingleton) {
				// 4.0.2
				addSingleton(beanName, singletonObject);
			}
		}
		return singletonObject;
	}
}

protected void addSingleton(String beanName, Object singletonObject) {
	synchronized (this.singletonObjects) {
		this.singletonObjects.put(beanName, singletonObject);
		this.singletonFactories.remove(beanName);
		this.earlySingletonObjects.remove(beanName);
		this.registeredSingletons.add(beanName);
	}
}

This aspect focuses on three methods: before singleton creation, after singleton creation and addSingleton

4.1 beforeSingletonCreation method and afterSingletonCreation method in defaultsingletonbeanregistry
protected void beforeSingletonCreation(String beanName) {
	if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
		throw new BeanCurrentlyInCreationException(beanName);
	}
}
protected void afterSingletonCreation(String beanName) {
	if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
		throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
	}
}

Key points: the purpose of these two methods is to add and delete the currently created beans for the singletonsCurrentlyInCreation set and pave the way for subsequent processing. The addSingleton method is mainly used to maintain the Bean cache map.

4.2 now go back to the method of 4.0.1 in step 4, singletonobject = singletonFactory getObject(); This line of code is where the instance object is really obtained. How does singletonFactory get it? Go back to step 2.3 in step 2. singletonFactory of ObjectFactory type is returned through the createBean method. Let's see how the createBean creates a Bean:
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {		
	try {
		Object beanInstance = doCreateBean(beanName, mbdToUse, args);
		return beanInstance;
	}	
}
// Anyone who has read some spring source code should understand that what spring really does starts with doXXX, and here is no exception
// I believe everyone has understood that the real creation of beans is realized by the doCreateBean method. Let's continue to analyze this method
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
	BeanWrapper instanceWrapper = null;
	if (instanceWrapper == null) {
		instanceWrapper = createBeanInstance(beanName, mbd, args);
	}
	Object bean = instanceWrapper.getWrappedInstance();
	boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences
	 && isSingletonCurrentlyInCreation(beanName));
	if (earlySingletonExposure) {
		addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
	}
	Object exposedObject = bean;
	try {
		// 4.3 fill in the attributes of the bean, and the dependent bean is initialized here
		populateBean(beanName, mbd, instanceWrapper);
		exposedObject = initializeBean(beanName, exposedObject, mbd);
	}
	if (earlySingletonExposure) {
		// 4.4 get the instance in the cache again. Note that you can get it from two caches: the first is earlySingletonObjects map and the second is singletonFactories map
		Object earlySingletonReference = getSingleton(beanName, false);
		if (earlySingletonReference != null) {
			if (exposedObject == bean) {
				exposedObject = earlySingletonReference;
			}
		}
	}
	return exposedObject;
}
// Issingletoncurrentyincreation method
public boolean isSingletonCurrentlyInCreation(String beanName) {
	return this.singletonsCurrentlyInCreation.contains(beanName);
}
// addSingletonFactory
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
	Assert.notNull(singletonFactory, "Singleton factory must not be null");
	synchronized (this.singletonObjects) {
	if (!this.singletonObjects.containsKey(beanName)) {
		this.singletonFactories.put(beanName, singletonFactory);
		this.earlySingletonObjects.remove(beanName);
		this.registeredSingletons.add(beanName);
	}
}

Sorting of important processes:
1.doCreateBean is mainly divided into two parts. The first part obtains the instance of BeanFactory through instanceWrapper, which is internally implemented by reflection. We won't do more analysis here. The variable earlySingletonExposure is obtained from three parts. The first two are easy to understand. In the third part, there is the collection singletons currentlyincorporation that we paved the way in 4.1. Since it has been set in 4.1, earlySingletonExposure must be true. Therefore, execute addsingletonfactor to assign a value to singletonFactories map, and complete the mapping of beanname - > objectfactory
2. In the populatebean method, the injection of Bean dependent attributes will be completed. Therefore, when the code goes to 4.3, the creation of testA stops. It will return to the first step to obtain testB, then create testB, and finally go to 4.3 again to complete the mapping of testA and testB ObjectFactory, that is, put them into the singletonFactories map cache.
3. When creating testB, you will initialize the dependent testA of testB again when you go to 4.3. At this time, you will go to the first step to obtain it again. When you go to 2.1 again, because the ObjectFactory of testA has value, you can obtain the singletonObject of testA through it. At this time, you will put the instance of testA into earlySingletonObjects, However, the testA instance at this time is incomplete and the initialization of the attribute testB dependency has not been completed. Finally, return the singletonObject reference of testA, complete the initialization of testB's dependence on testA, and then go to 4.4 to obtain the cache of testB, which is still not available here. Then return to 4.0.2, add testB to the singletonObjects map cache, and remove the cache of testB in singletonFactories. Here, testB actually has no value in earlySingletonObjects, Of course, some will also be removed, because the value has been obtained in singletonObjects, so the other two auxiliary maps do not need to retain data.
4. The above has completed the initialization of testB and put it into the singletonObjects cache. Continue to go back to 4.3. Continue to complete the creation of testA. When you go to 4.4, continue to get testA from the cache. Because testA has been put into the earlySingletonObjects map before, you can directly get the instance of testA in 4.4.
5. Go on and come to 4.0.2 again, but this time it is for testA. The addSingleton method will put the instance of testA into the singletonObjects map cache, remove the testA cached by singletonFactories and earlySingletonObjects map, and complete the instantiation of testA and testB.

Topics: Java Spring Spring Boot