Spring Source Analysis: Bean Lifecycle (Part 3)

In previous articles, we've covered the foundational steps for bean preparation, including bean definition processing and FactoryBean identification. Building upon this knowledge, we can now dive deeper into the implementation logic of the getBean method and gain better insights into how the createBean method works.

Usage of getBean

Before diving into the getBean method implementation, let's explore its common usage patterns:

// Initialize a Spring context
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
UserService bean1 = applicationContext.getBean(UserService.class);
UserService bean2 = (UserService) applicationContext.getBean("userService");
UserService bean3 = applicationContext.getBean("userService", UserService.class);
UserService bean4 = (UserService) applicationContext.getBean("userService", new OrderService());
bean1.test();
bean2.test();
bean3.test();
bean4.test();

The first two approaches are widely used. The third one checks whether the returned bean matches the specified type, and if so, performs a type conversion. The fourth approach uses constructor inference to select an appropriate constructor when creating the bean instance.

To make the fourth approach effective, you may want to use a prototype scope by setting the @Scope("prototype") annotation. This ensures that each call to getBean creates a new instance, allowing parameterized constructors to be invoked:

@Component
@Scope("prototype")
public class UserService {

    public UserService() {
        System.out.println(0);
    }

    public UserService(OrderService orderService) {
        System.out.println(1);
    }

    public void test() {
        System.out.println(11);
    }
}

General Flow of getBean

Due to the extensive code involved, I will not show the full source here but instead provide a simplified pseudo-code outline for clarity. Then, we'll examine each phase in detail:


protected <T> T doGetBean(
        String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly)
        throws BeansException {

    String beanName = transformedBeanName(name);
    Object beanInstance;

    // First, check the singleton cache for existing singletons
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        // Handle FactoryBean instances
    } else {
        // Check if bean definition exists in this factory, otherwise delegate to parent
        try {
            RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
            checkMergedBeanDefinition(mbd, beanName, args);
            String[] dependsOn = mbd.getDependsOn();
            if (dependsOn != null) {
                // Resolve dependencies
            }
            if (mbd.isSingleton()) {
                // Call createBean
            } else if (mbd.isPrototype()) {
                // Call createBean with prototype-specific logic
            } else {
                // Handle other scopes
            }
        } catch (Exception e) {
            ...
        }
    }

    return adaptBeanInstance(name, beanInstance, requiredType);
}

Singleton Cache

Regardless of whether a bean is singleton or prototype, Spring first attempts to retrieve it from the singleton cache. While some might consider this inefficient, Spring optimizes for the majority of beans being singletons. This overhead is minimal and resembles Java’s class loader delegation model — checking local caches before delegating upwards.

Parent Bean Factory

When analyzing bean definition creation, we can ignore singleton caching and focus on how definitions are processed. Evenif a parent factory exists, it can be bypassed in simple setups. The code snippet below shows how delegation occurs:


BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
    String nameToLookup = originalBeanName(name);
    if (parentBeanFactory instanceof AbstractBeanFactory) {
        return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
                nameToLookup, requiredType, args, typeCheckOnly);
    } else if (args != null) {
        return (T) parentBeanFactory.getBean(nameToLookup, args);
    } else if (requiredType != null) {
        return parentBeanFactory.getBean(nameToLookup, requiredType);
    } else {
        return (T) parentBeanFactory.getBean(nameToLookup);
    }
}

dependsOn Handling

After merging the bean definition, Spring resolves any dependsOn annotations. Here’s how the process works:


RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
    for (String dep : dependsOn) {
        if (isDependent(beanName, dep)) {
            throw new BeanCreationException(...);
        }
        registerDependentBean(dep, beanName);
        getBean(dep); // Create dependency first
    }
}

This mechanism does not resolve circular dependencies but uses two maps to track dependencies:

  • dependentBeanMap: tracks which beans depend on others
  • dependenciesForBeanMap: tracks what beans a given bean depends on

Singleton Creation

For singleton beans, the process involves calling getSingleton with a lambda expression that delegates to createBean:


sharedInstance = getSingleton(beanName, () -> {
    try {
        return createBean(beanName, mbd, args);
    } catch (BeansException ex) {
        destroySingleton(beanName);
        throw ex;
    }
});

Prototype Creation

Prototype beans are handled differently:


if (mbd.isPrototype()) {
    Object prototypeInstance = null;
    try {
        beforePrototypeCreation(beanName);
        prototypeInstance = createBean(beanName, mbd, args);
    } finally {
        afterPrototypeCreation(beanName);
    }
    beanInstance = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

Other Scopes

Spring supports various scope annotations like @RequestScope, @SessionScope, etc., which are essentially aliases for the base @Scope annotation:


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {
    @AliasFor(annotation = Scope.class)
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

Each scope is managed through a custom Scope implementation. For example, session-scoped beans are stored in HTTP session attributes:


Object scopedInstance = scope.get(beanName, () -> {
    beforePrototypeCreation(beanName);
    try {
        return createBean(beanName, mbd, args);
    } finally {
        afterPrototypeCreation(beanName);
    }
});

The scope's get method retrieves or stores the bean in the appropriate scope context (e.g., HTTP session), ensuring proper lifecycle management.

Conclusion

The getBean method primarily follows these steps:

  1. First, attempt to retrieve the bean from the singleton cache.
  2. If not found, check for the bean definition in the current container; if missing, delegate to the parent.
  3. Once the definition is located, create the bean instance according to its scope.
  4. Finally, return the constructed bean instance.

This overview helps clarify the internal flow of getBean, aiding further understanding of createBean's behavior. For additional questions or clarifications, feel free to reach out.

Tags: Spring Bean Lifecycle Dependency Injection scopes singleton

Posted on Fri, 11 Sep 2026 16:31:00 +0000 by nihal