Step‑by‑Step Source Code Walkthrough of Spring’s Singleton Bean Creation
This article dissects Spring’s singleton bean creation process, explaining the three‑level cache system, the doGetBean and doCreateBean workflows, circular‑dependency handling, proxy generation timing, and common pitfalls such as multithreaded duplicate creation and hot‑deployment cache issues.
Most Java back‑end developers use Spring‑managed beans daily, yet the internal creation mechanism remains opaque to the majority.
Underlying Caches and Global Call Chain
Spring maintains three caches: a concurrent singletonObjects map for fully initialized beans, a non‑concurrent earlySingletonObjects map for partially created beans, and a singletonFactories map that holds ObjectFactory instances for early proxy exposure. Two auxiliary sets track beans currently in creation and beans excluded from creation checks. The first cache uses ConcurrentHashMap because it is accessed by many threads, while the second and third caches use plain HashMap as they are only touched within a single bean‑creation thread.
The irreversible container startup sequence is:
finishBeanFactoryInitialization → preInstantiateSingletons → getBean() → doGetBean() → getSingleton(→createBean) → doCreateBean()The split between createBean (pre‑interception) and doCreateBean (instantiation, dependency injection, initialization, proxy generation) is a frequent source of confusion for newcomers.
doGetBean: Top‑Level Dispatch
doGetBeanperforms four core duties: cache lookup, concurrency guard, dependency pre‑processing, and branch routing. It first resolves the real bean name, then checks the third‑level cache with allowEarlyReference=true. If a cached instance is found, it is unwrapped and type‑checked. It also prevents duplicate creation across parent and child containers, marks the bean as being created, merges parent‑child BeanDefinition s, processes explicit @DependsOn dependencies (which cannot be solved by the third‑level cache), and finally acquires a global lock for singleton creation. Hidden branches such as prototype‑creation checks, parent‑container merging, and failure‑handling are often omitted in superficial tutorials but are critical for understanding startup errors.
getSingleton and Early References
The getSingleton method reads from the first cache; if the bean is currently being created, it falls back to the second cache. When allowEarlyReference is true (during the initial creation phase), it may also retrieve an early reference from singletonFactories, expose it via the second cache, and remove the factory entry. After the bean is fully initialized, allowEarlyReference becomes false to prevent a stale half‑bean from overwriting the mature proxy.
doCreateBean: Full Creation Pipeline
doCreateBeanis the heart of bean creation, orchestrating six actions: hot‑deployment cache cleanup, instantiation via reflection, early singleton exposure, property population, initialization (including AOP proxy creation), a second circular‑dependency verification, and disposable‑bean registration.
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
BeanWrapper instanceWrapper = null;
// Hot‑deployment compatibility: clear old cache
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
// 1. Instantiation
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
Object bean = instanceWrapper.getWrappedInstance();
mbd.resolvedTargetType = instanceWrapper.getWrappedClass();
// 2. Early singleton exposure condition
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName) && !inCreationCheckExclusions.contains(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
// 3. Property population + initialization
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);
} catch (Throwable ex) {
throw new BeanCreationException(beanName, "属性填充、初始化失败", ex);
}
// 4. Second circular‑dependency check
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
} else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
Set<String> actualDependentBeans = new LinkedHashSet<>();
for (String dependentBean : getDependentBeans(beanName)) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName, "循环依赖导致Bean引用不一致");
}
}
}
}
// 5. Register disposable bean callback (singleton only)
registerDisposableBeanIfNecessary(beanName, bean, mbd);
return exposedObject;
}The populateBean step resolves @Autowired, @Resource, and @Value annotations by recursively invoking doGetBean. Field injection therefore breaks circular dependencies, whereas constructor injection lacks an early‑exposure window and cannot resolve them.
Proxy generation follows two paths: when a circular dependency exists, a lambda stored in the second‑level cache creates the proxy early; otherwise, a post‑processor creates the proxy after full initialization. The early path can cause aspect‑order disorder and intermittent transaction failures, which are common sources of hidden production bugs.
Practical Pitfalls
Multithreaded duplicate creation : A custom BeanPostProcessor that bypasses Spring’s native getSingleton lock can cause the same singleton to be instantiated concurrently. The fix is to add an object‑level mutex inside the custom processor.
Nested circular dependencies causing NPE : In deep mutual‑dependency scenarios, the non‑thread‑safe second‑level HashMap may resize concurrently, yielding a null read. Spring provides no patch; the remedy is to avoid writing nested circular‑dependency code.
Bean reference inconsistency after AOP replacement : Post‑processor replacement leaves the old native reference in the second‑level cache, triggering a mismatch check. The solution is to eliminate reliance on the third‑level cache fallback and refactor the code to break the circular dependency (as mandated by large tech firms).
Hot‑deployment cache residue : Tools like JRebel refresh BeanDefinition but leave stale half‑beans in the second‑level cache, leading to functional anomalies. Disable the hot‑deployment cache or restart the application to clear the context.
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.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
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.
