Author: Guo Yanhong
The following examples are all discussions on singleton mode

Illustration Reference https://www.processon.com/view/link/60e3b0ae0e3e74200e2478ce
1. How does Spring create a Bean?
For singleton Beans, there is only one object throughout the entire lifecycle of the Spring container.
Spring uses a three-level cache during the creation of Beans, defined in DefaultSingletonBeanRegistry.java:
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
Taking OneBean under the com.gyh.general package as an example, debug the springboot startup process, and analyze how Spring creates a bean.
Refer to the figure below Spring creates a beanThe process. The most critical steps include:
1.getSingleton(beanName, true)
Search the bean object from the first, second, and third-level caches in turn; if the object exists in the cache, return it directly (early);
2.createBeanInstance(beanName, mbd, args)
Select a suitable constructor, create an instance object (instance), at this time the properties that depend on the instance are all null, which belongs to a semi-finished product;
3.singletonFactories.put(beanName, oneSingletonFactory)
Use the instance from the previous step to build a singletonFactory, and place it in the three-level cache;
4.populateBean(beanName, mbd, instanceWrapper)
Populate the bean: create objects or assign values for the properties defined for the bean;
5.initializeBean("one",oneInstance, mbd)
Initialize the bean: initialize the bean or perform other processing, such as generating a proxy object (proxy)}
6.getSingleton(beanName, false)
Search in the first and second levels of cache sequentially, check if there are any objects generated prematurely due to cyclic dependencies, and whether they are consistent with the initialized objects;
2. How does Spring solve cyclic dependencies?
Taking OneBean and TwoBean under the com.gyh.circular.threeCache package as an example, the two beans are interdependent (i.e., forming a closed loop).
Refer to the figure below Spring solves circular dependenciesFrom the process described above, it can be seen that Spring uses the objectFactory in the three-level cache to generate and return an early object, and expose this early address in advance for other objects to use for dependency injection, thereby solving the problem of cyclic dependency.
3. What cyclic dependencies can Spring not resolve?
3.1 @Async annotation used in a loop
3.1.1 Why does an error occur when @Async is used in a loop?
Taking OneBean and TwoBean under the com.gyh.circular.async package as an example, the two beans are interdependent, and the method in oneBean uses the @Async annotation. In this case, starting Spring fails and the error message is reported as follows:org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'a.one': Bean with name 'a.one' has been injected into other beans [a.two] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.
And through debug code, it was found that the error occurs within the AbstractAutowireCapableBeanFactory#doCreateBean method, due to earlySingletonReference != null and exposedObject != bean, resulting in an error.
Combined with the flowchart Spring solves circular dependenciesAs can be seen from the above picture:
1.In line 1, bean is the instance created by createBeanInstance(address1)
2.In line 2, exposedObject is the proxy object generated after initializeBean(address2)
3.In line 3, earlySingletonReference is the object created by getEarlyBeanReference【The address here is the same as bean(address1)】
The deep reason is that TwoBean has already relied on the earlySingletonReference object with the address of address1 during the populateBean process. At this point, after OneBean has been initialized, it returns a new object with the address of address2, causing spring to not know which is the final version of the bean, so an error is reported.
How is earlySingletonReference generated, refer to the getSingleton("one", true) process.
Does using @Async in a loop always result in an error?
Taking OneBean and TwoBean under the com.gyh.circular.async package as an example, the two beans are interdependent, making the methods in TwoBean (non-OneBean) use the @Async annotation. At this point, the spring is started successfully without any errors.
Debug code shows that although TwoBean uses the @Async annotation, its earlySingletonReference is null; therefore, it will not cause any errors.
The underlying reason is that OneBean is created first, followed by TwoBean, and throughout the entire chain, TwoBean's objectFactory has never been searched for in the third-level cache. (OneBean is searched twice during its creation, i.e., one->two->one; TwoBean's creation process only searches it once, i.e., two->one.)
Therefore, the preliminary condition for @Async causing circular dependency errors is:
1.The Bean in circular dependency uses the @Async annotation
2.And this Bean is created before other Beans in the loop.
3.Note: A Bean may exist in multiple loops at the same time; as long as it is the first Bean created in a loop, an error will occur.
3.1.3 Why does using @Transactional in a loop not cause any errors?
It is known that a Bean annotated with @Transactional will also have a proxy generated by Spring, but why does this type of Bean not cause any errors when it is in a loop?
Taking OneBean and TwoBean under the com.gyh.circular.transactional package as an example, the two Beans depend on each other, and the method in OneBean uses the @Transactional annotation. Starting Spring successfully will not cause any errors.
Debug code shows that during the generation of OneBean, although earlySingletonReference is not null, the addresses of exposedObject after initializeBean and the original instance are the same (i.e., no proxy is generated for the instance during the initializeBean step), so no error will occur.
3.1.4 Why does the same proxy generation produce two different phenomena?
The reason why the same proxy generation can produce different phenomena when involved in circular dependencies is that the nodes generating the proxy are different when they are in a circular dependency:
1.@Transactional generates a proxy at getEarlyBeanReference and exposes the address after the proxy (i.e., the final address) early on;
2.@Async generates a proxy at initializeBean, causing the address exposed in advance not to be the final address, resulting in an error.
Why can @Async not generate a proxy at getEarlyBeanReference? By comparing the execution process of the two, it is found that:
Both are wrapping the original instance object in the method AbstractAutoProxyCreator#getEarlyBeanReference, as shown in the figure below
When the Bean using @Transactional is creating a proxy, it gets an advice and then generates the proxy object proxy.
However, when the Bean using @Async is creating a proxy, it does not obtain an advice and cannot be proxied.
3.1.5 Why can @Async not return an advice at getEarlyBeanReference?
Within the method AbstractAutoProxyCreator#getAdvicesAndAdvisorsForBean, the main things it does are:
1.Find all Advisors in the current spring container
2.Return all Advisors that match the current bean
The Advisor returned in the first step is BeanFactoryCacheOperationSourceAdvisor and BeanFactoryTransactionAttributeSourceAdvisor, without any Advisor related to Async processing.
Delve deep into the reasons why the first step does not return an Advisor related to Async processing?
It is known that @Async, @Transactional, and @Cacheable need to be enabled in advance, that is, @EnableAsync, @EnableTransactionManagement, and @EnableCaching need to be annotated in advance.
For example, in the annotation definition of @EnableTransactionManagement and @EnableCaching, the Selector class is introduced, which in turn introduces the Configuration class. In the Configuration class, the corresponding Advisor is created and placed in the spring container, so the first step is to get these two Advisors.
The Configuration class introduced in the definition of @EnableAsync creates AsyncAnnotationBeanPostProcessor and not an Advisor, so it will not get it in the first step, so the @Async bean will not be proxied at this step.
3.2 Circular dependency caused by constructor
Taking OneBean and TwoBean under the com.gyh.circular.constructor package as an example, the constructors of the two classes depend on each other. When starting spring, an error is reported:org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'c.one': Requested bean is currently in creation: Is there an unresolvable circular reference?
The debug code shows that the two beans are already in a dead loop when they are created according to the constructor, and they cannot expose an available address in advance, so only an error can be reported.
4. How to solve the above circular dependency error?
1.Do not use @Async, and execute the methods that need asynchronous operations in the thread pool. (Recommended)
2.Extract the methods annotated with @Async. (Recommended)
3.Extract the methods using @Async to a separate class, which only does asynchronous processing and does not do other business dependencies, thus avoiding the formation of circular dependencies and solving the error problem. Refer to the com.gyh.circular.async.extract package.
4.Try not to use the constructor to depend on objects. (Recommended)
5.Destroy the loop (not recommended) that does not form a closed loop. Plan the object dependency and method call chain in advance during development to avoid circular dependencies as much as possible. (It is difficult, as the iterative development changes continuously, it is likely to produce loops.)
6.Destroy the creation order (not recommended)
7.Since the class annotated with @Async is created before other classes in the circular dependency, an error will occur. To solve this problem, you can make sure that this class is not created before other classes, such as using @DependsOn and @Lazy.

评论已关闭