We all know that Spring uses level 3 caching to solve loop dependencies, but is level 3 caching really necessary to solve loop dependencies? Is it ok to use only two levels of caching? This article provides an introduction to how Spring uses three-level caching to solve circular dependencies, and verifies whether two-level caching can solve circular dependencies.

Circular dependencies

Since we want to solve circular dependencies, we need to know what circular dependencies are. As shown below:

From the above figure, we can see that:

  • A is dependent on B
  • B depends on C
  • C depends on A
public class A {
    private B b;
}

public class B {
    private C c;
}

public class C {
    private A a;
}
Copy the code

This dependence forms a closed loop, resulting in a situation of circular dependence.

Here are the general steps for unresolved loop dependencies:

  1. Instantiate A before the property is filled and the @postconstruct initialization method is executed.
  2. Object A finds that it needs to inject object B, but there is no object B in the container (the initialization method will be put into the container once the object is created and the property injection and execution are complete).
  3. Instantiate B before B completes the property fill and initialization method (@postconstruct).
  4. B object found that C objects need to be injected, but C objects do not exist in the container.
  5. Instantiate C before C completes the property fill and initialization method (@postconstruct).
  6. C object found needs to inject A object, but there is no A object in the container.
  7. Repeat Step 1.

Three levels of cache

At the heart of Spring’s solution to loop dependencies is the pre-exposure of objects, which are placed in the second-level cache. The following table illustrates the level 3 cache:

The name of the describe
singletonObjects Level 1 cache, holding complete beans.
earlySingletonObjects Level 2 cache, which holds pre-exposed beans that are incomplete and have not completed property injection and init method execution.
singletonFactories Level 3 caches, which store Bean factories, mainly production beans, are stored in level 2 caches.

All spring-managed beans are eventually stored in singletonObjects, which holds beans that have lived through all life cycles (except destruction), intact and ready for use by the user.

EarlySingletonObjects hold beans that have been instantiated, but have not yet injected properties and executed init methods.

SingletonFactories stores the factories that produce beans.

Why do you need a factory to produce beans when the beans are already instantiated? This is really about AOP. If you don’t need to proxy for beans in your project, the Bean factory will return the object you instantiated from the beginning. If you need to proxy with AOP, the factory will come into play, and this is one of the things we’ll focus on in this article.

Resolving circular dependencies

How does Spring address circular dependencies with the level 3 caching described above? Here we use only cyclic dependencies formed by A and B for example:

  1. Instantiate A, at this point A has not completed the property fill and initialization method (@postconstruct) execution, A is A work in progress.
  2. Create A Bean factory for A and put it into singletonFactories.
  3. Object B needs to be injected to discover A, but object B is discovered in level 1, level 2, and level 3 caches.
  4. Instantiate B, at this point B has not completed the property fill and initialization method (@postconstruct) execution, B is only a work in progress.
  5. Create a Bean factory for B and put it into singletonFactories.
  6. Discovering B requires injecting object A. At this time, object A is not discovered at level 1 or level 2, but object A is discovered in the level 3 cache. Object A is obtained from the level 3 cache, put object A into the level 2 cache, and delete object A from the level 3 cache. (Note that A is still A work in progress and has not completed the property population or performed the initialization method.)
  7. Inject object A into object B.
  8. Object B completes the property population, performs the initialization method, and is put into the level 1 cache, while object B is deleted from the level 2 cache. (Object B is already a finished product.)
  9. Object A gets object B and injects object B into object A. (Object A returns A complete object B)
  10. Object A completes the property population, performs the initialization method, and is put into the level 1 cache, while object A is deleted from the level 2 cache.

We analyze the whole process from the source code:

Create a Bean method in AbstractAutowireCapableBeanFactory: : doCreateBean ()

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
    BeanWrapper instanceWrapper = null;
	
    if (instanceWrapper == null) {
        // instantiate the object
        instanceWrapper = this.createBeanInstance(beanName, mbd, args);
    }

    finalObject bean = instanceWrapper ! =null ? instanceWrapper.getWrappedInstance() : null; Class<? > beanType = instanceWrapper ! =null ? instanceWrapper.getWrappedClass() : null;
   
    // Add an ObjectFactory to the level 3 cache
	boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        // How to add a level 3 cache is detailed below
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // ③ Fill in the properties
    this.populateBean(beanName, mbd, instanceWrapper);
    // ④ Execute the initialization method and create the proxy
    exposedObject = initializeBean(beanName, exposedObject, mbd);
   
    return exposedObject;
}
Copy the code

You can add a level 3 cache as follows:

protected void addSingletonFactory(String beanName, ObjectFactory
        singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) { // Check that this object does not exist in level 1 cache
            this.singletonFactories.put(beanName, singletonFactory); // Add to level 3 cache
            this.earlySingletonObjects.remove(beanName); // Make sure the level 2 cache does not have this object
            this.registeredSingletons.add(beanName); }}}@FunctionalInterface
public interface ObjectFactory<T> {
	T getObject(a) throws BeansException;
}
Copy the code

From this code, we can see that after Spring instantiates an object, it creates a Bean factory for it and adds this factory to the level 3 cache.

Therefore, Spring does not initially expose the instantiated Bean in advance, but rather the ObjectFactory that wraps the Bean. Why do you do that?

This actually involves AOP, and if a Bean is created with a proxy, it should be the proxy Bean injected, not the original Bean. But Spring doesn’t know if a Bean will have a cyclic dependency at first, and usually (in the absence of a cyclic dependency) Spring creates a proxy for it after the property is populated and the initialization method is executed. However, if a cyclic dependency occurs, Spring will have to create a proxy object for it in advance, otherwise it will inject an original object instead of a proxy object. So, where do you create proxy objects in advance?

Spring does this by pre-creating the proxy object in the ObjectFactory. It executes the getObject() method to get the Bean. In fact, this is what it actually does:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if(! mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (BeanPostProcessor bp : getBeanPostProcessors()) {
            if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
                SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
                // If a proxy is needed, the proxy object is returned; Otherwise return the original objectexposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); }}}return exposedObject;
}
Copy the code

Proxied objects are recorded in earlyProxyReferences because the proxied objects were proxied in advance to avoid repeated creation of proxy objects later.

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
		implements SmartInstantiationAwareBeanPostProcessor.BeanFactoryAware {
    @Override
    public Object getEarlyBeanReference(Object bean, String beanName) {
        Object cacheKey = getCacheKey(bean.getClass(), beanName);
        // Record the proxied object
        this.earlyProxyReferences.put(cacheKey, bean);
        returnwrapIfNecessary(bean, beanName, cacheKey); }}Copy the code

From the above analysis, we can see that The purpose of Spring’s three-level caching is to delay the creation of proxy objects and make Bean creation conform to Spring’s design principles without cyclic dependencies.

How to get dependencies

We now know what Spring’s tertiary dependencies do, but how does Spring get dependencies when injecting properties?

He gets the required Bean using a getSingleton() method.

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Level 1 cache
    Object singletonObject = this.singletonObjects.get(beanName);
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        synchronized (this.singletonObjects) {
            // Level 2 cache
            singletonObject = this.earlySingletonObjects.get(beanName);
            if (singletonObject == null && allowEarlyReference) {
                // Level 3 cacheObjectFactory<? > singletonFactory =this.singletonFactories.get(beanName);
                if(singletonFactory ! =null) {
                    // Get the Bean from the Bean factory
                    singletonObject = singletonFactory.getObject();
                    // Put it into level 2 cache
                    this.earlySingletonObjects.put(beanName, singletonObject);
                    this.singletonFactories.remove(beanName); }}}}return singletonObject;
}
Copy the code

When Spring populates a Bean with properties, it first looks for the name of the object to inject, and then executes the getSingleton() method to get the object to inject. The process of getting the object is to fetch it from the level1 cache, or from the level2 cache if it does not have one. If it is not in the level 2 cache, it is fetched from the level 3 cache. If it is not in the level 3 cache, the doCreateBean() method is used to create the Bean.

The second level cache

As we now know, the purpose of the third level cache is to delay the creation of the proxy object, because without a dependency loop, there is no need to create a proxy for it in advance and it can be deferred until initialization is complete.

Since the goal is just to delay, could we not delay the creation, but instead create a proxy object for it after the instantiation is complete, so that we don’t need a third level cache? Therefore, we can modify the addSingletonFactory() method.

protected void addSingletonFactory(String beanName, ObjectFactory
        singletonFactory) {
    Assert.notNull(singletonFactory, "Singleton factory must not be null");
    synchronized (this.singletonObjects) {
        if (!this.singletonObjects.containsKey(beanName)) { // Check that this object does not exist in level 1 cache
            object o = singletonFactory.getObject(); // Get the Bean directly from the factory
            this.earlySingletonObjects.put(beanName, o); // Add to level 2 cache
            this.registeredSingletons.add(beanName); }}}Copy the code

This way, after each Bean is instantiated, the proxy object is created directly and added to the secondary cache. The test results are perfectly normal, and Spring’s initialization time should not matter much, because if the Bean itself does not require a proxy, it returns the original Bean directly, without going through the complex process of creating a proxy Bean.

conclusion

Testing shows that level 2 caching can also resolve cyclic dependencies. Why doesn’t Spring choose a second level cache instead of adding an extra layer of cache?

If Spring chooses level 2 caching to resolve circular dependencies, it means that all beans need to be proxy created as soon as they are instantiated, whereas Spring’s design principle is to delegate beans after they are initialized. So, Spring chooses level 3 caching. However, because of the loop dependency, Spring has to create the proxy in advance, because if the proxy object is not created in advance, then the original object is injected, which causes an error.