DEV Community

Silver_dev
Silver_dev

Posted on

Spring Boot Under the Hood. Part 6: Bean Lifecycle — Born, Post-Processed, Destroyed

By this point we have:

  • A fully built ApplicationContext (Part 3)
  • BeanDefinitions registered by scanning (Part 4)
  • Auto-configurations loaded (Part 5)

Now refresh() has to turn all those BeanDefinitions into living objects — beans. Let's go through it step by step.

6.1. BeanDefinition — the Bean's Blueprint

Before we talk about the lifecycle, we need to understand what a BeanDefinition is.

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
    String getBeanClassName();
    String getScope();
    boolean isLazyInit();
    boolean isPrimary();
    String[] getDependsOn();
    boolean isAutowireCandidate();
    ConstructorArgumentValues getConstructorArgumentValues();
    MutablePropertyValues getPropertyValues();
    String getInitMethodName();
    String getDestroyMethodName();
    int getRole();
    // ...
}
Enter fullscreen mode Exit fullscreen mode

A BeanDefinition is metadata about a bean, not the bean itself. From it, Spring learns:

  • Which class to instantiate
  • Which scope (singleton / prototype / request / session)
  • What to inject (constructor args / property values)
  • Which init/destroy method to call
  • Whether it's lazy or not

The implementation hierarchy:

Class When it's used
RootBeanDefinition The main implementation (the merge result)
ChildBeanDefinition Legacy, for parent-child definitions
GenericBeanDefinition The modern one (used in configurations)
AnnotatedGenericBeanDefinition For annotated classes (@Configuration, @Component)
ScannedGenericBeanDefinition For classes found by scanning
ConfigurationClassBeanDefinition For @Bean methods

A key feature: a BeanDefinition can act as a parent for other BeanDefinitions. When creating a bean, Spring performs a merge — combining parent and child into a RootBeanDefinition. This is called a MergedBeanDefinition.

6.2. The Full Bean Lifecycle — an Overview

Here is the complete scheme. We'll break down every step:

1.  BeanDefinition registered (scanning / @Bean / .imports)
2.  BeanFactoryPostProcessor.postProcessBeanFactory()
3.  BeanPostProcessor registration
4.  getBean() is called
5.  ─── for singletons ───
6.  MergedBeanDefinition is created (parent/child merge)
7.  InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()
8.  InstantiationAwareBeanPostProcessor.determineCandidateConstructors()
9.  Constructor invoked (or factory method)
10. MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition()
11. InstantiationAwareBeanPostProcessor.postProcessAfterInstantiation()
12. InstantiationAwareBeanPostProcessor.postProcessProperties()
       └─ @Autowired / @Value / @Resource
13. Aware interfaces: BeanNameAware, BeanClassLoaderAware, BeanFactoryAware
       └─ ApplicationContextAware, EnvironmentAware, ...
14. BeanPostProcessor.postProcessBeforeInitialization()
       └─ @PostConstruct (CommonAnnotationBeanPostProcessor)
15. InitializingBean.afterPropertiesSet()
16. @Bean(initMethod="...")
17. BeanPostProcessor.postProcessAfterInitialization()
       └─ AOP proxy goes here (AbstractAutoProxyCreator)
18. ─── singleton cache ───
19. Bean ready to use
20. ─── at shutdown ───
21. DestructionAwareBeanPostProcessor.postProcessBeforeDestruction()
       └─ @PreDestroy
22. DisposableBean.destroy()
23. @Bean(destroyMethod="...")
Enter fullscreen mode Exit fullscreen mode

6.3. BeanFactoryPostProcessor — Working with Metadata

This is the first stage. A BeanFactoryPostProcessor (BFPP) works with BeanDefinitions — before any beans are created.

@FunctionalInterface
public interface BeanFactoryPostProcessor {
    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory);
}
Enter fullscreen mode Exit fullscreen mode

It's invoked by AbstractApplicationContext.invokeBeanFactoryPostProcessors(). The order:

  1. BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry() — registers new BeanDefinitions
  2. BeanFactoryPostProcessor.postProcessBeanFactory() — modifies existing ones

The main BFPP in Spring is ConfigurationClassPostProcessor. It:

  • Processes @Configuration, @Bean, @ComponentScan, @Import, @EnableAutoConfiguration
  • Registers BeanDefinitions for all the beans it finds

This is where Parts 3–4 take place. ConfigurationClassPostProcessor has the highest precedence (Ordered.HIGHEST_PRECEDENCE), so it runs first.

Your own BFPP:

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        BeanDefinition bd = beanFactory.getBeanDefinition("myService");
        bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
    }
}
Enter fullscreen mode Exit fullscreen mode

Registration: @Component or a @Bean method. Don't inject other beans into it — at this stage they don't exist yet.

6.4. BeanPostProcessor — Working with Beans

A BeanPostProcessor (BPP) is invoked for every bean as it's being created.

public interface BeanPostProcessor {
    default Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean;
    }
    default Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }
}
Enter fullscreen mode Exit fullscreen mode

Two methods — before and after initialization. This is where all of Spring's magic is built: @Autowired, @PostConstruct, AOP proxies, @Transactional.

The BPP hierarchy

BeanPostProcessor
  ├─ InstantiationAwareBeanPostProcessor
       ├─ postProcessBeforeInstantiation()   before the bean is created
       ├─ postProcessAfterInstantiation()    after creation, before injection
       └─ postProcessProperties()            dependency injection
  
  ├─ SmartInstantiationAwareBeanPostProcessor
       └─ determineCandidateConstructors()   constructor selection
  
  ├─ MergedBeanDefinitionPostProcessor
       └─ postProcessMergedBeanDefinition()  collecting metadata for injection
  
  └─ DestructionAwareBeanPostProcessor
        └─ postProcessBeforeDestruction()     @PreDestroy
Enter fullscreen mode Exit fullscreen mode

Built-in Spring BPPs

BPP What it does Order
AutowiredAnnotationBeanPostProcessor @Autowired, @Value, @Inject Ordered.LOWEST_PRECEDENCE - 2
CommonAnnotationBeanPostProcessor @PostConstruct, @PreDestroy, @Resource Ordered.LOWEST_PRECEDENCE - 3
ConfigurationPropertiesBindingPostProcessor @ConfigurationProperties Ordered.HIGHEST_PRECEDENCE + 1
ApplicationListenerDetector Registers ApplicationListener beans LOWEST_PRECEDENCE
AbstractAutoProxyCreator AOP proxies (@Transactional, @Async, @Cacheable) depends on the implementation

Important: BPP ordering is critical. For example, @PostConstruct must run before AOP proxying — otherwise the @PostConstruct method would be invoked on the proxy, not on the target object.

6.5. Bean Creation — a Step-by-Step Breakdown

Let's walk through doCreateBean() from AbstractAutowireCapableBeanFactory — the central bean-creation method.

Step 1: createBean() — the entry point

@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) {
    // 1. Give BPPs a chance to return a proxy instead of the real bean
    Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
    if (bean != null) {
        return bean;
    }

    // 2. Actual creation
    return doCreateBean(beanName, mbdToUse, args);
}
Enter fullscreen mode Exit fullscreen mode

Step 2: resolveBeforeInstantiation() — the early proxy

protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
    Object bean = null;
    if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            Class<?> targetType = determineTargetType(beanName, mbd);
            bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
            if (bean != null) {
                bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
            }
        }
    }
    return bean;
}
Enter fullscreen mode Exit fullscreen mode

InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation() can return a ready-made object — in which case the regular creation cycle is skipped entirely. This is used, for example, for @Configuration proxies and in some AOP scenarios.

Step 3: doCreateBean() — the core logic

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
    // 1. Instantiate (constructor / factory method)
    BeanWrapper instanceWrapper = null;
    if (mbd.isSingleton()) {
        instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    if (instanceWrapper == null) {
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    Object bean = instanceWrapper.getWrappedInstance();

    // 2. MergedBeanDefinitionPostProcessor
    applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);

    // 3. Early registration (for circular dependencies)
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences 
        && isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // 4. Populate — inject dependencies
    Object exposedObject = bean;
    populateBean(beanName, mbd, instanceWrapper);

    // 5. Initialize
    exposedObject = initializeBean(beanName, exposedObject, mbd);

    // 6. Circular dependency check
    if (earlySingletonExposure) {
        Object earlySingletonReference = getSingleton(beanName, false);
        // ...
    }

    return exposedObject;
}
Enter fullscreen mode Exit fullscreen mode

Step 4: createBeanInstance() — choosing the constructor

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {
    // 1. If there's a factory method — use it
    if (mbd.getFactoryMethodName() != null) {
        return instantiateUsingFactoryMethod(beanName, mbd, args);
    }

    // 2. Determine the constructor
    Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
    if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR 
            || mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
        return autowireConstructor(beanName, mbd, ctors, args);
    }

    // 3. The default constructor
    return instantiateBean(beanName, mbd);
}
Enter fullscreen mode Exit fullscreen mode

determineConstructorsFromBeanPostProcessors() calls SmartInstantiationAwareBeanPostProcessor.determineCandidateConstructors(). This is where AutowiredAnnotationBeanPostProcessor finds the @Autowired constructor (or the single constructor).

Step 5: populateBean() — injecting dependencies

protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
    // 1. postProcessAfterInstantiation — a BPP can veto injection
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
            if (!bp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
                return;
            }
        }
    }

    // 2. postProcessProperties — the injection itself
    PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);
    if (pvs != null || hasInstantiationAwareBeanPostProcessors()) {
        for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) {
            PropertyValues pvsToUse = bp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
            // ...
        }
    }

    // 3. applyPropertyValues — setting values (for XML configs)
    if (pvs != null) {
        applyPropertyValues(beanName, mbd, bw, pvs);
    }
}
Enter fullscreen mode Exit fullscreen mode

This is where AutowiredAnnotationBeanPostProcessor.postProcessProperties() finds @Autowired fields and setters and injects them.

Step 6: initializeBean() — initialization

protected Object initializeBean(String beanName, Object bean, RootBeanDefinition mbd) {
    // 1. Aware interfaces
    invokeAwareMethods(beanName, bean);   // BeanNameAware, BeanClassLoaderAware, BeanFactoryAware

    // 2. postProcessBeforeInitialization
    Object wrappedBean = bean;
    if (mbd == null || !mbd.isSynthetic()) {
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
    }

    // 3. init-method
    try {
        invokeInitMethods(beanName, wrappedBean, mbd);
    } catch (Throwable ex) {
        throw new BeanCreationException(...);
    }

    // 4. postProcessAfterInitialization
    if (mbd == null || !mbd.isSynthetic()) {
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
    }

    return wrappedBean;
}
Enter fullscreen mode Exit fullscreen mode

invokeAwareMethods()

private void invokeAwareMethods(String beanName, Object bean) {
    if (bean instanceof Aware) {
        if (bean instanceof BeanNameAware bna) {
            bna.setBeanName(beanName);
        }
        if (bean instanceof BeanClassLoaderAware bcla) {
            ClassLoader bcl = getBeanClassLoader();
            if (bcl != null) bcla.setBeanClassLoader(bcl);
        }
        if (bean instanceof BeanFactoryAware bfa) {
            bfa.setBeanFactory(this);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

These are only three of the Aware interfaces. The rest (ApplicationContextAware, EnvironmentAware, ResourceLoaderAware, ApplicationEventPublisherAware, MessageSourceAware) are handled by ApplicationContextAwareProcessor — a BeanPostProcessor registered by the context.

applyBeanPostProcessorsBeforeInitialization()

This is where CommonAnnotationBeanPostProcessor.postProcessBeforeInitialization() runs — the one that invokes @PostConstruct methods.

The ordering of multiple BPPs follows @Order / Ordered. CommonAnnotationBeanPostProcessor has Ordered.LOWEST_PRECEDENCE - 3, so @PostConstruct runs later than other beforeInitialization callbacks but earlier than AOP proxying (which happens in afterInitialization).

invokeInitMethods()

protected void invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd) {
    boolean isInitializingBean = (bean instanceof InitializingBean);
    if (isInitializingBean && (mbd == null || !mbd.hasAnyExternallyManagedInitMethod("afterPropertiesSet"))) {
        ((InitializingBean) bean).afterPropertiesSet();
    }
    if (mbd != null && bean.getClass() != NullBean.class) {
        String initMethodName = mbd.getInitMethodName();
        if (StringUtils.hasLength(initMethodName) 
                && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName))
                && !mbd.hasAnyExternallyManagedInitMethod(initMethodName)) {
            invokeCustomInitMethod(beanName, bean, mbd);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The order: InitializingBean.afterPropertiesSet()@Bean(initMethod=...). @PostConstruct was already invoked earlier, in postProcessBeforeInitialization.

The final initialization order:

  1. @PostConstruct
  2. InitializingBean.afterPropertiesSet()
  3. @Bean(initMethod = "...")

applyBeanPostProcessorsAfterInitialization()

This is where the AOP proxy is created. AbstractAutoProxyCreator.postProcessAfterInitialization() checks whether the bean carries @Transactional, @Async, @Cacheable, etc., and if so, wraps it in a proxy (CGLIB or a JDK dynamic proxy).

Key point: after afterInitialization, it's the proxy — not the original object — that goes into the singleton cache. All subsequent @Autowired injections will receive the proxy.

6.6. Call Order — a Summary Table

For a MyService bean with the full set of callbacks:

# Callback Invoked by
1 Constructor createBeanInstance()
2 @Autowired fields/setters AutowiredAnnotationBeanPostProcessor
3 BeanNameAware.setBeanName() invokeAwareMethods()
4 BeanFactoryAware.setBeanFactory() invokeAwareMethods()
5 ApplicationContextAware.setApplicationContext() ApplicationContextAwareProcessor
6 @PostConstruct CommonAnnotationBeanPostProcessor
7 InitializingBean.afterPropertiesSet() invokeInitMethods()
8 @Bean(initMethod) invokeInitMethods()
9 AOP proxy AbstractAutoProxyCreator
10 Bean ready Singleton cache
11 @PreDestroy CommonAnnotationBeanPostProcessor
12 DisposableBean.destroy() DisposableBeanAdapter
13 @Bean(destroyMethod) DisposableBeanAdapter

6.7. Circular Dependencies

The problem:

A → B → A
Enter fullscreen mode Exit fullscreen mode

If A needs B and B needs A — the classic deadlock.

Spring's solution: the three-level cache.

public class DefaultSingletonBeanRegistry {
    // Level 1: fully initialized singleton beans
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    // Level 2: early singletons (before populate/init)
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

    // Level 3: singleton factories (lambdas producing early references)
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
}
Enter fullscreen mode Exit fullscreen mode

The getSingleton() algorithm:

  1. Check singletonObjects → if present, return it.
  2. Check earlySingletonObjects → if present, return it.
  3. Check singletonFactories → if present, invoke the factory, put the result into earlySingletonObjects, return it.
  4. Otherwise — create the bean.

An example:

  • We start creating A → put an ObjectFactory into level 3.
  • Injecting B into A → we start creating B.
  • B needs AgetSingleton("A") → level 3 → early reference → returns the half-finished A.
  • B finishes initializing completely.
  • We resume initializing AA is ready.

Limitations:

  • Works for singletons only.
  • Doesn't work with constructor injection — neither bean can be fully created through a constructor, because the constructor runs before the bean is registered in the cache. The fix: @Lazy on one of the parameters.
  • Spring Boot 2.6+ bans circular references by default. Re-enable via spring.main.allow-circular-references=true.

6.8. @lazy — Deferred Initialization

@Service
public class A {
    @Autowired
    @Lazy
    private B b;
}
Enter fullscreen mode Exit fullscreen mode

Spring injects a proxy for B; the real bean is created on first access. This sidesteps circular dependencies.

@Lazy on a @Configuration class makes the entire configuration lazy. @Lazy on a @Bean method makes that bean lazy.

6.9. ObjectProvider and @Lookup

ObjectProvider

@Service
public class MyService {
    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public void doWork() {
        PrototypeBean bean = prototypeBeanProvider.getObject();  // a new one every time
    }
}
Enter fullscreen mode Exit fullscreen mode

An ObjectProvider is a lazy resolver. getObject() asks the container for the bean every time. It works for the prototype scope too.

@Lookup

@Service
public abstract class MyService {
    @Lookup
    protected abstract PrototypeBean createPrototypeBean();
}
Enter fullscreen mode Exit fullscreen mode

Spring generates a subclass (CGLIB) that overrides the method and asks the container for the bean. An older approach — ObjectProvider is preferred.

6.10. Bean Destruction

registerShutdownHook()

Called from SpringApplication.refreshContext():

private void refreshContext(ConfigurableApplicationContext context) {
    if (this.registerShutdownHook) {
        try {
            context.registerShutdownHook();
        } catch (AccessControlException ex) { }
    }
    refresh(context);
}
Enter fullscreen mode Exit fullscreen mode

registerShutdownHook() adds a JVM shutdown hook that will call context.close() when the JVM terminates.

close()doClose()

protected void doClose() {
    // 1. LifecycleProcessor.onClose() — stops Lifecycle beans
    if (this.lifecycleProcessor != null) {
        this.lifecycleProcessor.onClose();
    }

    // 2. Destroy the beans
    destroyBeans();

    // 3. Close the BeanFactory
    closeBeanFactory();

    // 4. onClose()
    onClose();

    // 5. Reset the listeners
    if (this.earlyApplicationListeners != null) {
        this.applicationListeners.clear();
        this.applicationListeners.addAll(this.earlyApplicationListeners);
    }

    // 6. active = false
    this.active.set(false);
}
Enter fullscreen mode Exit fullscreen mode

destroyBeans()destroySingleton()

public void destroySingleton(String beanName) {
    removeSingleton(beanName);
    DisposableBean disposableBean = this.disposableBeans.remove(beanName);
    if (disposableBean != null) {
        disposableBean.destroy();
    }
}
Enter fullscreen mode Exit fullscreen mode

DisposableBeanAdapter.destroy() calls, in order:

  1. DestructionAwareBeanPostProcessor.postProcessBeforeDestruction()@PreDestroy
  2. DisposableBean.destroy()
  3. @Bean(destroyMethod) or AutoCloseable.close()

Important: Spring automatically calls close() on beans implementing AutoCloseable / Closeable unless you say otherwise. Disable it via @Bean(destroyMethod = "").

6.11. SmartLifecycle and Lifecycle

Lifecycle is an interface for beans with start/stop phases:

public interface Lifecycle {
    void start();
    void stop();
    boolean isRunning();
}
Enter fullscreen mode Exit fullscreen mode

SmartLifecycle extends it:

public interface SmartLifecycle extends Lifecycle, Phased {
    boolean isAutoStartup();
    void stop(Runnable callback);
    int getPhase();
}
Enter fullscreen mode Exit fullscreen mode

The LifecycleProcessor (by default, DefaultLifecycleProcessor) drives:

  • onRefresh() — calls start() on all beans with isAutoStartup(), ordered by getPhase() (ascending)
  • onClose() — calls stop() on all of them, in descending getPhase() order

Example: WebServerStartStopLifecycle — starts/stops Tomcat.

6.12. Spring Boot 3 vs Spring Boot 4

Aspect Spring Boot 3 Spring Boot 4
Bean lifecycle Unchanged Unchanged
BeanPostProcessor The same interfaces The same interfaces
Circular references Banned by default (since 2.6) Banned
AOT Present, but opt-in More aggressive, reflection-free by default
@ConstructorBinding Optional Legacy
BeanRegistrar Introduced in Spring Framework 7 The primary registration approach for AOT

With AOT (Boot 3.x / 4.x), Spring generates code that registers BeanDefinitions programmatically, without reflection. @PostConstruct, @Autowired, and other callbacks turn into direct code calls. It's faster and compatible with GraalVM native images.

6.13. Diagram: the Complete Bean Lifecycle

BeanDefinition
    │
    ▼
BeanFactoryPostProcessor.postProcessBeanFactory()   ← works with metadata
    │
    ▼
BeanPostProcessors registered in the context
    │
    ▼
getBean(beanName)
    │
    ├─ in singletonObjects?  → yes → return (already built)
    │
    ▼
createBean()
    │
    ├─ resolveBeforeInstantiation()
    │     └─ InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation()
    │           └─ if it returned an object → return (shortcut)
    │
    ▼
doCreateBean()
    │
    ├─ 1. createBeanInstance()
    │       ├─ determineCandidateConstructors()    ← the @Autowired constructor
    │       └─ new Instance() / factory method
    │
    ├─ 2. applyMergedBeanDefinitionPostProcessors()
    │       └─ @Autowired metadata is collected
    │
    ├─ 3. addSingletonFactory()                    ← early cache (for circulars)
    │
    ├─ 4. populateBean()
    │       ├─ postProcessAfterInstantiation()
    │       └─ postProcessProperties()             ← @Autowired / @Value
    │
    ├─ 5. initializeBean()
    │       ├─ invokeAwareMethods()                ← BeanNameAware, BeanFactoryAware
    │       ├─ postProcessBeforeInitialization()
    │       │     ├─ ApplicationContextAwareProcessor
    │       │     └─ CommonAnnotationBeanPostProcessor → @PostConstruct
    │       ├─ invokeInitMethods()
    │       │     ├─ InitializingBean.afterPropertiesSet()
    │       │     └─ @Bean(initMethod)
    │       └─ postProcessAfterInitialization()
    │             └─ AbstractAutoProxyCreator → AOP proxy
    │
    ├─ 6. addSingleton()                           ← the finished bean goes into the cache
    │
    ▼
Bean ready
    │
    ▼ (at shutdown)
destroySingleton()
    ├─ DestructionAwareBeanPostProcessor.postProcessBeforeDestruction()
    │     └─ @PreDestroy
    ├─ DisposableBean.destroy()
    └─ @Bean(destroyMethod) / AutoCloseable.close()
Enter fullscreen mode Exit fullscreen mode

6.14. Key Takeaways

  1. A BeanDefinition is metadata, not a bean. From it, Spring learns everything about the future object.
  2. A BeanFactoryPostProcessor works with metadata before beans are created. The main BFPP is ConfigurationClassPostProcessor.
  3. A BeanPostProcessor works with beans. Every bean passes through the BPP chain.
  4. The initialization order: @PostConstructInitializingBean.afterPropertiesSet()@Bean(initMethod).
  5. The AOP proxy is created in postProcessAfterInitialization(). After that, the singleton cache holds the proxy, not the original.
  6. Circular dependencies are resolved via the three-level cache — but only for singletons and never for constructor injection.
  7. @Lazy and ObjectProvider are the ways around circular dependencies.
  8. Destruction: @PreDestroyDisposableBean.destroy()@Bean(destroyMethod)AutoCloseable.close().
  9. SmartLifecycle is for beans with start/stop phases (e.g., the web server).
  10. Boot 4 + AOT: the lifecycle turns into generated code, reflection is minimized.

Top comments (0)