How Spring Solves Circular Dependencies and the Underlying Essence
This article explains how Spring resolves circular dependencies for singleton beans using a three‑level cache, demonstrates a simple prototype implementation, draws an analogy to the classic two‑sum algorithm, and highlights the core principle behind Spring's dependency injection mechanism.
Introduction
Spring's circular dependency problem has become a popular Java interview question. The article first questions the value of digging into framework source code without understanding the underlying principle.
Spring's Solution to Circular Dependencies
Spring handles circular dependencies only for default singleton beans where properties reference each other. Prototype beans do not support circular dependencies and will trigger a BeanCurrentlyInCreationException as shown below:
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}The core of Spring's solution is a three‑level cache maintained in DefaultSingletonBeanRegistry :
singletonObjects – the usual singleton pool containing fully created beans.
singletonFactories – factories that can create bean instances.
earlySingletonObjects – holds early references to beans that are not yet fully initialized.
The two auxiliary maps act as stepping stones; once a bean is fully created, its entry is moved to singletonObjects .
Essential Idea of Circular Dependency
To illustrate the essence, the article presents a minimal implementation that mimics Spring's behavior: a static cacheMap stores created beans, and getBean recursively creates and injects dependencies, handling circular references by checking the cache first.
private static Map
cacheMap = new HashMap<>(2);
public static
T getBean(Class
beanClass) {
String beanName = beanClass.getSimpleName().toLowerCase();
if (cacheMap.containsKey(beanName)) {
return (T) cacheMap.get(beanName);
}
Object object = beanClass.getDeclaredConstructor().newInstance();
cacheMap.put(beanName, object);
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
Class
fieldClass = field.getType();
String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
field.set(object, cacheMap.containsKey(fieldBeanName) ? cacheMap.get(fieldBeanName) : getBean(fieldClass));
}
return (T) object;
}This code demonstrates how a simple cache can resolve circular references without the full complexity of Spring.
Analogy to Two‑Sum Problem
The article draws a parallel between the bean‑caching logic and the classic "two‑sum" algorithm from LeetCode, providing a concise solution that uses a hashmap to find two numbers that add up to a target:
class Solution {
public int[] twoSum(int[] nums, int target) {
Map
map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
}The similarity lies in first checking the cache (map) for an existing entry before creating a new one, which is exactly how Spring resolves circular dependencies.
Conclusion
Understanding the three‑level cache and the simple caching principle helps readers grasp why Spring's implementation is as complex as it is, and provides a practical way to think about circular dependencies beyond merely reading source code.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.