Understanding How Spring Handles Circular Dependencies and Its Core Principle
This article explains Spring's three‑level cache mechanism for resolving circular dependencies in singleton beans, demonstrates a simplified implementation with Java code, draws an analogy to the classic two‑sum algorithm, and highlights the essential idea behind circular dependency handling.
Spring's circular‑dependency problem, a common Java interview question, mainly concerns singleton beans that reference each other through properties. The article first notes that prototype‑scoped beans do not support circular dependencies and will throw a BeanCurrentlyInCreationException.
Spring solves the singleton case by maintaining three internal maps (often called a three‑level cache) in DefaultSingletonBeanRegistry: singletonObjects – the final cache that holds fully created singleton beans. singletonFactories – stores factories capable of creating bean instances. earlySingletonObjects – holds early references to beans that are not yet fully initialized.
During bean creation, Spring first creates an instance and puts it into earlySingletonObjects, then resolves its dependencies using singletonFactories. Once all dependencies are satisfied, the bean is moved to singletonObjects. This “pouring water between cups” process is illustrated with several diagrams in the original article.
To make the concept clearer, the author provides a minimal Java implementation that mimics Spring's behavior. The code creates a static cacheMap to store bean instances, recursively instantiates beans, and injects fields by looking them up in the cache, thereby handling circular references without the full Spring infrastructure:
public class A {
private B b;
}
public class B {
private A a;
}
private static Map<String, Object> cacheMap = new HashMap<>(2);
private static <T> T getBean(Class<T> beanClass) throws Exception {
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;
}The author then draws an analogy to the classic "two‑sum" problem from LeetCode, showing that the same hashmap‑based approach used to resolve circular dependencies mirrors the algorithm that finds two numbers adding up to a target:
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> 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");
}
}Finally, the article concludes that understanding the essence of circular dependency—caching early bean references—helps avoid getting lost in Spring's source code, and encourages readers to implement a simple version first to grasp why Spring adopts such a complex mechanism.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.
