Spring Initialization Failures with BeanPostProcessor and Circular Dependencies
A secondary library in our team, TEST, intercepts beans by implementing BeanPostProcessor and manually creates AOP proxies during interception. In development, initialization fails when a proxied bean has a circular dependency, so I debugged it.
This article covers the Spring bean lifecycle, AOP, and circular dependencies.
Prerequisites
- First-level cache:
singletonObjects, containing fully initialized objects. - Second-level cache:
earlySingletonObjects, containing instantiated objects (used for circular dependencies). - Third-level cache:
singletonFactory, containing other instantiation operations.
Spring obtains three forms of a bean instance:
bean: the original bean.exposedObject: the extended bean.earlySingletonReference: a bean obtained from the first two caches (and, when the parameter is true, the third cache); an early-exposed circular dependency.
Assume class a is proxied, a references b, and b also references a. The source-code flow is as follows.
Source-code analysis
First obtain instance
a.
beanFactory.preInstantiateSingletons()->AbstractBeanFactory#getBean->AbstractBeanFactory#doGetBeanDefaultSingletonBeanRegistry#getSingleton(String,true)- Nothing is stored in
singletonObjects, and this bean is not being created, so this branch is not taken. ❌
- Nothing is stored in
AbstractBeanFactory#markBeanAsCreatedmarks the bean as being created.DefaultSingletonBeanRegistry#getSingleton(String, ObjectFactory<?>)AbstractAutowireCapableBeanFactory#createBeancreates the bean instance.AbstractAutowireCapableBeanFactory#resolveBeforeInstantiationperforms work before bean instantiation (extensible: a user may create and return the object early). ❌AbstractAutoProxyCreator#postProcessBeforeInstantiationcan create a proxy, but no proxy is created when annotations are used.
AbstractAutowireCapableBeanFactory#doCreateBeanenters the lambda from above and actually creates the bean.AbstractAutowireCapableBeanFactory#createBeanInstanceinstantiates the bean and wraps it inBeanWrapper.DefaultSingletonBeanRegistry#addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory)adds a singleton factory, preparing the third-level cache for AOP (proxy creation) and circular dependencies.AbstractAutowireCapableBeanFactory#populateBeanpopulates the bean instance.- It performs post-processing after instantiation; if explicit instantiation is true, it returns immediately. ❌
AutowiredAnnotationBeanPostProcessor#postProcessPropertiesinjects fields annotated with@Autowired.- It obtains dependency
b(go to step 2). AbstractAutowireCapableBeanFactory#initializeBeaninitializes the bean and obtains the extendedexposedObject; at this point there is no extension.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsBeforeInitializationperforms extension processing before initialization.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitializationperforms extension processing after initialization.- With no extension,
bean==exposedObject. For TEST,exposedObject=proxyhere, and its fields have not been populated.
DefaultSingletonBeanRegistry#getSingleton(String,false)reads an object from the first and second caches and assigns it toearlySingletonReference. At this point the second cache contains the instance, and it is a proxy. Other implementations store the original bean.- Because
bean==exposedObject, the proxy is returned directly. For TEST,bean!=exposedObject; the system detects that another bean has already used this bean and throws an exception.
- Put the bean into the first-level cache and remove it from the second-level cache.
Obtain instance
b.
AbstractBeanFactory#getBean->AbstractBeanFactory#doGetBeanobtains a field dependency.DefaultSingletonBeanRegistry#getSingleton(String,true)singletonObjectshas no entry and the bean is not being created, so this branch is not taken. ❌
AbstractBeanFactory#markBeanAsCreatedmarks the bean as being created.DefaultSingletonBeanRegistry#getSingleton(String, ObjectFactory<?>)AbstractAutowireCapableBeanFactory#createBeancreates the bean instance.AbstractAutowireCapableBeanFactory#resolveBeforeInstantiationperforms pre-instantiation work. ❌AbstractAutoProxyCreator#postProcessBeforeInstantiationcan create a proxy, but annotations do not create one here.
AbstractAutowireCapableBeanFactory#doCreateBeanactually creates the bean.AbstractAutowireCapableBeanFactory#createBeanInstanceinstantiates and wraps the bean inBeanWrapper.DefaultSingletonBeanRegistry#addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory)adds the singleton factory for AOP proxy creation and circular dependencies.AbstractAutowireCapableBeanFactory#populateBeanpopulates the bean.- The post-instantiation operation returns immediately when explicit instantiation is true. ❌
AutowiredAnnotationBeanPostProcessor#postProcessPropertiesinjects@Autowiredfields.- It gets dependency
a(go to step 3) and receives the proxy bean. Other implementations store the original bean. AbstractAutowireCapableBeanFactory#initializeBeaninitializes the bean and getsexposedObject.- It applies post-processors before initialization.
- It applies post-processors after initialization.
- There is no extension, so
bean==exposedObject.
DefaultSingletonBeanRegistry#getSingleton(String,false)reads from the first and second caches. Both are empty, and entering the third cache is disallowed, soearlySingletonReferenceis empty.- The bean instance is therefore returned directly.
- Put
binto the first-level cache.
Obtain instance
a.
AbstractBeanFactory#getBean->AbstractBeanFactory#doGetBeanobtains the field dependency.DefaultSingletonBeanRegistry#getSingleton(String)singletonObjectshas no entry, but the bean is already being created.DefaultSingletonBeanRegistry#getSingleton(String,true)- Neither
singletonObjectsnorearlySingletonObjectshas an entry, so Spring enters the third cache:singletonFactory. AbstractAutowireCapableBeanFactory#getEarlyBeanReferenceis the third-cache operation.AbstractAutoProxyCreator#wrapIfNecessarycreates the proxy. Other implementations do not take this step.- Store
(a, proxy)in the second-level cache. Other implementations store the original bean.
- Neither
Diagram

Cause
Suppose a is proxied, a references b, and b references a.
With normal AOP, when b is populated with a, a is already a proxy.
With TEST’s approach, however, a is still the original bean when b is populated with it; it is proxied only afterward, so the reference cannot be used.
Solutions
For the secondary library: use native AOP annotations.
On the consumer side: add
@Lazyto the non-proxied bean in the circular dependency so it is not loaded during container refresh and is loaded only when used.With
@Lazy, the path at 121233 is not taken; at 121235,earlySingletonReferenceis empty, and Spring directly returns the proxy class fromexposedObject.When the circular-dependency field is actually referenced, it loads the earlier proxy bean and completes the circular dependency.