Keywords: Spring Framework | Circular Dependency | Bean Injection
Abstract: This article provides an in-depth exploration of how the Spring framework handles circular dependencies between beans. By analyzing Spring's instantiation and injection processes, it explains why BeanCurrentlyInCreationException occurs with constructor injection while setter injection works seamlessly. The core mechanism of Spring's three-level cache for resolving circular dependencies is detailed, along with best practices using the InitializingBean interface for safe initialization. Additionally, performance issues in large-scale projects involving FactoryBeans in circular dependencies are discussed, including solutions such as manual injection via ApplicationContextAware and scenarios for disabling circular reference resolution.
Spring's Circular Dependency Resolution Mechanism
In the Spring framework, circular dependency refers to a situation where two or more beans depend on each other, such as bean A depending on bean B while bean B also depends on bean A. Spring resolves this issue through a clever instantiation and injection sequence, but developers must understand its workings to avoid potential pitfalls.
Analysis of Instantiation and Injection Process
When using setter injection, Spring's resolution process is as follows: First, bean A is instantiated, existing in an "early reference" state where the object is created but properties are not yet injected. Next, bean B is instantiated, also in an early reference state. Then Spring injects bean A into bean B, and finally injects bean B into bean A. The key to this process is that Spring allows injecting references to beans that are not fully initialized into other beans.
// Example: Class A definition
package mypackage;
public class A {
public A() {
System.out.println("Creating instance of A");
}
private B b;
public void setB(B b) {
System.out.println("Setting property b of A instance");
this.b = b;
}
}
// Example: Class B definition
package mypackage;
public class B {
public B() {
System.out.println("Creating instance of B");
}
private A a;
public void setA(A a) {
System.out.println("Setting property a of B instance");
this.a = a;
}
}
Corresponding XML configuration:
<bean id="a" class="mypackage.A">
<property name="b" ref="b" />
</bean>
<bean id="b" class="mypackage.B">
<property name="a" ref="a" />
</bean>
The execution output will show:
Creating instance of A
Creating instance of B
Setting property a of B instance
Setting property b of A instance
Limitations of Constructor Injection
Unlike setter injection, constructor injection in circular dependency scenarios leads to BeanCurrentlyInCreationException. This occurs because constructor injection requires all dependencies to be provided immediately when creating the bean instance, which circular dependencies make impossible.
// Problem example: Circular dependency with constructor injection
class A {
private final B b; // must be initialized in constructor
public A(B b) { this.b = b; }
}
class B {
private final A a; // must be initialized in constructor
public B(A a) { this.a = a; }
}
Safe Initialization with InitializingBean
Since Spring resolves circular dependencies with property injection that may not follow the order in configuration files, critical initialization logic that depends on other properties should not be executed in setter methods. The recommended approach is to implement the InitializingBean interface and perform all initialization in the afterPropertiesSet() method.
import org.springframework.beans.factory.InitializingBean;
public class ExampleBean implements InitializingBean {
private DependencyBean dependency;
public void setDependency(DependencyBean dependency) {
this.dependency = dependency;
}
@Override
public void afterPropertiesSet() throws Exception {
// Execute all initialization logic here
// Can safely assume all properties have been set
if (dependency == null) {
throw new IllegalStateException("dependency property not set");
}
dependency.initialize();
}
}
Performance Issues with FactoryBeans and Circular Dependencies
In large-scale projects, when FactoryBeans participate in circular dependencies, significant performance issues may arise. Spring may attempt to create beans multiple times when resolving such dependencies, leading to FactoryBeanNotInitializedException exceptions and substantial overhead.
Debugging approach: Set a conditional breakpoint in the exception handling section of AbstractBeanFactory.doGetBean():
catch (BeansException ex) {
// Explicitly remove instance from singleton cache
destroySingleton(beanName);
throw ex;
}
Alternative Solutions
For complex circular dependency scenarios, consider the following alternatives:
- Manual Injection via ApplicationContextAware: Implement the ApplicationContextAware interface to manually obtain dependent beans in the afterPropertiesSet() method.
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
public class A implements ApplicationContextAware, InitializingBean {
private B cyclicDependency;
private ApplicationContext ctx;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ctx = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
cyclicDependency = ctx.getBean(B.class);
}
public void useCyclicDependency() {
cyclicDependency.doSomething();
}
}
<ol start="2">
class A {
private final B b;
public A(@Lazy B b) { this.b = b; }
}
class B {
private final A a;
public B(A a) { this.a = a; }
}
<ol start="3">
AbstractRefreshableApplicationContext context = new ClassPathXmlApplicationContext();
context.setAllowCircularReferences(false);
context.refresh();
Conclusion and Recommendations
Spring elegantly resolves circular dependencies with setter injection through its three-level cache mechanism, but developers should be aware that: 1) constructor injection does not support circular dependencies; 2) property injection order is unpredictable, so critical initialization should be performed in afterPropertiesSet(); 3) FactoryBeans in circular dependencies may cause performance issues; 4) for complex scenarios, consider using ApplicationContextAware, @Lazy annotation, or redesigning dependencies. In large projects, regularly review circular dependencies to ensure they don't impact system performance and maintainability.