directory

Circular dependencies

The BeanFactory is the bean factory that manages our singleton beans, so there must bea cache to store these singleton beans, which in Spring is a map-structured cache with a beanName key and a bean value. When a bean is fetched, it is first fetched from the cache. If not, the instantiation and initialization of the object are triggered, and the object is put into the cache after completion, thus implementing a simple bean factory.

Since Spring provides dependency injection, which automatically infuses one object into the properties of another object, this can cause A problem of A cyclic dependency (as opposed to A dependsOn) : A object is created on B object, and B object is created on A object.

As you can see, since A depends on B, the creation of A triggers the creation of B, which in turn depends on A, triggers the retrieval of A, but A is being created and not in the cache, causing problems.

Multistage cache

Level 1 cache

This raises the problem of circular dependencies, so how can we solve it? In fact, it is very simple, just rely on level 1 cache. For the level 1 cache, we do not store the object in the cache after initialization, but after the object is instantiated. Since the bean in the cache has not been initialized at this time, it can be called the early object, that is, the object A has been exposed in advance. In this way, when B obtains A during the creation process, it can obtain A object from the cache. Finally, after the initialization of A object, it can update the level cache. In this way, the problem of loop dependence is solved.

But this leads to another problem: early and complete object exists in the level of the cache, if at this time to the other threads concurrent access to the bean, may from level 1 cache access to incomplete bean, it obviously doesn’t work, so we had to can only be obtained from level 1 cache plus a mutex object, in order to avoid this problem.

Another problem with mutex is that normal requests to fetch beans after the container is refreshed need to compete for locks, which can lead to poor performance using Spring in high-concurrency scenarios.

The second level cache

Since only relying on level 1 cache to solve the circular dependency needs to rely on locking to ensure the safe development of objects, and locking will bring performance problems, so how to optimize? The answer is to introduce another cache. This cache is also a Map structure with a key for beanName and a value for bean, and this cache can be called a level 2 cache. Early we will object to the second level cache, the cache is used to store A complete object (object initialization is completed), so that in the next create B in the process of obtaining A, from the first level cache, if there is no access to the cache, is obtained from the second level cache, though the object from the second level cache is early object, However, from the point of view of object memory relationship, objects in level 2 cache and objects in level 2 cache (Pointers) point to the same object, the difference is that the objects are in different stages, so there is no problem.

Since the logic to get the bean is to get it from the level 1 cache first and then from the level 2 cache if not, it is possible for other threads to get incomplete objects, so mutex is still needed. However, the locking logic here can sink down to the level 2 cache because early objects are stored in the level 2 cache, and fetching objects from the level 1 cache is unlocked. In this way, when the container initialization is complete, a normal getBean request can fetch objects directly from the level 1 cache without competing for the lock.

When cyclic dependencies meet AOP

It seems like second-level caching has solved the problem of loop dependency, and it looks pretty simple, but don’t forget another feature Spring provides: AOP. Spring supports the creation of proxy classes for objects in CGLIB and JDK dynamic proxies to provide AOP support. As mentioned in the previous article summarizing the bean life cycle, proxy objects are created (usually) after bean initialization (via the BeanPostProcessor post-processor). In normal thinking, a proxy object should be created on the basis of the original object, but when cyclic dependencies are involved with AOP, this is not so easy.

Again, in the previous scenario where A and B are interdependent, what if A needs to be represented? Since the early objects in the secondary cache are the original objects, and the proxy objects are created after the initialization of A, the A object referenced in the B object is not A proxy object, and there is A problem.

To solve this problem, it is easy to create the proxy object in advance. That is, if there are no cyclic dependencies, the proxy object is created after initialization, and if there are cyclic dependencies, the proxy object is created ahead of time. So how do you tell if a cyclic dependency has occurred? When A is obtained during the creation of B, it is found that A exists in the second-level cache, which indicates that cyclic dependence occurs. At this point, A proxy object is created for A, overwritten into the second-level cache, and copied to the corresponding properties of B, thus solving the problem. Of course, after the final initialization of A, it must be the proxy object in the level 1 cache.

If A and B depend on each other, there is another object C that also depends on A: A depends on B, B depends on A, A depends on C, C depends on A. When creating A proxy object for A, be careful not to create it twice.

After the object is instantiated, the beanName can be stored in a Set structure to indicate that the corresponding bean is being created. When other object creation processes depend on an object, the beanName can be determined whether it is in the Set or not. If it is, it indicates that a cyclic dependency has occurred.

Three levels of cache

While relying solely on the second-level cache solves the problems of circular dependencies and AOP, the solution seems to be that the logic of maintaining proxy objects and the logic of getBeans are too coupled, and Spring does not take this approach and instead introduces another third-level cache. The key of the level 3 cache is still beanName, but value is a function (ObjectFactory#getBean method) in which the logic to get the earlier object is performed: the getEarlyBeanReference method. In getEarlyBeanReference approach, Spring will call all SmartInstantiationAwareBeanPostProcessor getEarlyBeanReference method, This method allows you to modify the properties of an earlier object or replace an earlier object. This is another extension point Spring leaves for developers, and although we rarely use it, proxy objects are created through this post-processor when loop dependencies encounter AOP.

Spring level cache source code implementation

So how does tertiary caching work in Spring? We came to DefaultSingletonBeanRegistry:

Private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); Private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); Private final Map<String, ObjectFactory<? >> singletonFactories = new HashMap<>(16);Copy the code

In fact, the so-called three-level cache is three maps in the source code. The first-level cache is used to store the final singleton object, the second-level cache is used to store the early object, and the third-level cache is used to store the function interface.

In the ten several method to be called by the container to refresh, finishBeanFactoryInitialization method is mainly used to instantiate the singleton beans, in the logic will freeze BeanDefinition metadata, The beanFactory preInstantiateSingletons method is then called to loop through all managed Beannames and create the beans in turn. Here we will focus on the logic for creating the beans. This is AbstractBeanFactory’s doGetBean method (which is a long one and only partially pasted here) :

protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType, @Nullable final Object[] args, Boolean typeCheckOnly) throws BeansException {// Resolves FactoryBean's name(&) and alias final String beanName = transformedBeanName(name); Object bean; Object sharedInstance = getSingleton(beanName); if (sharedInstance ! = null &&args == null) {// This contains the logic for handling factoryBeans, which can be obtained by &+beanName, // The real bean of a FactoryBean has its own cache, factoryBeanObjectCache. Bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else {/ / can only solve the singleton circular dependencies if (isPrototypeCurrentlyInCreation (beanName)) {throw new BeanCurrentlyInCreationException(beanName); } // If there is a parent factory and there is no BeanDefinition in the current factory, BeanFactory parentBeanFactory = getParentBeanFactory(); if (parentBeanFactory ! = null && ! ContainsBeanDefinition (beanName) {// The bean's original name String nameToLookup = originalBeanName(name); if (parentBeanFactory instanceof AbstractBeanFactory) { return ((AbstractBeanFactory) parentBeanFactory).doGetBean( nameToLookup, requiredType, args, typeCheckOnly); } else if (args ! = null) { return (T) parentBeanFactory.getBean(nameToLookup, args); } else { return parentBeanFactory.getBean(nameToLookup, requiredType); String[] dependsOn = mbd.getDependson (); if (dependsOn ! = null) { for (String dep : dependsOn) { if (isDependent(beanName, Dep)) {/ / cycle depends on throw new exception BeanCreationException (MBD) getResourceDescription (), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); } registerDependentBean(dep, beanName); try { getBean(dep); } catch (NoSuchBeanDefinitionException ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", ex); }} // create a singleton bean if (mbd.isSingleton()) {// pass beanName and a singletonFactory of type ObjectFactory into the getSingleton method //ObjectFactory is a function, The final logic to create the bean is done by calling the getObject method of the ObjectFactory sharedInstance = getSingleton(beanName, () -> {try {return createBean(beanName, MBD, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; }}); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } } catch (BeansException ex) { cleanupAfterBeanCreationFailure(beanName); throw ex; } } return (T) bean; }Copy the code

For singletons, the call to the doGetBean method calls the getSingleton method, which takes in beanName and an ObjectFactory function, The getSingleton method creates the bean by calling the ObjectFactory’s getBean method:

public Object getSingleton(String beanName, ObjectFactory<? > singletonFactory) { Assert.notNull(beanName, "Bean name must not be null"); Synchronized (this.singletonObjects) {// Synchronize (this.singletonObjects); Otherwise enter to create the logical Object singletonObject = this. SingletonObjects. Get (beanName); If (singletonObject = = null) {/ / to be created before creating beanName in the singletonsCurrentlyInCreation, this is a Map structure, BeforeSingletonCreation (beanName) is used to indicate that the singleton is being created. boolean newSingleton = false; boolean recordSuppressedExceptions = (this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = new LinkedHashSet<>(); } try {// call singletonFactory getObject, The function in the outer doGetBean method into / / the actual call singletonObject = singletonFactory createBean method. The getObject (); newSingleton = true; } finally { if (recordSuppressedExceptions) { this.suppressedExceptions = null; } / / beanName from is creating singleton collection of bean singletonsCurrentlyInCreation removed afterSingletonCreation (beanName); } if (newSingleton) {// Put the created singleton bean into the level 1 cache, AddSingleton (beanName, singletonObject) to registeredSingletons; } } return singletonObject; }}Copy the code

The mainline logic of the getSingleton method is simple: create a singleton bean from the incoming function interface, store it in the level 1 cache, and clean up the bean’s corresponding data in the level 2 cache and level 3 cache. CreateBean (doCreateBean); doCreateBean (doCreateBean); Before doCreateBean call call nstantiationAwareBeanPostProcessor postProcessBeforeInstantiation interceptor bean instantiation, rear if the processor is returned to the bean, It will not go to the doCreateBean method, but we will skip to the doCreateBean method without worrying about this logic:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper == null) {// Create bean, that is, instantiate, InstanceWrapper = createBeanInstance(beanName, MBD, args); } / / eanWrapper gets to the Object final Object bean. = instanceWrapper getWrappedInstance (); Class<? > beanType = instanceWrapper.getWrappedClass(); if (beanType ! = NullBean.class) { mbd.resolvedTargetType = beanType; } synchronized (mbd.postProcessingLock) { if (! MBD. PostProcessed) {try {/ / rear MergedBeanDefinitionPostProcessor processor processing applyMergedBeanDefinitionPostProcessors (MBD, beanType, beanName); } catch (Throwable ex) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Post-processing of merged bean definition failed", ex); } mbd.postProcessed = true; }} // Determine if this object supports early exposure, Core conditions would require a singleton Boolean earlySingletonExposure = (MBD) isSingleton () && enclosing allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); If (earlySingletonExposure) {// Wrap our early object as a singletonFactory object that provides a getObject method that internally calls the getEarlyBeanReference method // The early objects are stored in the level 3 cache, which holds an interface implementation (the ObjectFactory interface), AddSingletonFactory (beanName, () -> getEarlyBeanReference(beanName, MBD, bean)); } // Initialize the bean instance. Object exposedObject = bean; PopulateBean (beanName, MBD, instanceWrapper); // Instantiate: Call three Aware, post-processor beforeInitialization (including implementation of @postconstruct), afterPropertiesSet, init-method, post-processor afterInitialization, and so on ExposeObject exposedObject = initializeBean(beanName, exposedObject, MBD); } catch (Throwable ex) { if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { throw (BeanCreationException) ex; } else { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); }} if (earlySingletonExposure) {// If (earlySingletonExposure) {// If (earlySingletonExposure) { The interface in level 3 cache removes Object earlySingletonReference = getSingleton(beanName, false) when the singleton is added to level 1 cache. if (earlySingletonReference ! If (exposedObject == bean) {if (exposedObject == bean) { // If the exposeObject has not changed after the initializeBean method, then it has not been modified by the post-processor, So replace exposeObject with earlySingletonReference exposedObject = earlySingletonReference; } } } return exposedObject; }Copy the code

The logic of this method is to instantiate the object first. If the object supports exposing the earlier object, then the earlier object is passed as an input parameter to the getEarlyBeanReference method, which is then wrapped as an ObjectFactory and stored in the tertiary cache. The populateBean method is then called to populate the object’s properties. In the process of populating an object property, it is possible to find A cyclic dependency. For example, if A is being created and A dependency B is found in the populateBean method, then getBean(B) is called and B follows the same process as A. When B also goes to populateBean to populate the property, If A dependency is found, getBean(A) is called again. So when populateBean creates A, it gets the bean from the level 3 cache. If A needs to be proxied, A proxy object is created, so that the A property in B is the proxy object, and the object obtained by the level 3 cache is stored in the level 2 cache.

After B completes the processing, we return to the logic of A. Assuming that populateBean(A) has completed the logic, then we enter the initializeBean method to initialize A. Note that the initializeBean method is the original object of A and the proxy object is stored in the second-level cache. Since the initializeBean method calls the before-afterInitialization method of the post-initialization handler, which may change the object, in the following logic, if the proxy object of A is obtained from the secondary cache, it will determine whether the original object has changed after the post-initialization. If it is the same object, the original object is overwritten by a proxy object from the level 2 cache, so the doCreateBean method returns the proxy object, which is eventually stored in the level 1 cache. The process is as follows: Create instance object A -> set attribute B of A -> Create instance object B -> set attribute A of B -> Create proxy object A and store it in level 2 cache -> initialize object A -> remove proxy object A from level 2 cache to overwrite object A -> store proxy object A in Level 1 cache

What we need to focus on is the logic of calling getBean(A) in the populateBean logic of B. GetBean-> doGetBean->getSingleton

protected Object getSingleton(String beanName, Boolean allowEarlyReference) {/ / from the first level cache access Object singletonObject = this. SingletonObjects. Get (beanName); If (singletonObject = = null && isSingletonCurrentlyInCreation (beanName)) {/ / if from level 1 cache didn't get to, and to obtain objects are created, Synchronized (this.singletonObjects) { Here use synchronous control lock / / early object was obtained from the second level cache singletonObject = this. EarlySingletonObjects. Get (beanName); If (singletonObject == null && allowEarlyReference) {// If (singletonObject == null && allowEarlyReference) { And allowEarlyReference is true (true is passed here) // Then try to get ObjectFactory< from level 3 cache? > singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory ! = null) {// Get the object from level 3 cache, note that level 3 cache stores ObjectFactory // call getObject, The getEarlyBeanReference method is called by the getEarlyBeanReference method. So here will call getEarlyBeanReference method singletonObject = singletonFactory. GetObject (); / / save object early in the second level cache enclosing earlySingletonObjects. Put (beanName singletonObject); / / interface of cache useless, directly to remove this. SingletonFactories. Remove (beanName); } } } } return singletonObject; }Copy the code

In the logic of the getSingleton method, the object is first fetched from level 1 cache. If the level 1 cache is not found, then the bean is being created from level 2 cache. If the level 2 cache is not found, then it is fetched from level 3 cache, which holds the ObjectFactory implementation. Its getBean method is eventually called to get the bean, which is then stored in the level-two cache, while the level-three cache is cleared. An allowEarlyReference parameter is also provided to control whether it can be retrieved from the tertiary cache.

For circular dependencies, getBean(A)-> Store Creating cache -> Store level 3 cache ->populateBean(A)->getBean(B)->populateBean(B)->getBean(A)->getSingleton(A), When getSingleton(A) is called during populateBean(B), it is obvious that both the level 1 cache and the level 2 cache are empty, but the level 3 cache is not empty, so the bean is fetched from the level 3 cache. The level 3 cache creation logic is as follows:

		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}
Copy the code

So we go directly to the getEarlyBeanReference method to get the early object:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; / / if the container has a rear InstantiationAwareBeanPostProcessors processor if (! mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { for (BeanPostProcessor bp : getBeanPostProcessors()) { if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { / / find SmartInstantiationAwareBeanPostProcessor post processor SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; / / call SmartInstantiationAwareBeanPostProcessor exposedObject = getEarlyBeanReference method ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }Copy the code

The logic of the getEarlyBeanReference approach is very simple, but very important, this method is mainly calls to rear SmartInstantiationAwareBeanPostProcessor processor, And circular dependencies of AOP is implemented through the SmartInstantiationAwareBeanPostProcessor getEarlyBeanReference method, Is related to a concrete class AnnotationAwareAspectJAutoProxyCreator, this for AOP principle analysis of the article in detail.

conclusion

Multi-level caching is not only to solve the problems of loop dependency and AOP, but also to consider the separation of logic, extensibility of structure and efficiency under the premise of ensuring data security. In order to prevent getSingleton method from returning incomplete beans, synchronized locking is used in this method. However, in order not to affect the normal acquisition of beans after container refresh, the acquisition of objects from level 1 cache is not locked, and in fact, there is no need to lock. Because there are either no objects in level 1 cache, or there are complete objects.

Typically, a container refresh triggers the instantiation and initialization of a singleton as follows:

Cyclic dependency occurs in the object attribute assignment phase, also known as populateBean. After the object is instantiated, it is wrapped into a function interface, ObjectFactory, and stored in the tertiary cache. Triggered by getEarlyBeanReference method SmartInstantiationAwareBeanPostProcessor post processor execution, if the circular dependencies in AOP, then here for processing.

Here each singleton object instantiated in three-level cache, even in the absence of circular dependencies, this is to prepare for the possibility of circular dependencies, if the final circular dependencies does not occur, then the object instantiated, normal initialization, then deposited in the cache, and when deposited in the first-level cache can clear the cache of secondary and tertiary.