Spring Framework Circular Dependency Resolution with Three-Level Cache
AOP and Circular Dependencies
In Spring Framework, AOP implementation relies on BeanPostProcessor classes, with AnnotationAwareAspectJAutoProxyCreator being particularly important. This class extends AbstractAutoProxyCreator and uses either JDK dynamic proxies or CGLib to generate proxy objects when aspect-oriented programming is applied to class methods.
The standard object creation flow for AOP-enabled beans is: Class A → create raw object → property injection → generate proxy object based on aspects → store proxy object in singletonObjects cache.
Since AOP and circular dependency resolution are core Spring features, the framework employs special handling to ensure both work together seamlessly.
Three-Level Cache Mechanism
Spring utilizes three levels of caching to resolve circular dependencies involving AOP proxies. Here's the relevant source code:
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonInstance = this.singletonObjects.get(beanName);
if (singletonInstance == null && isSingletonCurrentlyInCreation(beanName)) {
singletonInstance = this.earlySingletonObjects.get(beanName);
if (singletonInstance == null && allowEarlyReference) {
synchronized (this.singletonObjects) {
singletonInstance = this.singletonObjects.get(beanName);
if (singletonInstance == null) {
singletonInstance = this.earlySingletonObjects.get(beanName);
if (singletonInstance == null) {
ObjectFactory> factory = this.singletonFactories.get(beanName);
if (factory != null) {
singletonInstance = factory.getObject();
this.earlySingletonObjects.put(beanName, singletonInstance);
this.singletonFactories.remove(beanName);
}
}
}
}
}
}
return singletonInstance;
}
The singletonFactories cache stores ObjectFactory instances for bean names. After creating a raw object, Spring constructs an ObjectFactory and stores it in singletonFactories. This ObjectFactory is a functional interface typically implemented as a lambda: () -> getEarlyBeanReference(beanName, beanDefinition, bean).
getEarlyBeanReference Method
The getEarlyBeanReference method is crucial for AOP processing during circular dependency resolution:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object result = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (SmartInstantiationAwareBeanPostProcessor processor : getBeanPostProcessorCache().smartInstantiationAware) {
result = processor.getEarlyBeanReference(result, beanName);
}
}
return result;
}
In Spring, only AbstractAutoProxyCreator implements getEarlyBeanReference with meaningful AOP functionality:
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
String key = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(key, bean);
return wrapIfNecessary(bean, beanName, key);
}
This method creates a cache key (typically the bean name), stores the raw object in earlyProxyReferences, and invokes wrapIfNecessary to generate a proxy object if AOP is required.
Circular Dependency Resolution Process
When a raw object is created via constructor, Spring stores it in the third-level cache associated with a lambda expression. This lambda isn't executed immediately.
When Bean B requires Bean A during dependency injection, Spring checks the third-level cache. If present, the lambda executes, creating a proxy object that gets stored in earlySingletonObjects. Note that this proxy object isn't yet placed in singletonObjects since Bean A hasn't completed property population.
After Bean B completes creation, Bean A continues its lifecycle. Following prpoerty injection, Bean A undergoes AOP processing. However, since Bean A's raw object was already processed during circular dependency resolution, Spring needs to avoid duplicate AOP processing:
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean != null) {
String cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
The earlyProxyReferences cache tracks whether an object has already been proxied, preventing redundant AOP processing.
Proxy Object Completion
When Bean A is created, it starts as a raw object. However, when Bean B is created, it successfully obtains Bean A's proxy object. Subsequent property injection and initialization of Bean A affect the underlying target object within the proxy.
Both CGLib and JDK dynamic proxies maintain references to the target object. The proxy essentially wraps the original object address, so enhancements to the raw bean automatically reflect in the proxy object.
After bean instantiation completes, Spring performs a critical check:
if (earlySingletonExposure) {
Object earlyReference = getSingleton(beanName, false);
if (earlyReference != null) {
if (exposedObject == bean) {
exposedObject = earlyReference;
}
}
}
This code retrieves any early singleton reference from the cache. If Bean B created Bean A's proxy object during circular dependency resolution, this proxy is returned instead of the raw object, ensuring the final stored instance is the proxy.
Cache Summary
- singletonObjects: Caches beans that have completed their full lifecycle
- earlySingletonObjects: Caches beans with incomplete lifecycles, including proxy objects created during circular dependency resolution
- singletonFactories: Stores ObjectFactory lambdas that create early bean references when circular dependencies occur
- earlyProxyReferences: Tracks which raw objects have undergone AOP processing to prevent duplicate proxy creation
The three-level cache system, combined with earlyProxyReferences tracking, enables Spring to efficiently resolve circular dependencies while maintaining proper AOP functionality.