[Spring] three level cache and circular dependency in Spring

In the last article, we talked about the life cycle of springbeans and knew the process of instantiation, creation and destruction of springbeans, but we know that Spring supports one Bean to introduce other Bean objects, so the problem of interdependence is inevitable. How does Spring solve this problem? There are many ways for Spring to register beans. What circular dependencies can Spring solve and what circular dependencies can't Spring solve? This article will introduce them one by one.

1, What is circular dependency

Generally speaking, circular dependency refers to the phenomenon that N beans in Spring depend on each other to form a closed loop.

Interdependence is only the cause of closed loop. Closed loop is the result of interdependence. In special cases, self dependence is also a kind of closed loop.

2, Code demonstration of circular dependency

2.1. Introducing dependency

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.sxx</groupId>
    <artifactId>CyclicDependence</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring.version>5.1.6.RELEASE</spring.version>
        <junit.version>4.12</junit.version>
        <slf4j.version>1.7.35</slf4j.version>
        <aspectjweaver.version>1.8.9</aspectjweaver.version>
        <json.version>1.2.27</json.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aspectjweaver.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${json.version}</version>
        </dependency>
    </dependencies>
</project>

2.2. Create object

package com.sxx.cyclic.entity;

import com.alibaba.fastjson.JSON;

public class A {

    private String name;

    private B b;

    @Override
    public String toString() {
//        System. out. Println ("Bean object injected into a" + b);
        return JSON.toJSONString(this);
    }

    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public B getB() {
        return b;
    }
    
    public void setB(B b) {
        this.b = b;
    }
}
package com.sxx.cyclic.entity;

import com.alibaba.fastjson.JSON;

public class B {

    private Integer age;

    private A a;

    @Override
    public String toString() {
//        System. out. Println ("Bean object injected into B" + a);
        return JSON.toJSONString(this);
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public A getA() {
        return a;
    }

    public void setA(A a) {
        this.a = a;
    }
}

2.3. Writing xml files

<?xml version="1.0" encoding="utf-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="a" class="com.sxx.cyclic.entity.A">
        <property name="name" value="Zhang San"/>
        <!--introduce B-->
        <property name="b" ref="b"/>
    </bean>
    <bean id="b" class="com.sxx.cyclic.entity.B">
        <property name="age" value="20"/>
        <!--introduce A-->
        <property name="a" ref="a"/>
    </bean>
</beans>

2.4 testing

package com.sxx.cyclic.entity;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class CyclicTest {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new
                ClassPathXmlApplicationContext("classpath*:bean.xml");
        A a = applicationContext.getBean("a", A.class);
        System.out.println(a.toString());
    }
}

2.5 results

{"b":{"a":{"$ref":".."}, "Age": "20}," name ":" Zhang San "}

3, Circular dependency of Spring

From the above code, we can clearly find two problems:

        1. When rewriting the toString method of A and B objects, I printed out other bean objects introduced, but I commented them out during actual execution;

        2. When calling the toString method of a, attributes that should not have appeared appear in the printed a object, as shown in the following figure:

Imagine why these two situations occur?

First, we can try to release the print statement in toString to see what happens.

It can be seen that StackOverflowError appears after we release the toString print statement in A and B, which indicates that there is A circular call in our code. According to the previous code, it is easy to guess that the two beans A and B hold each other. When did this trigger? We can comment out the toString print information first, and then view the object information of A and B beans after the getBean method.

As can be seen from the figure, when we get the a object from the container through getBean after Spring is started, the A and B bean s are still interdependent and there is a closed loop. Therefore, when toString is called later, we need to rely on printing the B object, and the B object depends on Printing the a object, so StackOverflowError appears, This can also explain why the following phenomenon occurs.

{"b":{"a":{"$ref":".."}, "Age": "20}," name ":" Zhang San "}

3.1 how does Spring solve circular dependency

First release the flow chart, and then analyze it step by step from the source level

3.2. Spring creates singleton bean process

To understand the process of Spring solving circular dependencies, we first need to know the process of Spring creating singleton bean s.

3.2.1 L3 cache

To understand the creation process of singleton bean s, you must first know the concept of three-level cache. The three-level cache of Spring is at org springframework. beans. factory. support. Under defaultsingletonbeanregistry, the source code is as follows:

/**
 * L1 cache: singleton (object) pool. The objects in this pool are all initialized and can be used normally
 * It may come from level 3 or level 2
 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
        
/**
 * Level 3 cache: single instance factory pool, which is not the bean itself, but its factory. In the future, call getObject to get the real bean
 * Once obtained, delete it from here and enter level 2 (in case of closed loop) or level 1 (without closed loop)
 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
        
/**
 * Level 2 Cache: early (object) singleton pool, which is semi-finished products, but someone uses it to get out of level 3 in advance and expose the reference
 * Its attribute may be null, so it is called early object, early: semi-finished product
 * In the future, after the getBean pays the attribute, it will call addSingleton to clear level 2 and officially enter level 1
 */
private final Map<String, Object> earlySingletonObjects = new HashMap(16);

Note: the essence of level 3 cache is three map s with different functions. Strictly speaking, only singletonObjects is level 1 cache, and the other two are just Spring's auxiliary role in the process of generating beans, that is, to solve some problems in the process of generating beans.

3.2.2. Spring creates singleton bean process

We know that after Spring initialization, we need to get the corresponding Bean object through getBean(). What are the operations done in getBean?

As can be seen from the above figure, the getBean method calls the doGetBean() method, which is the actual execution content of getBean. We can see the following analysis of the relevant source code:

protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
        //Convert beanname, start with & or skip
		String beanName = this.transformedBeanName(name);
		//Try to get it first. If you can't get it and then create it, the loop dependency closed loop can get it; Take level 1-2-3
        Object sharedInstance = this.getSingleton(beanName);
        Object bean;
		//Judge whether it is a closed loop and enter when it is
        if (sharedInstance != null && args == null) {
            if (this.logger.isTraceEnabled()) {
                if (this.isSingletonCurrentlyInCreation(beanName)) {
                    this.logger.trace("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference");
                } else {
                    this.logger.trace("Returning cached instance of singleton bean '" + beanName + "'");
                }
            }
			//The following method: if it is an ordinary Bean, return sharedInstance directly
			//If it is a FactoryBean, return the instance object it created
            bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null);
        } else {
			//No prototype type bean s have been created
            if (this.isPrototypeCurrentlyInCreation(beanName)) {
				//If the current thread has created a bean of the prototype type of this beanName, throw an exception
                throw new BeanCurrentlyInCreationException(beanName);
            }

			//Check whether the BeanDefinition exists in the container (initialization does not exist)
            BeanFactory parentBeanFactory = this.getParentBeanFactory();
            if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) {
                //If the BeanDefinition does not exist in the current container, try whether it exists in the parent container
				String nameToLookup = this.originalBeanName(name);
                if (parentBeanFactory instanceof AbstractBeanFactory) {
                    return ((AbstractBeanFactory)parentBeanFactory).doGetBean(nameToLookup, requiredType, args, typeCheckOnly);
                }

                if (args != null) {
					//Returns the query result of the parent container
                    return parentBeanFactory.getBean(nameToLookup, args);
                }

                if (requiredType != null) {
                    return parentBeanFactory.getBean(nameToLookup, requiredType);
                }

                return parentBeanFactory.getBean(nameToLookup);
            }
			//typeCheckOnly is false. Put the current beanName into an alreadyCreated Set set
            if (!typeCheckOnly) {
				//Store the current bean into the set collection alreadyCreated
                this.markBeanAsCreated(beanName);
            }

            try {
				//beanDefinitionMap.get()
                RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);
                //Just check whether the RootBeanDefinition is abstract
				this.checkMergedBeanDefinition(mbd, beanName, args);
				//Initialize all beans with dependencies on dependencies first
                String[] dependsOn = mbd.getDependsOn();
                String[] var11;
                if (dependsOn != null) {
                    var11 = dependsOn;
                    int var12 = dependsOn.length;

                    for(int var13 = 0; var13 < var12; ++var13) {
                        String dep = var11[var13];
						//Check whether there is circular dependency
                        if (this.isDependent(beanName, dep)) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
                        }
						//Register dependencies
                        this.registerDependentBean(dep, beanName);

                        try {
							//Initializing dependencies
                            this.getBean(dep);
                        } catch (NoSuchBeanDefinitionException var24) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", var24);
                        }
                    }
                }
				//Create an instance of singleton
                if (mbd.isSingleton()) {
					//After the bean is instantiated, it is put into the first level cache
                    sharedInstance = this.getSingleton(beanName, () -> {
                        try {
							//Execute create bean
                            return this.createBean(beanName, mbd, args);
                        } catch (BeansException var5) {
							//Displays the deletion of a bean from the singleton cache and deletes all beans temporarily referenced by the bean
                            this.destroySingleton(beanName);
                            throw var5;
                        }
                    });
                    bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				//Create an instance of prototype
                } else if (mbd.isPrototype()) {
                    var11 = null;

                    Object prototypeInstance;
                    try {
                        this.beforePrototypeCreation(beanName);
						//Execute create bean
                        prototypeInstance = this.createBean(beanName, mbd, args);
                    } finally {
                        this.afterPrototypeCreation(beanName);
                    }

                    bean = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
				// If it is not singleton or prototype, it needs to be delegated to the corresponding implementation class
                } else {
                    String scopeName = mbd.getScope();
                    Scope scope = (Scope)this.scopes.get(scopeName);
                    if (scope == null) {
                        throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
                    }

                    try {
                        Object scopedInstance = scope.get(beanName, () -> {
                            this.beforePrototypeCreation(beanName);

                            Object var4;
                            try {
                                var4 = this.createBean(beanName, mbd, args);
                            } finally {
                                this.afterPrototypeCreation(beanName);
                            }

                            return var4;
                        });
                        bean = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                    } catch (IllegalStateException var23) {
                        throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", var23);
                    }
                }
            } catch (BeansException var26) {
                this.cleanupAfterBeanCreationFailure(beanName);
                throw var26;
            }
        }

        if (requiredType != null && !requiredType.isInstance(bean)) {
            try {
                T convertedBean = this.getTypeConverter().convertIfNecessary(bean, requiredType);
                if (convertedBean == null) {
                    throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
                } else {
                    return convertedBean;
                }
            } catch (TypeMismatchException var25) {
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", var25);
                }

                throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
            }
        } else {
            return bean;
        }
    }

The specific mainstream process is shown in the figure below:

Spring 5 Taking version 1.6 as an example, the entries of each method are as follows:

1. getBean entry:

org.springframework.beans.factory.support.AbstractBeanFactory#doGetBeanĀ 

2. Try to get the Bean's entry org. From the L3 cache according to the conditions springframework. beans. factory. support. DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)

3. The name is the same as 2, but the function is completely different. It creates org. Org through factory springframework. beans. factory. support. DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)

4. Execute the entry org. To create the Bean springframework. beans. factory. support. AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])

5. The method of actually creating Bean. Org springframework. beans. factory. support. AbstractAutowireCapableBeanFactory#doCreateBean

6. Instantiate Bean's entry org springframework. beans. factory. support. AbstractAutowireCapableBeanFactory#createBeanInstance

7. Really fill the entry of Bean attribute

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean

8. Method entry for calling Bean's post processor

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)

3.2.3. Three getsingletons

As can be seen from the above, two getSingleton methods with the same name and different functions appear when creating a singleton bean. In fact, three getSingleton methods are overloaded in DefaultSingletonBeanRegistry, and their functions are also different.

	@Nullable
    public Object getSingleton(String beanName) {
        return this.getSingleton(beanName, true);
    }

    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        //Take it from the L1 cache first
		Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
            synchronized(this.singletonObjects) {
				//Getting from L2 cache
                singletonObject = this.earlySingletonObjects.get(beanName);
                //Alloweerlyreference whether circular dependency is allowed
				if (singletonObject == null && allowEarlyReference) {
                    //Get it from the L3 cache. If it is found, end the loop here
					ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
					    //Call the early object method abstractautowirecapablebeanfactory getEarlyBeanReference
                        singletonObject = singletonFactory.getObject();
                        //The early objects returned by the L3 cache are put into the L2 cache
						this.earlySingletonObjects.put(beanName, singletonObject);
                        //Remove L3 cache
						this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
		//Return this object
        return singletonObject;
    }

    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "Bean name must not be null");
        synchronized(this.singletonObjects) {
			//Try to get from L1 cache
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
				//Judge whether it is a single instance being destroyed
                if (this.singletonsCurrentlyInDestruction) {
                    throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)");
                }

                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
                }
				//Check before creation
                this.beforeSingletonCreation(beanName);
                boolean newSingleton = false;
                boolean recordSuppressedExceptions = this.suppressedExceptions == null;
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = new LinkedHashSet();
                }

                try {
					//Call the createBean method
                    singletonObject = singletonFactory.getObject();
                    newSingleton = true;
                } catch (IllegalStateException var16) {
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        throw var16;
                    }
                } catch (BeanCreationException var17) {
                    BeanCreationException ex = var17;
                    if (recordSuppressedExceptions) {
                        Iterator var8 = this.suppressedExceptions.iterator();

                        while(var8.hasNext()) {
                            Exception suppressedException = (Exception)var8.next();
                            ex.addRelatedCause(suppressedException);
                        }
                    }

                    throw ex;
                } finally {
                    if (recordSuppressedExceptions) {
                        this.suppressedExceptions = null;
                    }

                    this.afterSingletonCreation(beanName);
                }

                if (newSingleton) {
					//After the bean is instantiated, it is put into the first level cache
                    this.addSingleton(beanName, singletonObject);
                }
            }

            return singletonObject;
        }
    }

According to the source code of getSingleton, the functions of the three methods are as follows:

The first getSingleton: call the second getSingleton and pass in beanName and true at the same time;

The second getSingleton: get the bean from the first level cache first. If there is no verification condition, and judge whether to enter the following method according to alloweerlyreference. If you enter, get the bean from the third level cache and move the third level cache into the second level cache. Otherwise, null will be returned directly;

The third getSingleton: first take the bean from the first level cache. If not, create it through the incoming singletonFactory and put it into the first level cache, and clear the second and third level cache at the same time.

3.3. Spring's process of solving circular dependency

In fact, from the previous flow chart of Spring solving circular dependency and the source code analysis of Spring creating singleton bean s, we can easily know that the flow of Spring dealing with circular dependency is as follows:

  1. The entry is getBean("a") method;
  2. Enter the doGetBean() method to start creating the A object;
  3. For the first time, go to the first level cache to query, because Spring has just started, and you can't query it for the first time;
  4. Start initializing A in doGetBean and put it into the L3 cache;
  5. After initialization, A starts to fill the attributes. When filling the attributes, it is found that B object needs to be injected, so getBean("b") is triggered;
  6. Repeat the process of 1-4, then start to fill in the attribute of B, and enter the getBean("a") method again;
  7. At this time, it is not the same as entering getBean("a") for the first time. According to the condition detection, there is A closed loop. Start trying to get A semi-finished product from the L3 cache, that is, the A object without filling attribute, and then fill the B object;
  8. After the initialization of B object is completed, move B to the first level cache;
  9. At this time, when you return to the first getBean("a"), you can get A finished B object. At this time, the filling of A object is completed
  10. So far, the A and B cycle dependency resolution is completed.

So far, we can explain the problem of $ref in the B attribute of object A printed out when calling the toString() method

4, Some problems of Spring circular dependency

4.1. Circular dependency that Spring cannot solve

From the above process, we know that Spring uses the change of level-3 cache to solve the circular dependency. If there are multiple instances of bean s, you need to instantiate A new object every time, and there is no level-3 cache at all, so it can't solve the cache dependency of multiple instances. In addition, we know that Spring starts getBean("b") to trigger the instantiation process of b only after the instantiation of A is completed and the attributes are filled. If it is A circular dependency through the construction method, the construction method needs to be called during instantiation, and there is no way to trigger the instantiation process of b. therefore, the circular dependency in this case cannot be solved.

premiseDependency injection methodCan circular dependency be resolvedreason
A. B interdependence (circular dependency)setter injection is adoptedyesL3 cache can solve circular dependency
A. B interdependence (circular dependency)All are injected by constructornoWhen instantiating A, the constructor needs to be called and B needs to be introduced. At this time, the process of filling A attribute cannot be triggered
A. B interdependence (circular dependency)A injects B by setter and B injects a by constructoryesA can trigger B's instantiation process when filling in attributes. B can get a's proxy bean in the three-level cache when instantiating, so B can initialize normally
A. B interdependence (circular dependency)A injects B in a constructor and B injects a in a setternoWhen instantiating A, the constructor needs to be called and B needs to be introduced. At this time, the process of filling A attribute cannot be triggered

4.2 why use L3 cache to solve circular dependency

We already know that Spring uses L3 cache to solve circular dependency. Why L3 cache? Can L1 or L2 cache solve circular dependency?

First, let's talk about the next level cache.

We know that some Spring beans need to be enhanced by aop. For such beans, a proxy bean should be put into the cache. The generation of proxy bean is in initializeBean (the third stage). Therefore, we deduce that if only the first level cache is used, the cache insertion should be placed after initializeBean.

If the cache is recorded during initializeBean, in case of cyclic dependency, the cyclic dependent beans need to be injected during populatebean (phase 2). At this time, there are no cyclic dependent beans in the cache, which will lead to the re creation of bean instances. This is obviously not feasible.

Can L2 cache be used to solve circular dependency?

The answer is yes, so why doesn't Spring use L2 cache? We have described in detail the different functions of Spring's three-level cache. We know that Spring's two-level cache stores semi-finished bean s without too many extensions. If it is only used to solve circular dependencies, it is entirely possible. However, the three-level cache is a Factory, in which our code and pre and post processors can be embedded before and after creation, Aop and other operations take place here. Its scalability is stronger than that of L2 cache, and it looks clearer. This is the beauty of L3 cache.

Keywords: Java Spring Cache

Added by WormTongue on Fri, 25 Feb 2022 13:51:15 +0200