How does Spring solve the problem of circular dependency?

Series blog posts:
[IoC-0] introduction of Spring IoC
[IoC-1] introduction of control inversion of IoC
[IoC-2] BeanDefinition scanning registration of IoC
[IoC-3] creation process of Spring bean
[IoC-4] dependency injection principle of IoC
[IoC-5] Spring IoC Summary - control inversion and dependency injection

Related reading:
@Difference between Resource and @ Autowired
Generation rules for bean name
BeanDefinition detailed analysis

preface

As we all know, Spring solves the problem of circular dependency. It can also be found on the Internet that Spring uses three-level cache to solve circular dependency.
Have you thought about why Spring uses L3 cache to solve the problem of circular dependency? Can I use two-level cache? Is it OK to use only L1 cache?
Which circular dependency scenarios can't be solved by Spring?
This article first analyzes how Spring solves the problem of circular dependency through three-level cache.
Later, we'll analyze why we use L3 cache and what cyclic dependency scenarios Spring can't solve.

text

We were analyzing earlier bean creation process It was mentioned that the creation of beans is triggered by AbstractBeanFactory#getBean().
Follow the source code of AbstractBeanFactory#getBean(java.lang.String), and you will find that it will call DefaultSingletonBeanRegistry#getSingleton()

// DefaultSingletonBeanRegistry#getSingleton()
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // Quick check for existing instance without full singleton lock
    Object singletonObject = this.singletonObjects.get(beanName);
    // There is no beanName in the L1 cache and beanName is being created
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        singletonObject = this.earlySingletonObjects.get(beanName);
        // There is no beanName in the bean's early reference, and early reference is allowed
        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;
}

The above is the code of L3 cache.
Let's take a good look at how it solves circular dependency!

L3 cache is defined as follows:

// DefaultSingletonBeanRegistry.java

/** L1 cache: Cache of singleton objects: bean name to bean instance */
// Used to store bean s that have been fully initialized 
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** L3 cache: Cache of singleton factories: bean name to ObjectFactory */
// It is used to store the early references of bean s (all will be stored, and it will be used only when circular dependency is applied)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

/** L2 Cache of early singleton objects: bean name to bean instance */
// It is used to store the objects obtained by the three-level cache ObjectFactory (only in case of circular dependency)  
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

Prepare examples

We prepare the simplest example of circular dependency, a -- > scenario of a:

@Service
public class AService {
    @Autowired
    AService aService;
    
    public AService getaService() {
        return aService;
    }
}

public class CircleTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.kvn.beans.circle");
        AService a = applicationContext.getBean(AService.class);
        System.out.println(a + "--" + a.getaService());
    }
}

The program can run normally, and the AService injected in AService is itself.
This proves that Spring solves the problem of circular dependency.

Cycle dependent process analysis

AbstractBeanFactory#getBean() will first get the bean from the cache when triggering bean loading, that is, it will call the DefaultSingletonBeanRegistry#getSingleton() method.

Debugging DefaultSingletonBeanRegistry#getSingleton() through the simplest example above will find:
During the initialization of AService, when getSingleton() is called for the first time, there is no AService in singletonObjects in the first level cache, and the bean is not being created.
Later, when creating an instance of AService in createBean(), the early reference of the bean will be exposed in the three-level cache earlySingletonObjects through ObjectFactory.
When populateBean() injects dependency, getSingleton() will be called for the second time. At this time, AService is already being created, and there are early references of beans in the three-level cache earlySingletonObjects.
At this time, it can be injected.

In fact, when getSingleton() is called for the first time, there is no AService in all three caches
The early reference of the bean exposed in the third level cache earlySingletonObjects is an ObjectFactory object.

Breakpoint diagram of the second call to getSingleton():

When creating a bean, cache usage:

Analyze the usage of cache in combination with the bean creation process

The bean creation process is divided into three stages:
1 create an instance createBeanInstance
2 fill dependent populateBean
3 initializeBean

The following figure shows the bean creation process:

  • Without circular dependency:
    After a new bean instance is created, the early reference of the bean will be exposed to the singletonFactories of the three-level cache through ObjectFactory.
    After bean initialization, the corresponding L2 cache and L3 cache will be cleared, and the initialized beans will be put into the L1 cache singletonObjects.

  • In the case of cyclic dependency: (a -- > B -- > a scenario)

Summary

Spring solves the problem of circular dependency through three-level cache.
The definition and usage of L3 cache are as follows:

/** L1 cache: used to store fully initialized bean s */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

/** L2 cache: used to store objects obtained from L3 cache ObjectFactory */ 
// The L2 cache will not be stored for non cyclic dependency scenarios
// In the scenario of circular dependency, objects will be obtained through the ObjectFactory of the L3 cache and put into the L2 cache for use
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

/** Level 3 cache: used to store early references to bean s */
// No matter whether there is circular dependency or not, the level 3 cache will be stored after createbean instance
// In case of circular dependency, the third level cache will be used. The objects will be obtained through the third level cache, put into the second level cache, and delete the third level cache at the same time
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

If this article is helpful to you, welcome to like the collection!

Keywords: Java Spring

Added by westen on Tue, 28 Sep 2021 04:20:32 +0300