Introduction to Circular Dependencies
During application development, we often face circular dependency scenarios where two or more beans depend on each other. This creates a chicken-and-egg problem during initialization. Spring has designed its three-level cache mechanism specifically to resolve these issues.
Understanding the Three-Level Cache Structure
Before diving into implementation details, let's understand what each cache level stores:
- First-level cache (singletonObjects): Stores fully initialized beans ready for use in the application.
- Second-level cache (earlySingletonObjects): Holds partially initialized beans used specifically for resolving circular dependencies.
- Third-level cache (singletonFactories): Contains ObjectFactory<?> lambda expressions that handle cases where AOP is involved in circular dependencies.
The cache levels are named based on their query order, not their declaration order in the code. Spring primarily supports three injection methods: constructor injection, setter injection, and interface injection. However, interface injection is rarely used due to its complexity, while constructor injection cannot handle circular dependencies. Setter injection, which separates instantiation from initialization, is the foundation for Spring's circular dependency resolution.
Scenario Analysis
1. No Dependencies with AOP
Let's examine a simple case where a bean has no dependencies but requires AOP proxying. The creation process follows this path:
ClassPathXmlApplicationContext#refresh
→ finishBeanFactoryInitialization(beanFactory)
→ beanFactory.preInstantiateSingletons()
→ getBean() → doGetBean()
During bean creation, we observe these cache states:
Map<String, Object> singletonObjects: No simpleBean
Map<String, Object> earlySingletonObjects: No simpleBean
Map<String, ObjectFactory<?>> singletonFactories: No simpleBean
Set<String> singletonsCurrentlyInCreation: Contains simpleBean
The critical methods in doCreateBean include:
- createBeanInstance: Creates a semi-finished object through reflection
- populateBean: Fills in the bean's properties
- initializeBean: Completes the bean initialization
- applyBeanPostProcessorsAfterInitialization: Creates AOP proxies if needed
In this scenario, the proxy is created after full initialization. The first-level cache alone would suffice, as the other caches aren't utilized.
2. Circular Dependencies Without AOP
Consider two classes, ComponentA and ComponentB, that reference each other. Let's trace the creation process:
When creating ComponentA, the cache states are:
Map<String, Object> singletonObjects: No componentA or componentB
Map<String, Object> earlySingletonObjects: No componentA or componentB
Map<String, ObjectFactory<?>> singletonFactories: Contains componentA's lambda
Set<String> singletonsCurrentlyInCreation: Contains componentA
During populateBean for ComponentA, Spring attempts to resolve the ComponentB dependency. Since ComponentB doesn't exist, its creation begins. The process then attempts to resolve ComponentA's reference within ComponentB, triggering the circular dependency resolution.
At this point, the third-level cache's lambda for ComponentA is executed, returning the semi-finished ComponentA instance. This instance is placed in the second-level cache and returned to complete ComponentB's initialization. ComponentB is then moved to the first-level cache and returned to complete ComponentA's initialization.
In this scenario, the third-level cache could be eliminated, as a two-level cache would suffice.
3. Circular Dependencies With AOP
Now let's examine the more complex case where circular dependencies exist and both beans require AOP proxying. The process starts similarly to the previous scenario, but with a critical difference:
When ComponentB requests ComponentA during its property population, the third-level cache's lambda doesn't return the raw semi-finished ComponentA. Instead, it creates an AOP proxy for the semi-finished ComponentA through the getEarlyBeanReference method. This proxy is placed in the second-level cache and returned to ComponentB.
After ComponentB completes its initialization, its own AOP proxy is created and placed in the first-level cache. This proxy is then returned to ComponentA to complete its initialization. Since ComponentA's proxy already exists in the second-level cache, it's moved to the first-level cache without creating a duplicate proxy.
The final cache states are:
Map<String, Object> singletonObjects: Contains both componentA and componentB proxies
Map<String, Object> earlySingletonObjects: Empty
Map<String, ObjectFactory<?>> singletonFactories: Empty
Set<String> singletonsCurrentlyInCreation: Empty
In this scenario, all three cache levels are essential. The third-level cache enables early proxy creation, while the second-level cache stores these early proxies to prevent duplicate creation.
Why Three Levels?
Technically, Spring could function with just two levels by creating proxies earlier in the process. However, this would violate Spring's design principle of delaying proxy creation until absolutely necessary. The three-level approach maintains this principle while still resolving circular dependencies efficiently.
The third-level cache acts as a factory that can generate either regular objects or early proxies as needed, preserving Spring's optimization strategy of creating proxies as late as possible in the bean lifecycle.