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.
- Where are the places where my code can be slipped into the container's work, and when is each one called?
- When several things step into the same place, in what order are they called?
- Why is there not just one
BeanPostProcessor, but this many different kinds?
| 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.
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.
BeanFactoryPostProcessor— one method,postProcessBeanFactory(ConfigurableListableBeanFactory). It reads and edits the registered blueprints (lines 70–80). Its Javadoc is firm: work with bean definitions but never bean instances, or beans get created too early (lines 31–35).BeanDefinitionRegistryPostProcessor— extends the one above and addspostProcessBeanDefinitionRegistry(BeanDefinitionRegistry). It registers new blueprints (lines 34–44).ConfigurationClassPostProcessor, which reads@Configurationclasses and turns@Beanmethods into blueprints, is one of these (section 6 of Part 2).
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);
The same pattern twice. The registering kind (②–④) finishes before the editing kind (⑥–⑧) runs.
The main thread is this.
BeanDefinitionRegistryPostProcessors are found and called in the orderPriorityOrdered→Ordered→ the rest. Notice that each group looks them up again withgetBeanNamesForType(lines 108, 122, 138). A blueprint registered by an earlier group may itself be another post-processor. The last group loops withwhileuntil nothing new appears (lines 135–150).- The
postProcessBeanFactoryof everything collected so far is called (lines 152–154). - Now the plain
BeanFactoryPostProcessors are called in the same three groups (lines 164–204). Names already handled in step 1 are filtered out throughprocessedBeansso nothing is called twice. - 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.
- The registering kind ran first, and when the editing kind ran there were 11 blueprints but the
Greeterconstructor had not run even once. That is why editing the blueprint alone changes the object that is built later. - Ten names under
singletons created so farstand out. They are things the container put in itself, such as the environment (AbstractApplicationContextlines 763–775), and the post-processors themselves (registrar,rewriter,ConfigurationClassPostProcessor). Post-processors are beans and have to be built before everyone else to do their job.
Easy to get wrong
- Make a
@Beanmethod that returns a post-processorstatic. The example does. The Javadoc of bothBeanFactoryPostProcessorandBeanPostProcessorrecommends it (BeanFactoryPostProcessorlines 43–48,BeanPostProcessorlines 38–44). Withoutstatic, the@Configurationclass has to be instantiated first to call the method, and that instance becomes a bean built before the post-processors are all in place. TheBeanPostProcessorCheckerin section 3 warns about exactly this. - The
@Orderannotation has no effect on post-processors. The Javadoc says so (BeanFactoryPostProcessorlines 59–61). The source above groups by whether an interface is implemented, as inisTypeMatch(ppName, PriorityOrdered.class). To get an order, implementOrderedorPriorityOrdered.
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));
}
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);
}
}
It is removed from where it was and added at the end. So the final list looks like this.
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.
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;
}
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);
}
}
}
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;
}
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);
}
}
}
}
}
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:
- Constructor →
@Autowiredsetter. Injection (populateBean) comes beforeinitializeBean, exactly as in Part 4. BeanNameAware→BeanClassLoaderAware→BeanFactoryAware. The order of theifs ininvokeAwareMethods.ApplicationContextAware. Set byApplicationContextAwareProcessor, the first post-processor in the before-initialisation hook.- The before-initialisation hooks of
TracerA→TracerB, and only then@PostConstruct. As the picture in section 3 showed,CommonAnnotationBeanPostProcessoris an internal post-processor and was moved to the end. afterPropertiesSet→customInit. The order ininvokeInitMethods.- The after-initialisation hooks, again
TracerA→TracerB. afterSingletonsInstantiatedis called separately, after every singleton exists (below).- Destruction is
postProcessBeforeDestruction(both Tracers, then@PreDestroy) →destroy()→customDestroy. Section 6 looks at it.
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();
// ...
}
}
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 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)
}
// ...
}
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());
// ...
}
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).
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
Lifecycleis only detected on top-level singletons. The Javadoc says so (lines 37–41), andgetLifecycleBeans()collects only beans that are already-created singletons or of typeSmartLifecycle(lines 528–546).- A
SmartLifecycleis stopped throughstop(Runnable). For aSmartLifecycle,DefaultLifecycleProcessorcalls only the callback-takingstop(Runnable), neverstop()(lines 473–486). The default implementation callsstop()and then runs the callback (SmartLifecyclelines 142–145), so implementing onlystop()as in the example is fine. But a component that stops asynchronously should implementstop(Runnable)itself and run the callback once it has really stopped, so that the phase group's timeout wait ends properly (lines 640 and 652–661).
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.
- Part 1: How I Read It with AI, and the Map
- Part 2: The Big Map — Modules and Packages
- Part 3: The BeanFactory Hierarchy and BeanDefinition
- Part 4: How a Bean Gets Created — the Creation Flow and the Three-Level Cache
- Part 5: How @Autowired Picks the Bean — Dependency Injection Resolution
- The same idea of "places to plug in", built in C: Digging into Apache httpd's Source 6: Modules and Hooks — How a Module Plugs into the Server