namalzi

← All posts

Digging into Spring's Source 6: Lifecycle Callbacks and PostProcessors — Where You Can Step into the Container

Spring · Published

When exactly, and in what order, are BeanFactoryPostProcessor, BeanPostProcessor and its sub-interfaces, the Aware interfaces, @PostConstruct, InitializingBean, init-method, @PreDestroy, DisposableBean, destroy-method, SmartInitializingSingleton and SmartLifecycle called? Checked against the real source and real program output, with a look at why there are so many different hooks.

What this post covers

This is the sixth post in the "Digging into Spring's Source" series. Part 4 followed a bean through its three creation steps (instantiate, inject, initialise), and Part 5 looked at how the bean for an @Autowired spot is chosen. This time we collect all the places where you can step into that flow. A great many Spring features hang off these spots.

There are three questions to answer.

Item Details
Source Spring Framework 7.1.0-SNAPSHOT
Commit f447f3c310 (2026-10-01, main branch)
Modules read most spring-beans, spring-context
Classes read most PostProcessorRegistrationDelegate, AbstractAutowireCapableBeanFactory, ApplicationContextAwareProcessor, DisposableBeanAdapter, DefaultLifecycleProcessor

Every line number and every detail in this post refers to that commit. In other versions the line numbers will differ and some details may too. Part 1 explains how to get the source at the same commit.

This post keeps to the main thread that someone new to Spring can follow. What was left out is only named at the end, under "What this post leaves out".

Words to know first

A callback is a method you hand over with the instruction "when this happens, call this". Your code does not call Spring; Spring calls your code. Every spot in this post is a callback.

A post-processor is an object that steps in right after the container has made something and touches it up. One that edits blueprints (BeanDefinition) is a BeanFactoryPostProcessor; one that touches up the bean objects being built is a BeanPostProcessor. This distinction came up once in Part 2.

Aware is the shared name of a family of interfaces that say "container, tell me about ○○". Implement BeanNameAware and you receive your own bean name; implement ApplicationContextAware and you receive the container itself. They are tiny interfaces with one method each.

Initialisation and destruction callbacks are methods called once when a bean is fully built and once when it is thrown away. There are three ways to declare each: an annotation (@PostConstruct, @PreDestroy), an interface (InitializingBean, DisposableBean) or configuration (@Bean(initMethod=…), destroyMethod=…). Section 5 looks at why there are three.

Ordered and PriorityOrdered are interfaces that say "call me earlier". The number returned by getOrder() wins when it is smaller (Ordered lines 49 and 55). PriorityOrdered is special treatment: regardless of the number, it always comes before plain Ordered (PriorityOrdered lines 20–28).

A phase is the number SmartLifecycle uses to decide start and stop order. It appears in section 6.

1. Why so many places to step in — one map

refresh(), which switches the container on, has twelve steps (AbstractApplicationContext lines 582–626; Part 7 goes through them one by one). The spots in this post are scattered along those steps. Let us open the map first.

The hooks laid over the flow of refresh. prepareBeanFactory registers ApplicationContextAwareProcessor, invokeBeanFactoryPostProcessors edits blueprints, registerBeanPostProcessors sorts the post-processors, finishBeanFactoryInitialization builds beans while calling BPP, Aware and init callbacks, then afterSingletonsInstantiated and SmartLifecycle.start in finishRefresh follow. In close(), SmartLifecycle.stop comes first and then destroySingletons calls the destruction callbacks. Every blue box is a place to step in. Time runs from top to bottom.

There are many spots because what they handle and when they run differ. Grouped in a table, there are five families.

Family What it handles When it is called Main interfaces
Blueprint post-processing BeanDefinition (no object yet) Before any bean is built, once BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor
Bean post-processing The bean object being built Per bean, at several points of the creation flow BeanPostProcessor and its four sub-interfaces
The bean's own callbacks That bean itself When that bean is built, and when it is thrown away Aware family, InitializingBean, DisposableBean, @PostConstruct, @PreDestroy
All-done callback Every singleton After all singletons exist, once SmartInitializingSingleton
Start and stop Long-running work (threads, connections) After the container is up, and just before it closes Lifecycle, SmartLifecycle

"Blueprint vs object", "per bean vs after everything", "building vs running" each need their own place. Now we go down from the top.

2. The blueprint stage — BeanFactoryPostProcessor

The fifth step of refresh(), invokeBeanFactoryPostProcessors, hands the work to a class called PostProcessorRegistrationDelegate (AbstractApplicationContext lines 795–796). It is a final class with a private constructor, a helper made only of static methods (lines 62–65).

There are two interfaces.

The reason for splitting them is in the calling order. The registering kind has to finish first so that the editing kind sees every blueprint. Here is the skeleton of invokeBeanFactoryPostProcessors. (A // ... line marks something cut for this post; everything else is the source as it is.)

			// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.
			String[] postProcessorNames =
					beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
			for (String ppName : postProcessorNames) {
				if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
					currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
					processedBeans.add(ppName);
				}
			}
			sortPostProcessors(currentRegistryProcessors, beanFactory);
			// ...
			invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup());
			// ...

			// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
			// ...

			// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
			boolean reiterate = true;
			while (reiterate) {
				// ...
			}

			// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
			invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
			invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);

spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java, lines 107–154

The order in which BeanFactoryPostProcessors are called. Those registered in code, then BDRPP beans that are PriorityOrdered, Ordered and the rest (repeated until no new ones appear), then postProcessBeanFactory of those BDRPPs, then BFPP beans that are PriorityOrdered, Ordered and the rest, and finally clearMetadataCache drops the merged-definition cache. The same pattern twice. The registering kind (②–④) finishes before the editing kind (⑥–⑧) runs.

The main thread is this.

  1. BeanDefinitionRegistryPostProcessors are found and called in the order PriorityOrdered → Ordered → the rest. Notice that each group looks them up again with getBeanNamesForType (lines 108, 122, 138). A blueprint registered by an earlier group may itself be another post-processor. The last group loops with while until nothing new appears (lines 135–150).
  2. The postProcessBeanFactory of everything collected so far is called (lines 152–154).
  3. Now the plain BeanFactoryPostProcessors are called in the same three groups (lines 164–204). Names already handled in step 1 are filtered out through processedBeans so nothing is called twice.
  4. Finally clearMetadataCache() drops the cache of merged blueprints (lines 206–208). A post-processor may have edited an original blueprint, so the merged copies from Part 3 must be rebuilt.

At the top of the method sits a warning comment: the many lists and many passes look like they could be refactored, but they are intentional, so do not (lines 71–82). The promise includes that a PriorityOrdered post-processor is created before an Ordered one. Post-processors are beans too, built with getBean(), and if that order changed, which beans get touched by which post-processors would change with it.

Example: registering and editing a blueprint

One post-processor of each kind, plus a greeter blueprint registered directly on the container before refresh().

// An example written for this post (not Spring's source)
    @Configuration
    static class Config {

        /** The registering kind. Called before any other BeanFactoryPostProcessor. */
        @Bean static BeanDefinitionRegistryPostProcessor registrar() {
            return registry -> {
                System.out.println("  [BDRPP] postProcessBeanDefinitionRegistry: registering extraGreeter");
                BeanDefinition extra = BeanDefinitionBuilder.genericBeanDefinition(Greeter.class)
                        .addPropertyValue("message", "value written by BDRPP").getBeanDefinition();
                registry.registerBeanDefinition("extraGreeter", extra);
            };
        }

        /** The editing kind. No object exists yet. */
        @Bean static BeanFactoryPostProcessor rewriter() {
            return beanFactory -> {
                System.out.println("  [BFPP] postProcessBeanFactory: definitions=" + beanFactory.getBeanDefinitionCount()
                        + ", singletons created so far=" + java.util.Arrays.toString(beanFactory.getSingletonNames()));
                BeanDefinition greeter = beanFactory.getBeanDefinition("greeter");
                System.out.println("  [BFPP] greeter.message before: " + greeter.getPropertyValues().get("message"));
                greeter.getPropertyValues().add("message", "changed by BFPP");
            };
        }
    }

In main, the blueprint goes in with context.registerBeanDefinition("greeter", …addPropertyValue("message", "original value")…), then refresh() runs and both beans' message is printed. The output:

[refresh]
  [BDRPP] postProcessBeanDefinitionRegistry: registering extraGreeter
  [BFPP] postProcessBeanFactory: definitions=11, singletons created so far=[environment, systemProperties, systemEnvironment, applicationStartup, org.springframework.context.annotation.internalConfigurationAnnotationProcessor, org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry, registrar, org.springframework.context.event.internalEventListenerProcessor, rewriter, org.springframework.context.event.internalEventListenerFactory]
  [BFPP] greeter.message before: original value
  Greeter constructor (the bean is actually created now)
  Greeter constructor (the bean is actually created now)
[getBean]
  greeter.message = changed by BFPP
  extraGreeter.message = value written by BDRPP

This example was run on the released Spring Framework 7.0.9 and produced the output above (the 7.1.0-SNAPSHOT this series reads was not built and run directly). The full code is in ex/u04/FactoryPostProcessorExample.java.

Easy to get wrong

3. Lining up the post-processors — registerBeanPostProcessors

The sixth step, registerBeanPostProcessors, finds the BeanPostProcessor beans and only puts them into the container's list. They are actually called in section 4, when beans are built. This is the second static method of the same class.

		// First, register the BeanPostProcessors that implement PriorityOrdered.
		sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
		registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

		// Next, register the BeanPostProcessors that implement Ordered.
		// ...
		sortPostProcessors(orderedPostProcessors, beanFactory);
		registerBeanPostProcessors(beanFactory, orderedPostProcessors);

		// Now, register all regular BeanPostProcessors.
		// ...
		registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);

		// Finally, re-register all internal BeanPostProcessors.
		sortPostProcessors(internalPostProcessors, beanFactory);
		registerBeanPostProcessors(beanFactory, internalPostProcessors);

		// Re-register post-processor for detecting inner beans as ApplicationListeners,
		// moving it to the end of the processor chain (for picking up proxies etc).
		beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
	}

spring-context/src/main/java/org/springframework/context/support/PostProcessorRegistrationDelegate.java, lines 258–292

Same PriorityOrdered → Ordered → the rest order as section 2, but with two more lines at the end. The "internal" post-processors are re-registered, and so is ApplicationListenerDetector. Internal ones are those that implement MergedBeanDefinitionPostProcessor (lines 246–248). Both CommonAnnotationBeanPostProcessor, which handles @PostConstruct, and AutowiredAnnotationBeanPostProcessor, which handles @Autowired, fall into this group (InitDestroyAnnotationBeanPostProcessor lines 87–88, AutowiredAnnotationBeanPostProcessor lines 158–159).

What "re-register" means becomes clear in the method that adds to the list.

	@Override
	public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) {
		Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null");
		synchronized (this.beanPostProcessors) {
			// Remove from old position, if any
			this.beanPostProcessors.remove(beanPostProcessor);
			// Add to end of list
			this.beanPostProcessors.add(beanPostProcessor);
		}
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java, lines 980–989

It is removed from where it was and added at the end. So the final list looks like this.

The final BeanPostProcessor list of an AnnotationConfigApplicationContext. ApplicationContextAwareProcessor, BeanPostProcessorChecker, PriorityOrdered beans (internal ones are moved later), Ordered beans (TracerA 10 then TracerB 20), beans with no order, the re-registered internal post-processors (CommonAnnotation, AutowiredAnnotation), and ApplicationListenerDetector at the very end. The red box is the trap. The post-processor that calls @PostConstruct is PriorityOrdered, yet it stands behind my own Ordered post-processors.

The ApplicationContextAwareProcessor at the front is not added in this step but in the earlier prepareBeanFactory, directly through addBeanPostProcessor (AbstractApplicationContext line 737). That is why it precedes every bean post-processor, and, as section 5 shows, why ApplicationContextAware is handled before any other post-processor.

BeanPostProcessorChecker — warning about beans built too early

At the start of this step a post-processor called BeanPostProcessorChecker is slipped in (lines 229–234). It counts in advance how many post-processors will be registered, and if a bean finishes initialising while the number registered is still below that total, it logs a warning that the bean "is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)" (lines 415–446). If building post-processor A needs bean B, B is built before A, and B is never touched by the post-processors registered after A. This is the situation the static advice of section 2 avoids (lines 429–433).

Easy to get wrong: the return type decides the order

This is where I actually got stuck while writing the example. I had a Tracer class implementing Ordered and declared it as @Bean static BeanPostProcessor tracerA(), with BeanPostProcessor as the return type. The one with order 20 was called before the one with 10. Changing the return type to Tracer gave the expected 10 → 20.

The reason is beanFactory.isTypeMatch(ppName, Ordered.class) in the source above (lines 243 and 250). The type of a bean that does not exist yet is judged from its blueprint alone, and for a blueprint made from a @Bean method the only known type is the method's return type. If that is BeanPostProcessor, there is no way to tell it is Ordered, so it lands in the "no order" group and is called in registration order. To get an order, write the concrete type as the return type.

4. Hooks while one bean is built — the BeanPostProcessor family

From here on, these are the spots that repeat for every bean. Part 4 showed the two hooks around initializeBean(); in fact there are more along the whole creation flow.

The order of hooks while one bean is built. postProcessBeforeInstantiation (shortcut), instantiation (determineCandidateConstructors), postProcessMergedBeanDefinition, postProcessAfterInstantiation and postProcessProperties inside populateBean, invokeAwareMethods, postProcessBeforeInitialization (ApplicationContextAware, @PostConstruct), afterPropertiesSet and init-method, postProcessAfterInitialization (proxy), and registerDisposableBeanIfNecessary. Blue boxes are where post-processors step in; the yellow box is where the bean is noted for destruction.

BeanPostProcessor itself has only two methods, both default, so you implement only what you need (lines 67–111). The remaining spots belong to four sub-interfaces that extend it.

Interface Added methods Where it is called (AbstractAutowireCapableBeanFactory) Example user
InstantiationAwareBeanPostProcessor (line 44) postProcessBeforeInstantiation createBean → resolveBeforeInstantiation (line 513, lines 1123–1139) Skip instantiation and hand out a proxy
postProcessAfterInstantiation Top of populateBean (lines 1415–1421) Return false to skip injection entirely
postProcessProperties Middle of populateBean (lines 1438–1449) @Autowired field and method injection (AutowiredAnnotationBeanPostProcessor line 490)
SmartInstantiationAwareBeanPostProcessor (line 37) determineCandidateConstructors Just before instantiation (line 1318) Choosing the @Autowired constructor (Part 5)
getEarlyBeanReference Inside the early-reference lambda (line 969) Making the proxy early for a circular reference (section 5 of Part 4)
MergedBeanDefinitionPostProcessor (line 38) postProcessMergedBeanDefinition Right after instantiation, once per bean (lines 573–584) Scanning the class for @Autowired and @PostConstruct spots ahead of time
DestructionAwareBeanPostProcessor (line 30) postProcessBeforeDestruction, requiresDestruction On destruction (DisposableBeanAdapter lines 198–202) Calling @PreDestroy

Let us look at only the first spot in code. It is the question createBean() asks before entering doCreateBean().

	protected @Nullable Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
		Object bean = null;
		if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
			// Make sure bean class is actually resolved at this point.
			if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
				Class<?> targetType = determineTargetType(beanName, mbd);
				if (targetType != null) {
					bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
					if (bean != null) {
						bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
					}
				}
			}
			mbd.beforeInstantiationResolved = (bean != null);
		}
		return bean;
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java, lines 1123–1139

If a post-processor returns a non-null object here, createBean() takes that object as the bean and stops (lines 513–516). No constructor, no injection, no init methods; only the after-initialisation hook runs once. As the Javadoc puts it, this is a shortcut for "suppressing default instantiation" and handing out a special proxy (InstantiationAwareBeanPostProcessor lines 46–53).

Why not one interface

The Javadoc of the two sub-interfaces says the same thing: this interface is "mainly for internal use within the framework"; implement the plain BeanPostProcessor as far as possible (InstantiationAwareBeanPostProcessor lines 34–36, SmartInstantiationAwareBeanPostProcessor lines 29–32). The door shown to users is kept small, the two hooks around initialisation, while the dangerous spots that replace instantiation or interfere with injection are set apart with a "use only if you know what you are doing".

The container benefits from the split as well. AbstractBeanFactory pre-filters the registered post-processors by kind and caches the result (lines 1024–1047; the cache class is at lines 2116–2125). Every call site in the table above loops over a filtered list such as getBeanPostProcessorCache().instantiationAware, not the whole list. These spots repeat for every bean, so the instanceof checks are not done each time.

5. The bean's own callbacks — Aware, initialisation, and the real order

The hooks of section 4 were places where someone else (a post-processor) touches the bean. This section is about the callbacks the bean itself receives. They all happen inside initializeBean() (lines 1809–1834; the body is in section 3 of Part 4).

Aware is handled in two places

The first line of initializeBean(), invokeAwareMethods, handles only three.

	private void invokeAwareMethods(String beanName, Object bean) {
		if (bean instanceof Aware) {
			if (bean instanceof BeanNameAware beanNameAware) {
				beanNameAware.setBeanName(beanName);
			}
			if (bean instanceof BeanClassLoaderAware beanClassLoaderAware) {
				ClassLoader bcl = getBeanClassLoader();
				if (bcl != null) {
					beanClassLoaderAware.setBeanClassLoader(bcl);
				}
			}
			if (bean instanceof BeanFactoryAware beanFactoryAware) {
				beanFactoryAware.setBeanFactory(AbstractAutowireCapableBeanFactory.this);
			}
		}
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java, lines 1836–1851

The rest, such as ApplicationContextAware and EnvironmentAware, are not here. The spring-beans module does not know what an ApplicationContext is (the layers of Part 2). So spring-context handles them with a BeanPostProcessor: the ApplicationContextAwareProcessor that section 3 said is registered at the front.

	@Override
	public @Nullable Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		if (bean instanceof Aware) {
			invokeAwareInterfaces(bean);
		}
		return bean;
	}

spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java, lines 82–88

invokeAwareInterfaces checks seven interfaces with instanceof, from EnvironmentAware to ApplicationContextAware, and sets each value (lines 90–112). The Javadoc of Aware says it too: merely implementing Aware provides no default functionality; the processing must be done explicitly, for example in a BeanPostProcessor (lines 25–29). Aware is only a marker; who reads the marker and fills in the value differs per module. The seven are handled in the order of the ifs, and the class Javadoc lists that order as well (lines 39–47).

The order of the three init methods

invokeInitMethods calls the interface way first and the configuration way second.

	protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd)
			throws Throwable {

		boolean isInitializingBean = (bean instanceof InitializingBean);
		if (isInitializingBean && (mbd == null || !mbd.hasAnyExternallyManagedInitMethod("afterPropertiesSet"))) {
			// ...
			((InitializingBean) bean).afterPropertiesSet();
		}

		if (mbd != null && bean.getClass() != NullBean.class) {
			String[] initMethodNames = mbd.getInitMethodNames();
			if (initMethodNames != null) {
				for (String initMethodName : initMethodNames) {
					if (StringUtils.hasLength(initMethodName) &&
							!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
							!mbd.hasAnyExternallyManagedInitMethod(initMethodName)) {
						invokeCustomInitMethod(beanName, bean, mbd, initMethodName);
					}
				}
			}
		}
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java, lines 1866–1889

The annotation way, @PostConstruct, is not in this method. CommonAnnotationBeanPostProcessor calls it in the before-initialisation hook (InitDestroyAnnotationBeanPostProcessor lines 214–226). CommonAnnotationBeanPostProcessor extends that class and, in its constructor, registers jakarta.annotation.PostConstruct and PreDestroy as the init and destroy annotations (lines 190–195). That is why @PostConstruct comes before afterPropertiesSet.

hasAnyExternallyManagedInitMethod in the code above is a guard against calling the same method twice. A method marked @PostConstruct is recorded in the blueprint as an init method "managed externally" (by a post-processor) during postProcessMergedBeanDefinition (lines 361–372). If the same method is also named as initMethod, invokeInitMethods skips it.

Example: implement everything and print the order

One bean implements every callback in this post, and two post-processors with different Ordered values (they are also DestructionAwareBeanPostProcessors) are registered. The essentials:

// An example written for this post (not Spring's source)
    static class Talkative implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware,
            ApplicationContextAware, InitializingBean, DisposableBean, SmartInitializingSingleton {

        Talkative() { System.out.println("  constructor"); }

        @Autowired
        void setHelper(Helper helper) { System.out.println("  @Autowired setter (dependency injection)"); }

        // ... the four Aware methods, @PostConstruct, afterPropertiesSet, customInit,
        //     afterSingletonsInstantiated, @PreDestroy, destroy and customDestroy each print one line
    }

    @Configuration
    static class Config {
        // The return type must be Tracer so the container can see Ordered (section 3, "Easy to get wrong")
        @Bean static Tracer tracerB() { return new Tracer("TracerB(order=20)", 20); }  // registered first
        @Bean static Tracer tracerA() { return new Tracer("TracerA(order=10)", 10); }  // smaller order
        @Bean Helper helper() { return new Helper(); }
        @Bean(initMethod = "customInit", destroyMethod = "customDestroy")
        Talkative talkative() { return new Talkative(); }
    }

Tracer prints its own name in postProcessBeforeInitialization, postProcessAfterInitialization and postProcessBeforeDestruction, for the talkative bean only. The container is created and then closed right away. The output:

[refresh]
  constructor
  @Autowired setter (dependency injection)
  BeanNameAware.setBeanName(talkative)
  BeanClassLoaderAware.setBeanClassLoader
  BeanFactoryAware.setBeanFactory
  ApplicationContextAware.setApplicationContext
  TracerA(order=10).postProcessBeforeInitialization
  TracerB(order=20).postProcessBeforeInitialization
  @PostConstruct
  InitializingBean.afterPropertiesSet
  @Bean(initMethod) customInit
  TracerA(order=10).postProcessAfterInitialization
  TracerB(order=20).postProcessAfterInitialization
  SmartInitializingSingleton.afterSingletonsInstantiated
[close]
  TracerA(order=10).postProcessBeforeDestruction
  TracerB(order=20).postProcessBeforeDestruction
  @PreDestroy
  DisposableBean.destroy
  @Bean(destroyMethod) customDestroy

This example was run on the released Spring Framework 7.0.9 and produced the output above (the 7.1.0-SNAPSHOT this series reads was not built and run directly). The full code is in ex/u04/LifecycleOrderExample.java.

Matching the output against the source:

  1. Constructor → @Autowired setter. Injection (populateBean) comes before initializeBean, exactly as in Part 4.
  2. BeanNameAware → BeanClassLoaderAware → BeanFactoryAware. The order of the ifs in invokeAwareMethods.
  3. ApplicationContextAware. Set by ApplicationContextAwareProcessor, the first post-processor in the before-initialisation hook.
  4. The before-initialisation hooks of TracerA → TracerB, and only then @PostConstruct. As the picture in section 3 showed, CommonAnnotationBeanPostProcessor is an internal post-processor and was moved to the end.
  5. afterPropertiesSet → customInit. The order in invokeInitMethods.
  6. The after-initialisation hooks, again TracerA → TracerB.
  7. afterSingletonsInstantiated is called separately, after every singleton exists (below).
  8. Destruction is postProcessBeforeDestruction (both Tracers, then @PreDestroy) → destroy() → customDestroy. Section 6 looks at it.

The [refresh] part of the example output, grouped by who makes each call. Up to injection (Part 4): constructor → @Autowired setter. invokeAwareMethods (spring-beans itself, only three): BeanNameAware → BeanClassLoaderAware → BeanFactoryAware. The before-initialization hook runs the post-processors in order: ① spring-context's ApplicationContextAwareProcessor, which fills in seven kinds from EnvironmentAware to ApplicationContextAware (in the example, ApplicationContextAware); ② TracerA(order=10) → TracerB(order=20); ③ CommonAnnotationBeanPostProcessor, an internal post-processor moved to the end, which calls @PostConstruct. Then invokeInitMethods: afterPropertiesSet → @Bean(initMethod) customInit. The after-initialization hook: TracerA → TracerB. Finally, after every singleton is made, preInstantiateSingletons calls SmartInitializingSingleton.afterSingletonsInstantiated Items 1–7 above, lined up in one column. Notice that @PostConstruct (the red box) comes after my own Ordered post-processors. The order is exactly the example's actual output.

The Javadoc of the BeanFactory interface lists this as the "standard order" (lines 70–98). It matches the output above.

Easy to get wrong: @PostConstruct is not "the first init hook"

People tend to remember only "@PostConstruct runs before afterPropertiesSet", but as the output shows it runs after the before-initialisation hook of any Ordered post-processor I registered myself. If your post-processor expects a bean whose @PostConstruct has already run, that is wrong. The other way round, inside @PostConstruct you can use the container received through ApplicationContextAware, because that one stands at the front.

After every singleton exists — SmartInitializingSingleton

preInstantiateSingletons() builds every singleton and then makes one more pass looking for SmartInitializingSingleton.

		// Trigger post-initialization callback for all applicable beans...
		for (String beanName : beanNames) {
			Object singletonInstance = getSingleton(beanName, false);
			if (singletonInstance instanceof SmartInitializingSingleton smartSingleton) {
				// ...
				smartSingleton.afterSingletonsInstantiated();
				// ...
			}
		}

spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java, lines 1141–1150

The difference from afterPropertiesSet is timing. afterPropertiesSet runs when that one bean is done, so other beans may not exist yet; this callback comes "with a guarantee that all regular singleton beans have been created already". The Javadoc therefore says a getBeansOfType call here causes no "accidental side effects during bootstrap" (lines 47–50). As the NOTE in the same Javadoc says, it is not called for lazily initialised singletons or for beans of any other scope (lines 51–54).

6. On close — destruction order and Lifecycle

What close() does

close() delegates to doClose() (lines 1146–1171). If registerShutdownHook() was called, the same doClose() runs in a thread when the JVM exits (lines 1069–1088). That is why a successful close() removes that hook again (lines 1158–1165).

The flow of close(). Publish ContextClosedEvent, stop SmartLifecycle beans from the highest phase through LifecycleProcessor.onClose, destroy singletons in reverse registration order with dependents first, and for each bean DisposableBeanAdapter calls postProcessBeforeDestruction (@PreDestroy), DisposableBean.destroy and destroy-method or close in turn, ending with closeBeanFactory. The green box is the end. Whatever is running (Lifecycle) is stopped first, then the objects are cleaned up.

Of what doClose() does, four things matter for this post (lines 1182–1233). It publishes ContextClosedEvent (line 1191), stops what is running through LifecycleProcessor.onClose() (line 1200), throws the singletons away with destroyBeans() (line 1208), and closes the container itself with closeBeanFactory() (line 1211). The source comment gives the reason for stopping Lifecycle beans first: "to avoid delays during individual destruction" (line 1197).

destroyBeans() leads to destroySingletons() (lines 1246–1248), which walks the names of beans that need destruction in reverse registration order and disposes of them one by one (DefaultSingletonBeanRegistry lines 699–705). When one bean is disposed of, the beans that depend on it go first (lines 776–790). The reverse of the creation order.

One bean's destruction — DisposableBeanAdapter

While a bean is built, registerDisposableBeanIfNecessary is called at the end of doCreateBean() (line 640). A singleton with at least one destruction callback is wrapped in a DisposableBeanAdapter and noted in the disposableBeans map (AbstractBeanFactory lines 1935–1943). The order of the three destruction styles lives in that adapter's destroy().

	@Override
	public void destroy() {
		if (!CollectionUtils.isEmpty(this.beanPostProcessors)) {
			for (DestructionAwareBeanPostProcessor processor : this.beanPostProcessors) {
				processor.postProcessBeforeDestruction(this.bean, this.beanName);
			}
		}

		if (this.invokeDisposableBean) {
			// ...
				((DisposableBean) this.bean).destroy();
			// ...
		}

		if (this.invokeAutoCloseable) {
			// ...
				((AutoCloseable) this.bean).close();
			// ...
		}
		else if (this.destroyMethods != null) {
			// ... invokeCustomDestroyMethod(destroyMethod)
		}
		// ...
	}

spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java, lines 196–259

A mirror image of initialisation. Post-processors first (@PreDestroy is InitDestroyAnnotationBeanPostProcessor.postProcessBeforeDestruction, lines 234–253) → the interface (DisposableBean.destroy) → configuration (destroy-method). The five lines under [close] in the section 5 example are this order. The two Tracers coming before @PreDestroy is again the "internal post-processors go last" of section 3.

destroyMethod is inferred even when you do not set it. The default of @Bean's destroyMethod is "(inferred)" (Bean line 346; the constant is at AbstractBeanDefinition line 166). With that value, and only when the bean is not a DisposableBean, the adapter picks shutdown for an ExecutorService, close for any other AutoCloseable, and otherwise looks for a public method named close or shutdown (lines 407–459). This is why a DataSource registered with @Bean gets closed without any extra setting. The default of initMethod, by contrast, is the empty string, so there is no inference on the init side (line 317).

Stopping what keeps running — Lifecycle and SmartLifecycle

Lifecycle is an interface with just start(), stop() and isRunning() (lines 50–84). It is for things that keep running after they are built, such as thread pools or message listeners, which is why "building" and "running" are kept apart. The NOTE in its Javadoc matters: this interface "does not imply specific auto-startup semantics"; implement SmartLifecycle for that (lines 22–23).

SmartLifecycle is Lifecycle plus Phased (line 67); isAutoStartup() defaults to true (lines 100–102) and getPhase() defaults to Integer.MAX_VALUE (line 82, lines 158–160).

The thing that drives them is DefaultLifecycleProcessor. The last step of refresh(), finishRefresh(), calls its onRefresh() (AbstractApplicationContext lines 1011–1014), and the doClose() above calls its onClose(). onRefresh() calls startBeans(true), that is, it starts only the auto-startup candidates (DefaultLifecycleProcessor lines 304–307; the test is at lines 383–387). The start and stop order is in the TreeMap of two methods.

	private void startBeans(boolean autoStartupOnly) {
		Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
		Map<Integer, LifecycleGroup> phases = new TreeMap<>();
		// ...
	}

	// ...

	private void stopBeans(boolean pauseableOnly) {
		Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
		Map<Integer, LifecycleGroup> phases = new TreeMap<>(Comparator.reverseOrder());
		// ...
	}

spring-context/src/main/java/org/springframework/context/support/DefaultLifecycleProcessor.java, lines 365–450

A group (LifecycleGroup) is made per phase and put in a TreeMap, so starting goes from the lowest phase up and stopping, with reverseOrder(), from the highest down. A plain Lifecycle that is not Phased counts as phase 0 (lines 562–564). Here you can see why the default phase of SmartLifecycle is Integer.MAX_VALUE: the auto-started components are placed where they start last and stop first (SmartLifecycle lines 70–74).

The phase order of SmartLifecycle. On onRefresh, early.start at phase 10, late.start at phase 20 and the default Integer.MAX_VALUE start in that order; on close they stop in the reverse order and then destroySingletons follows. A note box says a plain Lifecycle has phase 0 but is not auto-started. Start from the lowest phase, stop from the highest. The grey box is the plain bean of the example.

Example: two phases

// An example written for this post (not Spring's source)
    @Configuration
    static class Config {
        @Bean Worker late() { return new Worker("late", 20); }    // registered first, larger phase
        @Bean Worker early() { return new Worker("early", 10); }  // registered later, smaller phase
        @Bean PlainWorker plain() { return new PlainWorker(); }
    }

Worker implements SmartLifecycle and prints its name and phase in start() and stop(). PlainWorker is a plain Lifecycle. The output:

[refresh]
  start early (phase 10)
  start late (phase 20)
  plain.isRunning() = false
[close]
  stop  late (phase 20)
  stop  early (phase 10)

This example was run on the released Spring Framework 7.0.9 and produced the output above (the 7.1.0-SNAPSHOT this series reads was not built and run directly). The full code is in ex/u04/SmartLifecycleExample.java. Regardless of registration order, phase 10 started first and stopped last, and plain, a plain Lifecycle, got neither start() nor stop(). No stop() appears for it on the way down because doStop only stops beans whose isRunning() is true (lines 467–468).

Easy to get wrong

What this post leaves out

Only names and locations. The inside of ConfigurationClassPostProcessor, the best-known BeanDefinitionRegistryPostProcessor (Part 8). The AOT path invokeMergedBeanDefinitionPostProcessors, which applies MergedBeanDefinitionPostProcessor to every blueprint before any bean is built (PostProcessorRegistrationDelegate lines 321–323). ApplicationListenerDetector, which finds and registers beans that are ApplicationListeners (from line 48; events are in Part 12). Registering destruction callbacks for beans of non-singleton scopes (AbstractBeanFactory lines 1944–1952). Waiting for a destroy method that returns a reactive type (DisposableBeanAdapter lines 501–513). The container pause and restart added in 7.0 (onPause, onRestart, LifecycleProcessor lines 36–54) and the per-phase concurrent start and stop timeouts (DefaultLifecycleProcessor lines 603–631). And the rollback when refresh() fails, which stops the Lifecycle beans already started and destroys the singletons already created (AbstractApplicationContext lines 628–652).

Recap

Concept In one line
BeanFactoryPostProcessor Reads and edits blueprints before any bean is built. Never touches bean instances
BeanDefinitionRegistryPostProcessor Registers new blueprints. Called before the editing kind, repeatedly until no new ones appear
PriorityOrdered → Ordered → the rest The three-group order for both blueprint and bean post-processors. Judged by interface, so @Order does nothing
Post-processor return type The return type of the @Bean method is the type the container knows. Ordered must be visible there for the order to count
Internal post-processors re-registered The @PostConstruct and @Autowired handlers are moved to the end of the list. My Ordered post-processor comes before them
The BeanPostProcessor family The two hooks around initialisation are for everyone; the four sub-interfaces that step into instantiation, injection and destruction are "mainly for internal use"
postProcessBeforeInstantiation Return an object and the whole creation flow is skipped — a shortcut
Aware Only a marker. The three from spring-beans are set by invokeAwareMethods; the rest by ApplicationContextAwareProcessor
Initialisation order Aware → before-init hooks (ApplicationContextAware, …, @PostConstruct) → afterPropertiesSet → init-method → after-init hooks
SmartInitializingSingleton Once, after every singleton exists. A safe place to look up other beans
Destruction order Reverse registration order, dependents first. Per bean: postProcessBeforeDestruction (@PreDestroy) → destroy() → destroy-method or close()
destroyMethod inference Default (inferred): shutdown for an ExecutorService, close for an AutoCloseable, otherwise a method named close or shutdown
SmartLifecycle Run after building, stop before closing. Start from the lowest phase, stop from the highest. Default phase Integer.MAX_VALUE

What to look at next

This post marked spots along the steps of refresh() as it went. The next one, Part 7: The Twelve Steps of refresh() — the Order the Container Comes Up In, follows those twelve steps from first to last: what prepareBeanFactory prepares, why the message source and the event multicaster are created where they are, and how the post-processors of this post fit into those steps as one continuous line.

More posts