What this post covers
This is the seventh post in the "Digging into Spring's Source" series. Part 6 looked at the places where you can hook into the container while a single bean is being built (BeanPostProcessor, lifecycle callbacks such as @PostConstruct) and the order in which they are called. This time we zoom out to the order in which the container itself starts up. Part 1 and Part 2 said "refresh() walks through twelve steps" and moved on; here we open that method line by line.
There are four questions to answer.
- When
refresh()is called, what happens and in what order? Are there really twelve steps? - What does
ApplicationContextadd on top of theBeanFactorywe saw in Part 3? - Why are there two branches,
GenericApplicationContextandAbstractRefreshableApplicationContext? - What happens when startup fails, and when the context is closed with
close()?
| Item | Details |
|---|---|
| Source | Spring Framework 7.1.0-SNAPSHOT |
| Commit | f447f3c310 (2026-10-01, main branch) |
| Module read most | spring-context |
| Classes read most | AbstractApplicationContext, GenericApplicationContext, AbstractRefreshableApplicationContext, ConfigurableApplicationContext |
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.
My study notes go much deeper into each step. This post keeps only the main thread that someone seeing Spring for the first time can follow. What was left out is named at the end, under "If you want to go deeper".
Words to know first
Bean and container mean the same as in the earlier posts. A bean is an object Spring creates and looks after for you; the container is what builds and keeps those beans. The container's core interface is BeanFactory (Part 3), and how a single bean gets built is in Part 4.
ApplicationContext is an interface that extends BeanFactory. It is what you create with new AnnotationConfigApplicationContext(...). Besides handing out beans it also reads configuration properties, publishes events and loads files. Section 1 shows exactly what it adds. In this post, "the context" means an ApplicationContext.
refresh() is the method that actually starts the context. Reading the blueprints, processing them, creating the beans and announcing "ready" all happen inside it. Why it is called "refresh" is explained in section 7.
The template method pattern is a design where a parent class fixes the whole sequence in one method and lets subclasses fill in only a few of the steps. refresh() is exactly that "method that fixes the sequence", and there are a few slots for subclasses to fill.
Events and listeners: the context turns things like "I have started" and "I am closing" into event objects and publishes them, and beans that implement ApplicationListener receive them. Only two events appear in this post, ContextRefreshedEvent and ContextClosedEvent. The event mechanism itself is the subject of Part 12.
Post-processors were the subject of Part 6. A BeanFactoryPostProcessor edits blueprints (BeanDefinitions); a BeanPostProcessor edits bean objects while they are being built. Here we only look at which step of refresh() calls each of them.
1. What ApplicationContext adds to BeanFactory
The answer is in one line of the interface declaration.
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
spring-context/src/main/java/org/springframework/context/ApplicationContext.java lines 59–60
It bundles six interfaces. Two are from the BeanFactory family we saw in Part 3; four are new.
| Interface | What it adds | Where it comes from |
|---|---|---|
ListableBeanFactory |
Look beans up as a list, not just one name at a time | spring-beans (Part 3) |
HierarchicalBeanFactory |
Can have a parent container | spring-beans (Part 3) |
EnvironmentCapable |
Has properties and profiles (Environment) |
spring-core |
MessageSource |
Resolves internationalised messages | spring-context |
ApplicationEventPublisher |
Publishes events (publishEvent) |
spring-context |
ResourcePatternResolver |
Loads files and classpath resources by pattern | spring-core |
The two grey boxes are the BeanFactory family from Part 3. The two blue ones are the stage for this post.
Notice that neither refresh() nor close() is in that list. As its Javadoc says, ApplicationContext is read-only (lines 28–30); the methods that start and stop it live one level down, in ConfigurableApplicationContext.
public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {
spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java line 45
This is where refresh() (line 222), close() (line 267), registerShutdownHook() (line 256), isActive() (line 289) and getBeanFactory(), which exposes the BeanFactory inside (line 310), are declared. Code that merely uses beans gets the read-only ApplicationContext; starting and stopping is opened up only through Configurable….
The class that actually implements all of this is AbstractApplicationContext (AbstractApplicationContext lines 145–146). Almost every method read in this post is in that class.
The context has a BeanFactory
Look at how AbstractApplicationContext implements a BeanFactory method and the design shows itself.
@Override
public Object getBean(String name) throws BeansException {
assertBeanFactoryActive();
return getBeanFactory().getBean(name);
}
It does not build anything itself; it delegates to the BeanFactory it holds inside. The context did not grow by inheriting from BeanFactory. It holds one DefaultListableBeanFactory (from Part 4) and wraps properties, events and messages around it. The assertBeanFactoryActive() in front is a one-line "are we switched on?" check.
protected void assertBeanFactoryActive() {
if (!this.active.get()) {
if (this.closed.get()) {
throw new IllegalStateException(getDisplayName() + " has been closed already");
}
else {
throw new IllegalStateException(getDisplayName() + " has not been refreshed yet");
}
}
}
active and closed are two AtomicBoolean fields (lines 209–212). refresh() sets active; close() sets closed and clears active. These two flags run through the whole post.
2. The skeleton of refresh() — counted: twelve
Part 1 showed that the constructor of new AnnotationConfigApplicationContext(AppConfig.class) is two lines, register and refresh (AnnotationConfigApplicationContext lines 89–93). Here is the body of that refresh(). The exception handling is covered separately in section 8, so it is shortened to // ... here.
@Override
public void refresh() throws BeansException, IllegalStateException {
this.startupShutdownLock.lock();
try {
this.startupShutdownThread = Thread.currentThread();
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (RuntimeException | Error ex) {
// ...
}
finally {
contextRefresh.end();
}
}
finally {
this.startupShutdownThread = null;
this.startupShutdownLock.unlock();
}
}
Count the method calls that each carry a comment, from prepareRefresh to finishRefresh, and there are exactly twelve. The "twelve steps" that Parts 1 and 2 mentioned holds for this commit. StartupStep is a timing tool that measures each section, so it is not counted as a step.
The four blocks from Part 2 lined up with the twelve steps. Grey boxes are slots for subclasses whose default implementation is empty, blue ones are post-processors and bean creation, and green is the last step.
| Step | Method | In one line | Section |
|---|---|---|---|
| 1 | prepareRefresh |
Set active, check the environment, prepare the bucket for early events |
3 |
| 2 | obtainFreshBeanFactory |
Get the BeanFactory to hold inside (where the two branches split) |
3, 7 |
| 3 | prepareBeanFactory |
Plug context-level machinery into the BeanFactory |
3 |
| 4 | postProcessBeanFactory |
Slot for subclasses (empty by default) | 4 |
| 5 | invokeBeanFactoryPostProcessors |
Edit and add blueprints | 4 |
| 6 | registerBeanPostProcessors |
Create and register the bean post-processors first | 4 |
| 7 | initMessageSource |
Set up the message source | 5 |
| 8 | initApplicationEventMulticaster |
Set up the event multicaster | 5 |
| 9 | onRefresh |
Slot for subclasses (empty by default) | 5 |
| 10 | registerListeners |
Register listeners, deliver events held back so far | 5 |
| 11 | finishBeanFactoryInitialization |
Create every singleton bean | 6 |
| 12 | finishRefresh |
Start Lifecycle beans, publish ContextRefreshedEvent |
6 |
The startupShutdownLock at the top is a lock that stops starting and stopping from happening at the same time (line 215). close() in section 8 takes the same lock. startupShutdownThread records which thread currently holds it (line 218).
3. Steps 1 to 3: prepare
Step 1, prepareRefresh() (lines 668–703) does four things. It records the start time, clears closed and sets active (lines 670–672). It checks that every property the Environment marks as required is present (line 688). It keeps a separate copy of listeners registered in code before refresh() (lines 691–698). And it creates an empty set called earlyApplicationEvents (line 702). What that set is for becomes clear in section 5. (The call to initPropertySources(), whose default implementation is empty (lines 710–712), is not counted.)
Step 2, obtainFreshBeanFactory(), is two lines.
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
return getBeanFactory();
}
Both refreshBeanFactory() and getBeanFactory() are abstract methods (line 1633, line 1656). How the inner BeanFactory is obtained is left to the subclass. That is the fork of section 7.
Step 3, prepareBeanFactory(), is the preparation step most worth reading. It takes the plain BeanFactory just obtained and plugs in what it needs to work as "the BeanFactory inside a context". Keeping only the essentials:
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// Tell the internal bean factory to use the context's class loader etc.
beanFactory.setBeanClassLoader(getClassLoader());
// ...
// Configure the bean factory with context callbacks.
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
// ...
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
// ...
// BeanFactory interface not registered as resolvable type in a plain factory.
// MessageSource registered (and found for autowiring) as a bean.
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);
// Register early post-processor for detecting inner beans as ApplicationListeners.
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));
// ...
// Register default environment beans.
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
// ...
}
Three things get plugged in.
ApplicationContextAwareProcessor(line 737). ABeanPostProcessorthat hands the context or theEnvironmentto any bean implementingApplicationContextAware,EnvironmentAwareand so on. Open it andpostProcessBeforeInitializationchecks seven interfaces in turn withinstanceof(ApplicationContextAwareProcessorlines 83–112). So "the context recognisesAwareinterfaces" is implemented with one of the post-processors we met in Part 4. TheignoreDependencyInterfacecalls just below (lines 738–744) exclude the setters of those same seven interfaces from autowiring: the post-processor will fill them, so injection should keep out.registerResolvableDependency(lines 748–751). These four lines are why@Autowired ApplicationContext ctx;works even though no bean of typeBeanFactoryorApplicationContextwas ever registered. They tell theBeanFactoryin advance: "if someone asks for this type, give them this object." As the comment says,MessageSourceis not here; it is registered as a bean in step 7.ApplicationListenerDetector(line 754). A post-processor that puts a freshly built bean on the listener list if it is anApplicationListener(ApplicationListenerDetectorlines 75–95). It is a safety net for inner beans thatregisterListenersin step 10 cannot find by name.
Finally it registers four objects as singleton beans: environment, systemProperties, systemEnvironment and applicationStartup (lines 764–775). That is why @Autowired Environment env; works.
What the code above plugs in, grouped by kind. Blue is the post-processors that work while beans are created; yellow is objects handed out during injection (the LoadTimeWeaver lines trimmed from the code above are left out).
Easy to get wrong
Awarecallbacks are a feature of the context, not of theBeanFactory. Use aDefaultListableBeanFactoryon its own, without a context, andApplicationContextAwareis never called, because the post-processor that calls it is plugged in here in step 3. TheinvokeAwareMethodsfrom Part 4 handles only three interfaces itself:BeanNameAware,BeanClassLoaderAwareandBeanFactoryAware.- These two post-processors go in before step 6. They are added directly with
addBeanPostProcessorin step 3, so they come before the post-processors that are registered as beans and collected in step 6. That is why, in the example in section 6,ApplicationContextAwareprints before our ownBeanPostProcessor.
4. Steps 4 to 6: process the blueprints
Step 4, postProcessBeanFactory(), has an empty default implementation (lines 787–788). It is one of the "slots a subclass fills" in the template method pattern. The moment described in its Javadoc matters: the blueprints have been loaded, but no post-processor has run and not a single bean has been created (lines 779–782). The web contexts plug their servlet-related post-processors in here (GenericWebApplicationContext line 153; Part 20 covers it).
Step 5, invokeBeanFactoryPostProcessors(), is a one-line delegation.
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
// ...
}
getBeanFactoryPostProcessors() is the list of processors added in code with addBeanFactoryPostProcessor (line 203); the BeanFactoryPostProcessors registered as beans are found in the BeanFactory, created and called in order by PostProcessorRegistrationDelegate (from PostProcessorRegistrationDelegate line 68). The ordering rules were the subject of Part 6, so they are not repeated here.
This step matters because annotation configuration is interpreted here. ConfigurationClassPostProcessor, which reads @Configuration classes and adds the @Bean methods and @ComponentScan results as blueprints, is itself a kind of BeanFactoryPostProcessor, so it runs in this step (registered at AnnotationConfigUtils lines 158–161). This is what Part 2 meant by "much of collecting definitions is implemented with the machinery for processing them". The details are in Part 8.
Step 6, registerBeanPostProcessors(), delegates the same way (lines 812–814; the implementation starts at PostProcessorRegistrationDelegate line 211). It creates every BeanPostProcessor bean now and plugs them into the BeanFactory. A post-processor has to exist before the ordinary beans it will intercept, which is why this is step 6 rather than step 11.
Two steps with similar names, but step 5's post-processors work on the spot while step 6's wait until step 11.
Easy to get wrong
- Step 5 calls, step 6 only registers. A
BeanFactoryPostProcessoractually runs in this step and is done. ABeanPostProcessoris only created and registered here; its real work happens in step 11, as each bean is built. - Because post-processor beans are created earlier than ordinary beans, a post-processor that receives an ordinary bean through
@Autowireddrags that bean forward too, and that bean then misses the post-processors that are not registered yet. This is the trap covered in Part 6.
5. Steps 7 to 10: set up the extras
Steps 7 and 8 have the same shape: "if a bean with this name exists, use it; otherwise create the default and register it as a bean."
- Step 7,
initMessageSource()(lines 821–846): uses a bean namedmessageSourceif there is one, otherwise creates and registers aDelegatingMessageSource(lines 838–841). TheMessageSourcemethods from section 1 delegate to this object (lines 1537–1550). - Step 8,
initApplicationEventMulticaster()(lines 854–871): uses a bean namedapplicationEventMulticasterif there is one, otherwise creates aSimpleApplicationEventMulticaster(lines 864–865). This is the object that actually hands events to the listeners.
Step 9, onRefresh(), is another empty slot like step 4 (lines 907–909). As its Javadoc says, it is where a subclass "initialises special beans, before singletons are instantiated". No class in any module of this repository overrides it (apart from one test); it is a slot for frameworks built on top. (Spring Boot is known to start its embedded web server here, but Spring Boot is a separate project from this repository, so that is not something this post verified in the source.)
Step 10, registerListeners(), has three parts.
protected void registerListeners() {
// Register statically specified listeners first.
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let post-processors apply to them!
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}
// Publish early application events now that we finally have a multicaster...
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (!CollectionUtils.isEmpty(earlyEventsToProcess)) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}
The first part is for listeners registered directly in code, the second for listeners registered as beans. Note that the second part passes only names (addApplicationListenerBean). As the comment says, this is not yet the time to create beans, so it writes down the name and the actual object is fetched after step 11.
The third part is what the earlyApplicationEvents set from step 1 is for. Open publishEvent() and you see that, while that set is not null, events are not delivered but collected.
// Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else if (this.applicationEventMulticaster != null) {
this.applicationEventMulticaster.multicastEvent(applicationEvent, eventType);
}
Step 1 creates the set (so it is no longer null) and step 10 sets it back to null, so anything published in between — by a BeanFactoryPostProcessor in step 5, say — piles up in the set and goes out all at once in step 10. It is a device that stops events sent before there is a multicaster (step 8) or any listeners (step 10) from being lost.
The "collecting time" is while earlyApplicationEvents is not null (steps 1–10).
Easy to get wrong
- Before step 8 there is no multicaster.
getApplicationEventMulticaster()throws anIllegalStateExceptionsaying "call 'refresh' before multicasting events" when it isnull(lines 468–474).publishEvent, however, catches events in the early set as shown above, so no exception is thrown duringrefresh(). - The earliest event a listener bean can receive is one from after step 10. Earlier events are held in the set and delivered in step 10, when bean listeners are registered by name only, so a bean that wants to receive them gets created on the spot.
6. Steps 11 and 12: create beans and start running
Step 11, finishBeanFactoryInitialization(), is where the preInstantiateSingletons() from Part 4 is called. A few lines of preparation come first.
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// ...
// Initialize conversion service for this context.
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}
// Register a default embedded value resolver if no BeanFactoryPostProcessor
// (such as a PropertySourcesPlaceholderConfigurer bean) registered any before:
// at this point, primarily for resolution in annotation attribute values.
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}
// ...
// Stop using the temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(null);
// Allow for caching all bean definition metadata, not expecting further changes.
beanFactory.freezeConfiguration();
// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();
}
- If there is a bean named
conversionService, it becomes theBeanFactory's type converter (lines 955–959). That is the machinery that turns string properties into numbers or dates, the subject of Part 14. - If no embedded value resolver has been registered, the
Environment's placeholder resolution (${...}) goes in as the default (lines 964–966). This is why@Value("${server.port}")works without any extra setup. freezeConfiguration()declares "the blueprints will not change any more" so theBeanFactorycan cache metadata (line 992; implementation atDefaultListableBeanFactorylines 998–1002).- The final
preInstantiateSingletons()(line 995) creates every non-lazy singleton withgetBean()(DefaultListableBeanFactorylines 1102–1151). Once all beans exist it callsafterSingletonsInstantiated()once on every bean that implementsSmartInitializingSingleton(lines 1142–1150). It is the place for work that must wait until all singletons, not just your own bean, are ready.
Step 12, finishRefresh(), is five lines.
protected void finishRefresh() {
// Reset common introspection caches in Spring's core infrastructure.
resetCommonCaches();
// Clear context-level resource caches (such as ASM metadata from scanning).
clearResourceCaches();
// Initialize lifecycle processor for this context.
initLifecycleProcessor();
// Propagate refresh to lifecycle processor first.
getLifecycleProcessor().onRefresh();
// Publish the final event.
publishEvent(new ContextRefreshedEvent(this));
}
resetCommonCaches()empties the caches that piled up while reading reflection and annotations during startup (lines 1042–1047). The beans are built, so that memory is no longer needed.clearResourceCaches()empties the resource cache used for classpath scanning (lines 1050–1055).initLifecycleProcessor()has the same shape as steps 7 and 8: if there is nolifecycleProcessorbean, it creates and registers aDefaultLifecycleProcessor(lines 880–898).getLifecycleProcessor().onRefresh()callsstart()on the auto-startup beans among those implementing theLifecycleinterface (start(),stop(),isRunning()—Lifecycleline 50) (DefaultLifecycleProcessorlines 295–315).SmartLifecyclehasisAutoStartup()defaulting totrue(SmartLifecycleline 100), so just implementing it gets you started here. Things like message-queue consumers or schedulers, which "must start running only after every bean is built", use this slot.- Finally it publishes
ContextRefreshedEvent: the signal that the container is fully up. Spring uses this signal itself: Part 21 showsDispatcherServletreceiving this event and collecting the strategy objects it handles requests with.
Example: which step calls what
One hook each in steps 5, 6, 11 and 12, and we watch the order of the output.
// An example written for this post (not Spring's code)
package ex.u05;
// ... (imports omitted)
public class RefreshOrderExample {
// Step 5, invokeBeanFactoryPostProcessors: the moment the blueprints (BeanDefinitions) can be edited
static class LoggingFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
String name = "refreshOrderExample.Greeter"; // a nested class gets a bean name like this
System.out.println("[5] BeanFactoryPostProcessor: Greeter definition="
+ beanFactory.containsBeanDefinition(name)
+ ", instance=" + beanFactory.containsSingleton(name));
}
}
// Registered in step 6, registerBeanPostProcessors; called for every bean in step 11
static class LoggingPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean.getClass().getPackageName().startsWith("ex.u05")) {
System.out.println("[11] BeanPostProcessor before: " + bean.getClass().getSimpleName());
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean.getClass().getPackageName().startsWith("ex.u05")) {
System.out.println("[11] BeanPostProcessor after: " + bean.getClass().getSimpleName());
}
return bean;
}
}
// Step 11, finishBeanFactoryInitialization: one ordinary singleton bean
static class Greeter implements ApplicationContextAware, SmartInitializingSingleton {
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
System.out.println("[11] ApplicationContextAware: Greeter");
}
@PostConstruct
void init() {
System.out.println("[11] @PostConstruct: Greeter");
}
@Override
public void afterSingletonsInstantiated() {
System.out.println("[11] SmartInitializingSingleton: after every singleton is created");
}
}
// Step 12, finishRefresh: LifecycleProcessor.onRefresh() calls start()
static class Starter implements SmartLifecycle {
private boolean running;
@Override
public void start() {
this.running = true;
System.out.println("[12] SmartLifecycle.start()");
}
@Override
public void stop() {
this.running = false;
}
@Override
public boolean isRunning() {
return this.running;
}
}
// Step 12, finishRefresh: ContextRefreshedEvent comes last
static class RefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
System.out.println("[12] ContextRefreshedEvent");
}
}
public static void main(String[] args) {
System.out.println("-- refresh() begins");
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
LoggingFactoryPostProcessor.class, LoggingPostProcessor.class,
Greeter.class, Starter.class, RefreshedListener.class);
System.out.println("-- refresh() done: " + context.getBean(Greeter.class).getClass().getSimpleName());
context.close();
}
}
The output:
-- refresh() begins
[5] BeanFactoryPostProcessor: Greeter definition=true, instance=false
[11] ApplicationContextAware: Greeter
[11] BeanPostProcessor before: Greeter
[11] @PostConstruct: Greeter
[11] BeanPostProcessor after: Greeter
[11] BeanPostProcessor before: Starter
[11] BeanPostProcessor after: Starter
[11] BeanPostProcessor before: RefreshedListener
[11] BeanPostProcessor after: RefreshedListener
[11] SmartInitializingSingleton: after every singleton is created
[12] SmartLifecycle.start()
[12] ContextRefreshedEvent
-- refresh() done: Greeter
The [5], [11] and [12] lines of the output above, sorted into step boxes. The order is exactly the actual output.
Four things to read off it.
- In step 5,
Greeter's blueprint exists but the object does not. That is the world aBeanFactoryPostProcessorsees. ApplicationContextAwarecomes before ourBeanPostProcessor'sbefore. As section 3 showed,ApplicationContextAwareProcessorwas plugged in earlier, in step 3.SmartInitializingSingletonis called once, after all three beans are built. That is different from@PostConstruct, which is called per bean.start()comes beforeContextRefreshedEvent, exactly in the line order offinishRefresh().
This example was run on the released Spring Framework 7.0.9 and produced the output above (it was not run on a self-built 7.1.0-SNAPSHOT, the version this series reads).
7. Two branches: GenericApplicationContext and AbstractRefreshableApplicationContext
refreshBeanFactory() from step 2 was abstract. Two subclasses fill it in, in opposite ways.
The yellow boxes show how each branch obtains its BeanFactory; the red box is the fate of a second refresh().
GenericApplicationContext creates its BeanFactory once, in the constructor, and keeps it in a final field.
private final DefaultListableBeanFactory beanFactory;
// ...
private final AtomicBoolean refreshed = new AtomicBoolean();
/**
* Create a new GenericApplicationContext.
* @see #registerBeanDefinition
* @see #refresh
*/
public GenericApplicationContext() {
this.beanFactory = new DefaultListableBeanFactory();
}
So all its refreshBeanFactory() has to do is "make sure this is not the second time".
@Override
protected final void refreshBeanFactory() throws IllegalStateException {
if (!this.refreshed.compareAndSet(false, true)) {
throw new IllegalStateException(
"GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
}
this.beanFactory.setSerializationId(getId());
}
Having the BeanFactory from the start means blueprints can be registered before refresh(): registerBean(Class) (lines 499–500) or registerBeanDefinition from BeanDefinitionRegistry. The AnnotationConfigApplicationContext of Part 1 is on this branch (AnnotationConfigApplicationContext line 57); the register(componentClasses) in its constructor is exactly "register blueprints before refresh()".
AbstractRefreshableApplicationContext does the opposite: it creates a new BeanFactory on every refresh().
@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
beanFactory.setApplicationStartup(getApplicationStartup());
customizeBeanFactory(beanFactory);
loadBeanDefinitions(beanFactory);
this.beanFactory = beanFactory;
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
If a BeanFactory already exists, it destroys the beans and drops it, creates a new DefaultListableBeanFactory (lines 195–197) and reads the blueprints again through the abstract loadBeanDefinitions(). ClassPathXmlApplicationContext, which reads XML files, is on this branch (ClassPathXmlApplicationContext line 53; its loadBeanDefinitions is at AbstractXmlApplicationContext line 83). "Edit the config file and call refresh() again to reload it" was the original use case, and it is this branch that gives the method its name, refresh. The class Javadoc describes it as the base class for contexts that "support multiple calls to refresh(), creating a new internal bean factory instance every time" (lines 29–32).
GenericApplicationContext |
AbstractRefreshableApplicationContext |
|
|---|---|---|
When the BeanFactory is created |
Once, in the constructor | On every refresh() |
| Who puts the blueprints in | The outside (registerBean, a BeanDefinitionReader) |
The inside (loadBeanDefinitions) |
Calling refresh() twice |
IllegalStateException |
Allowed: all beans are dropped and the definitions re-read |
| Typical subclass | AnnotationConfigApplicationContext |
ClassPathXmlApplicationContext |
getBeanFactory() |
Always returns the same object (lines 324–326) | IllegalStateException if there is none (lines 164–171) |
Example: before refresh(), after it, a second time, and after close()
// An example written for this post (not Spring's code)
package ex.u05;
// ... (imports omitted)
public class RefreshTwiceExample {
static class Greeter {
String hello() {
return "hello";
}
}
public static void main(String[] args) {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean(Greeter.class); // only the blueprint — no object yet
try {
context.getBean(Greeter.class); // ask for it before refresh()?
}
catch (IllegalStateException ex) {
System.out.println("before refresh, getBean: " + ex.getMessage());
}
context.refresh(); // start the container
System.out.println("after refresh, getBean: " + context.getBean(Greeter.class).hello());
try {
context.refresh(); // start it once more?
}
catch (IllegalStateException ex) {
System.out.println("second refresh: " + ex.getMessage());
}
context.close();
try {
context.getBean(Greeter.class); // ask for it after close()?
}
catch (IllegalStateException ex) {
System.out.println("after close, getBean: " + ex.getMessage());
}
}
}
before refresh, getBean: org.springframework.context.support.GenericApplicationContext@6fdb1f78 has not been refreshed yet
after refresh, getBean: hello
second refresh: GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once
after close, getBean: org.springframework.context.support.GenericApplicationContext@6fdb1f78 has been closed already
The first and fourth lines come from assertBeanFactoryActive() in section 1; the third from the refreshBeanFactory() above. They are all IllegalStateException, but the message tells you which flag caused it. (The @6fdb1f78 part is an object hash and differs on every run.) This example too was run on Spring Framework 7.0.9.
Easy to get wrong
- "Register before
refresh()" only works on theGeneric…branch. On theAbstractRefreshable…branch there is noBeanFactoryat all beforerefresh()(getBeanFactory()throws), so there is nowhere to put a blueprint. You hand it the config locations and letrefresh()do the reading. - A second
refresh()on aGenericApplicationContextis an exception, not a restart. To start again with different blueprints, create a new context. 7.x also hasrestart()andpause()onConfigurableApplicationContext(line 234, line 244), but those stop and restartLifecyclebeans; they do not rebuild beans. They are not covered here.
8. When it fails, and when it closes
An exception in the middle of refresh()
Here is the catch block that section 2 shortened.
catch (RuntimeException | Error ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Stop already started Lifecycle beans to avoid dangling resources.
if (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning()) {
try {
this.lifecycleProcessor.stop();
}
catch (Throwable ex2) {
logger.warn("Exception thrown from LifecycleProcessor on cancelled refresh", ex2);
}
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
The red boxes are the exception coming in and going out. Everything built in between is cleaned up.
The Javadoc of ConfigurableApplicationContext.refresh() states the principle: after this method, "either all or no singletons at all should be instantiated" (lines 212–221). So if the tenth bean fails in step 11, the previous nine are destroyed too with destroyBeans() (line 645; implementation at lines 1246–1248), and cancelRefresh() clears active and the caches (lines 1025–1030). The exception is not swallowed; it is rethrown, so whoever called new AnnotationConfigApplicationContext(...) receives it. Note also that only steps 4 to 12 are inside the try: steps 1 to 3 create no beans, so there is nothing to clean up.
close() and doClose()
close() takes the lock, calls doClose(), and then removes the JVM shutdown hook if one was registered (lines 1146–1171). The real work is in doClose().
protected void doClose() {
// Check whether an actual close attempt is necessary...
if (this.active.get() && this.closed.compareAndSet(false, true)) {
// ...
try {
// Publish shutdown event.
publishEvent(new ContextClosedEvent(this));
}
catch (Throwable ex) {
logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
}
// Stop all Lifecycle beans, to avoid delays during individual destruction.
if (this.lifecycleProcessor != null) {
try {
this.lifecycleProcessor.onClose();
}
// ...
}
// Destroy all cached singletons in the context's BeanFactory.
destroyBeans();
// Close the state of this context itself.
closeBeanFactory();
// Let subclasses do some final clean-up if they wish...
onClose();
// Reset common introspection caches to avoid class reference leaks.
resetCommonCaches();
// ...
// Reset internal delegates.
this.applicationEventMulticaster = null;
this.messageSource = null;
this.lifecycleProcessor = null;
// Switch to inactive.
this.active.set(false);
}
}
The reverse of starting up. The event goes out first, whatever was running is stopped, and only then are the beans destroyed.
The order is the reverse of startup. Step 12 was start() → ContextRefreshedEvent; closing is ContextClosedEvent → stop() (onClose() calls stopBeans — DefaultLifecycleProcessor lines 336–339) → destroy the beans. The news that "we are closing" is announced while the beans are still alive; what was running is stopped; only then is anything destroyed. The if at the top sets closed with compareAndSet, so a second close() passes through quietly.
The destroySingletons() that destroyBeans() calls walks the list of beans that have a destruction callback from the end (DefaultSingletonBeanRegistry lines 703–705). Beans enter that list in creation order (AbstractAutowireCapableBeanFactory line 640), so the bean created last is destroyed first.
Example: the order on close()
// An example written for this post (not Spring's code)
package ex.u05;
// ... (imports omitted)
public class CloseOrderExample {
// Registered first — destroyed later
static class Repository implements DisposableBean {
@Override
public void destroy() {
System.out.println("[3] DisposableBean.destroy(): Repository");
}
}
// Registered later — destroyed first
static class Service {
@PreDestroy
void shutdown() {
System.out.println("[3] @PreDestroy: Service");
}
}
static class Starter implements SmartLifecycle {
private boolean running;
@Override
public void start() {
this.running = true;
}
@Override
public void stop() {
this.running = false;
System.out.println("[2] SmartLifecycle.stop()");
}
@Override
public boolean isRunning() {
return this.running;
}
}
static class ClosedListener implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("[1] ContextClosedEvent");
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
Repository.class, Service.class, Starter.class, ClosedListener.class);
System.out.println("-- close() begins");
context.close();
System.out.println("-- close() done: isActive=" + context.isActive() + ", isClosed=" + context.isClosed());
}
}
-- close() begins
[1] ContextClosedEvent
[2] SmartLifecycle.stop()
[3] @PreDestroy: Service
[3] DisposableBean.destroy(): Repository
-- close() done: isActive=false, isClosed=true
You can see the order event → stop() → destruction, and that Service, registered after Repository, is destroyed first. This example too was run on Spring Framework 7.0.9.
Easy to get wrong
- If you never call
close(),@PreDestroyis never called. The only two routes intodoClose()areclose()and the JVM shutdown hook, and the hook is registered only if you callregisterShutdownHook()yourself (lines 1069–1088). In a program whosemainsimply ends, you need one or the other. SinceConfigurableApplicationContextisCloseable, the shortest way istry (var context = new AnnotationConfigApplicationContext(...)). (Spring Boot is known to register this hook for you, but Spring Boot is a separate project from this repository, so that is not something this post verified in the source.) - An exception thrown inside a
ContextClosedEventlistener does not stop the close.publishEventis wrapped intry/catch, logs a warning and carries on (lines 1189–1195). getBean()after closing is an exception. That is the last line of the example in section 7, "has been closed already". If you keep a reference to a context somewhere and use it after closing, this is the message you meet.
If you want to go deeper
Things this post only named in passing. The 7.x restart() and pause() with their ContextRestartedEvent and ContextPausedEvent (ConfigurableApplicationContext lines 234–244). The tail of publishEvent, which also propagates the event to the parent context (lines 453–460). isStartupShutdownThreadStuck(), which works out whether the startup thread is stuck in a System.exit when the shutdown hook cannot get the lock (lines 1118–1136). At the start of step 11, the bootstrapExecutor (an executor that creates beans on background threads, lines 948–952) and BeanFactoryInitializer (lines 969–972). The LoadTimeWeaver in steps 3 and 5 (a device that rewrites bytecode as classes load, lines 757–761). refreshForAotProcessing, the AOT alternative to refresh() (GenericApplicationContext, Part 16). ApplicationStartup, which times startup with StartupStep (line 587). And the insides of Environment, MessageSource and the event multicaster, which Part 12 covers.
Recap
| Concept | In one line |
|---|---|
ApplicationContext |
Two BeanFactory interfaces plus Environment, MessageSource, event publishing and resource loading. Read-only |
ConfigurableApplicationContext |
Adds refresh(), close() and Lifecycle: the side that starts and stops |
AbstractApplicationContext |
Implements the above. Does not inherit BeanFactory; holds one inside and delegates |
refresh() |
Twelve steps: prepare (1–3) → process the blueprints (4–6) → set up the extras (7–10) → create beans and start running (11–12) |
Step 3 prepareBeanFactory |
The Aware post-processor, the listener detector, and the registrations that make @Autowired ApplicationContext work |
| Steps 5 and 6 | BeanFactoryPostProcessors are run here; BeanPostProcessors are only registered here |
| Early events | Events published between step 1 and step 10 are held back and delivered in step 10 |
| Step 11 | freezeConfiguration then preInstantiateSingletons; SmartInitializingSingleton at the end |
| Step 12 | Lifecycle.start(), then ContextRefreshedEvent |
GenericApplicationContext |
BeanFactory created once in the constructor. Registration before refresh() is possible; a second refresh() throws |
AbstractRefreshableApplicationContext |
A new BeanFactory on every refresh(), with the blueprints read again |
| Failure | Destroys every singleton created, clears active, rethrows the exception |
close() |
ContextClosedEvent → stop() → destroy the beans (reverse creation order) → clear active |
What to look at next
In step 5 we passed in one line over "ConfigurationClassPostProcessor reads @Configuration classes and adds blueprints". Behind that line is how a single @Bean method becomes a BeanDefinition, how @ComponentScan searches the classpath, and why a @Configuration class is wrapped with CGLIB. Part 8: How @Configuration and @ComponentScan Become Blueprints covers it.
- 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 6: Lifecycle Callbacks and PostProcessors — Where You Can Step into the Container