Spring framework's solution to circular dependency -- three-level cache mechanism

------------Restore content start------------

Recently, I reviewed the basic knowledge related to the Spring framework. The Spring framework often appears in the interview questions to solve the problem of circular dependency. I also read many online blogs. The following is my learning record. If there is anything unreasonable, please correct it!

Problem introduction:

What is circular dependency? Circular dependency means that during the object creation process, the object's properties, constructor parameters, or method parameters depend on other objects: for example, the input parameters of the setter method of object A (which can also be constructor input parameters or interface method input parameters) are object B, while object B also has object A as the input parameters of the setter method. The two refer to each other to form A closed-loop reference, This example is the simplest example of circular dependency;

The code is shown in the figure below:

@NoArgsConstructor
public class StudentA {

    private StudentB  studentB;


    public StudentA(StudentB studentB){
        this.studentB = studentB;

    }

    public void setStudentB(StudentB studentB) {
        this.studentB = studentB;
    }
}
@NoArgsConstructor
public class StudentB {

    private StudentA studentA;

    public StudentB(StudentA studentA){
        this.studentA = studentA;
    }

    public void setStudentA(StudentA studentA) {
        this.studentA = studentA;
    }
}

 

What is Spring's three-level cache mechanism: in Spring, there are three-level cache mechanisms: first level cache (singleton objects), second level cache (early singleton objects) and third level cache (singleton factories);

The source code is as follows:

  private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);//L1 cache: mainly stores the instantiated and assigned attributes bean object
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);//L3 cache: mainly used to store beanName and beanFactory(bean Correspondence between instances (factories)
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);//L2 cache: it is mainly used to store objects that have been instantiated but whose object attributes have not been assigned bean object

//getBean Core source code of operation
@Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//First pass beanName Go to L1 cache lookup
        Object singletonObject = this.singletonObjects.get(beanName);
        if(singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
//If there is no instance in the L1 cache, go further to the L2 cache to find it. If it is found at this time, it will be returned directly
            singletonObject = this.earlySingletonObjects.get(beanName);
//If the L2 cache is not found and the instance object is not found, check allowEarlyReference Is it true,This flag is the key for L2 cache to solve circular dependency. If this flag is set to false,
//that spring Solving the circular dependency problem will fail (the default is true);This flag is called whether circular reference is allowed, that is, what is stored in the L2 cache is only the early reference of the object instance, and the attribute of the object has not been assigned
            if(singletonObject == null && allowEarlyReference) {
                Map var4 = this.singletonObjects;
                synchronized(this.singletonObjects) {

                    singletonObject = this.singletonObjects.get(beanName);
                    if(singletonObject == null) {
//In this case, the L2 cache actually finds the reference of the object instance
                        singletonObject = this.earlySingletonObjects.get(beanName);
                        if(singletonObject == null) {
//Repeat the L1 and L2 cache lookup process. If there is still no, it will be created through the L3 cache beanName Corresponding singletonFactory(Single case (factory)
                            ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                            if(singletonFactory != null) {
//Create one from the singleton factory bean Examples of
                                singletonObject = singletonFactory.getObject();
//Put the instance into L2 cache (Note: This is only done at this time) bean Instantiation of, property not assigned)
                                this.earlySingletonObjects.put(beanName, singletonObject);
//Removes the singleton factory of the object from the L3 cache
                                this.singletonFactories.remove(beanName);
                            }
                        }
                    }
                }
            }
        }

        return singletonObject;
    }

I believe many people think that L2 cache can solve circular dependency after seeing the above source code explanation. Why do you need L3 cache? I thought so at first, and then I saw an article explaining the significance of L3 cache;

It is AOP aspect oriented programming of Spring. We know that the principle of aspect oriented programming is dynamic proxy. Let's take a look at the source code of getEarlyBeanReference method

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
        implements AutowireCapableBeanFactory {
        
    protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
        Object exposedObject = bean;
        // 1.If bean Not empty && mbd Not synthetic && existence InstantiationAwareBeanPostProcessors
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            for (BeanPostProcessor bp : getBeanPostProcessors()) {
                // 2.Apply all SmartInstantiationAwareBeanPostProcessor,call getEarlyBeanReference method
                if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                    SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                    // 3.allow SmartInstantiationAwareBeanPostProcessor Return specified bean Early references to
                    exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
                }
            }
        }
        // 4.Return to be bean Reference the exposed object, if not SmartInstantiationAwareBeanPostProcessor If it is modified, the returned value is the value of the input parameter bean Object itself
        return exposedObject;
    }
}

With AOP on,

Then call the getEarlyBeanReference method of AbstractAutoProxyCreator of the parent class of AnnotationAwareAspectJAutoProxyCreator. The corresponding source code is as follows:
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
        implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
        
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        this.earlyProxyReferences.put(cacheKey, bean);
        // If you need a proxy, return a proxy object. If you don't need a proxy, directly return the currently passed in object bean object
        return wrapIfNecessary(bean, beanName, cacheKey);
    }
}

above

wrapIfNecessary is the core method for Spring to implement Bean proxy

  • wrapIfNecessary will be called in two places: getEarlyBeanReference and postProcessAfterInitialization
  • Call getAdvicesAndAdvisorsForBean() inside the wrapIfNecessary method to return all advice \ advisor \ interceptors matching the current Bean, which is used to judge whether this class needs to create an agent.
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
        implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
        
    /**
     * Spring The core method of implementing Bean proxy. wrapIfNecessary will be called in two places. One is getEarlyBeanReference,
     * The other is postProcessAfterInitialization
     */
    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        //It has been processed
        // 1.Judge current bean Is it targetSourcedBeans It exists in the cache (it has been processed). If it exists, it will directly return the current value bean
        if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        }
        //Need not be woven into logic
        // 2.stay advisedBeans Exists in the cache, and value by false,Then it means no processing is required
        if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        }
        //Is it basic bean Is it necessary to skip
        // 3.bean The class is aop Infrastructure || bean If it should be skipped, it is marked as no processing and returned
        if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }

        // Create proxy if we have advice.
        // Return matching current Bean All of Advice\Advisor\Interceptor
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
        // 5.If there are enhancers, create a proxy
        if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            //establish Bean Corresponding agent, SingletonTargetSource Information used to encapsulate implementation classes
            // 5.1 Create proxy object: this way SingletonTargetSource of target Properties store our original bean Instance (that is, the proxied object),
            // It is used to execute our real method through reflection after the execution of the last added logic( method.invoke(bean, args))
            Object proxy = createProxy(
                    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            // 5.2 When the agent is created, the cacheKey -> Proxy class class Put in cache
            this.proxyTypes.put(cacheKey, proxy.getClass());
            // Return proxy object
            return proxy;
        }
        //Should Bean There is no need for proxy, and there is no need to generate again next time
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }
}Back to the above example, we are right A Carried out AOP Agent, then at this time getEarlyBeanReference An object after proxy will be returned instead of the object created during instantiation, which means B Injected in A Will be a proxy object instead of A The object created during the instantiation phase of.

What is the point of L3 caching?
Take A and B on our as an example, where A is represented by AOP. First, we analyze the creation process of A and B when the three-level cache is used

 

 

 

Assuming that it is possible to directly use L2 cache instead of L3 cache, what is the significance of L3 cache and L2 cache?

 

 

 

 

The only difference between the above two processes is that the time to create an agent for A object is different. When the L3 cache is used, the time to create an agent for A is when A needs to be injected into B. if the L3 cache is not used, the agent for A needs to be created immediately after A is instantiated and then put into the L2 cache. For the whole creation process of A and B, the time consumed is The same (so the common saying that L3 cache improves efficiency is wrong)

In this case, the difference is where to create the proxy. If you do not use L3 cache, using L2 cache violates Spring's design of combining AOP and Bean life cycle! The life cycle of Spring combining AOP and Bean itself is completed through the post processor AbstractAutoProxyCreator, which is in the post-processing postProcessAfterInitialization method Complete the AOP proxy for the initialized Bean. If there is a circular dependency, there is no way. You can only create the proxy for the Bean first, but if there is no circular dependency, the design is to let the Bean complete the proxy at the last step of the life cycle, rather than immediately complete the proxy after instantiation.

 

Finally, the explanation on the role of L3 cache refers to the explanation of the blogger. The original address is: https://www.jianshu.com/p/abda18eaa848

 

Posted by jesirose on Wed, 01 Dec 2021 10:36:21 -0800