How Spring Boots Up: The refresh() Sequence and @Configuration Class Processing
Prerequisites
- ›Article 2: BeanFactory hierarchy and BeanPostProcessor
- ›Understanding of Java annotations and reflection
How Spring Boots Up: The refresh() Sequence and @Configuration Class Processing
Every Spring application — whether a command-line tool, a web server, or a microservice — starts with a single method call: AbstractApplicationContext.refresh(). This method orchestrates the entire bootstrap sequence: preparing the environment, scanning for components, processing @Configuration classes, registering post-processors, and instantiating all singleton beans. Understanding refresh() is the key to understanding how the IoC container we explored in Part 2 gets populated and activated.
ApplicationContext: More Than a BeanFactory
Before diving into refresh(), it's worth understanding what ApplicationContext adds beyond the BeanFactory we covered in the previous article. The interface declaration tells the story:
spring-context/src/main/java/org/springframework/context/ApplicationContext.java#L59-L60
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory,
HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher,
ResourcePatternResolver {
classDiagram
class ApplicationContext {
<<interface>>
}
class ListableBeanFactory {
<<interface>>
Bean enumeration
}
class HierarchicalBeanFactory {
<<interface>>
Parent-child contexts
}
class EnvironmentCapable {
<<interface>>
Profiles + properties
}
class MessageSource {
<<interface>>
i18n message resolution
}
class ApplicationEventPublisher {
<<interface>>
Event broadcasting
}
class ResourcePatternResolver {
<<interface>>
Classpath scanning
}
ListableBeanFactory <|-- ApplicationContext
HierarchicalBeanFactory <|-- ApplicationContext
EnvironmentCapable <|-- ApplicationContext
MessageSource <|-- ApplicationContext
ApplicationEventPublisher <|-- ApplicationContext
ResourcePatternResolver <|-- ApplicationContext
ApplicationContext composes BeanFactory (via ListableBeanFactory and HierarchicalBeanFactory) with four additional capabilities: environment abstraction (profiles and property sources), internationalization, event publishing, and classpath resource scanning. These aren't luxuries — they're essential infrastructure that the configuration processing pipeline depends on.
The two most common concrete implementations are:
AnnotationConfigApplicationContext— for standalone annotation-based applications. It internally creates aDefaultListableBeanFactoryand registers anAnnotatedBeanDefinitionReader.GenericWebApplicationContext— for web applications, typically created by Spring Boot'sSpringApplication.
The 13-Step refresh() Sequence
The refresh() method is the single most important method in Spring Framework. Let's examine it in full:
sequenceDiagram
participant App as Application
participant AAC as AbstractApplicationContext
participant BF as BeanFactory
participant BPPs as BeanPostProcessors
App->>AAC: refresh()
Note over AAC: 1. prepareRefresh()
Note right of AAC: Set startup time, active flag,<br/>validate required properties
AAC->>BF: 2. obtainFreshBeanFactory()
Note right of BF: Get or create the<br/>DefaultListableBeanFactory
AAC->>BF: 3. prepareBeanFactory(beanFactory)
Note right of BF: Register environment beans,<br/>system properties, ClassLoader
Note over AAC: 4. postProcessBeanFactory()
Note right of AAC: Template method for subclasses<br/>(web contexts register scopes)
AAC->>BF: 5. invokeBeanFactoryPostProcessors()
Note right of BF: 🔥 ConfigurationClassPostProcessor<br/>runs here — discovers all beans
AAC->>BF: 6. registerBeanPostProcessors()
Note right of BF: Sort & register all BPPs<br/>by PriorityOrdered/Ordered
Note over AAC: 7. initMessageSource()
Note over AAC: 8. initApplicationEventMulticaster()
Note over AAC: 9. onRefresh()
Note right of AAC: Template method<br/>(web contexts start server)
Note over AAC: 10. registerListeners()
AAC->>BF: 11. finishBeanFactoryInitialization()
Note right of BF: Pre-instantiate ALL<br/>non-lazy singletons
Note over AAC: 12. finishRefresh()
Note right of AAC: Publish ContextRefreshedEvent,<br/>start Lifecycle beans
Note over AAC: 13. Exception handling
Note right of AAC: destroyBeans() + cancelRefresh()<br/>on any failure
Let me highlight the three most consequential steps:
Step 5 — invokeBeanFactoryPostProcessors() is where the magic happens. This step invokes all BeanFactoryPostProcessor and BeanDefinitionRegistryPostProcessor beans. The most important one is ConfigurationClassPostProcessor, which scans the classpath, processes @Configuration classes, handles @ComponentScan, resolves @Import chains, and registers every discovered bean definition. Before step 5, the factory has only a handful of infrastructure beans. After step 5, it has your entire application's bean definitions.
Step 6 — registerBeanPostProcessors() discovers all BeanPostProcessor bean definitions (created during step 5) and instantiates them in priority order. As we saw in Part 2, BeanPostProcessor is the universal extension hook — so this step effectively arms the factory for everything that follows.
Step 11 — finishBeanFactoryInitialization() triggers DefaultListableBeanFactory.preInstantiateSingletons(), which iterates over every registered bean definition and calls getBean() for each non-lazy singleton. This is when the bean creation pipeline from Part 2 actually executes for your application beans.
Tip: If your application is slow to start, the bottleneck is almost always in step 5 (classpath scanning) or step 11 (singleton instantiation). Spring Boot's startup actuator tracks timing per step — check
spring.context.beans.post-processandspring.context.refreshstartup steps.
ConfigurationClassPostProcessor: Driving Annotation-Based Configuration
ConfigurationClassPostProcessor is arguably the most complex class in Spring. At over 1,100 lines, it implements both BeanDefinitionRegistryPostProcessor and BeanFactoryPostProcessor, giving it hooks at two different phases of step 5:
Its imports reveal the scope of its responsibilities — it touches AOT code generation (lines 44–75), bean registration, annotation processing, and even property source handling:
The processing flow delegates to ConfigurationClassParser:
flowchart TD
CCPP["ConfigurationClassPostProcessor<br/>postProcessBeanDefinitionRegistry()"]
CCPP --> FindCandidates["Find @Configuration candidates<br/>in existing bean definitions"]
FindCandidates --> CCP["ConfigurationClassParser.parse()"]
CCP --> ProcessConfig["Process @Configuration class"]
ProcessConfig --> CS["@ComponentScan<br/>→ ClassPathBeanDefinitionScanner"]
ProcessConfig --> Import["@Import<br/>→ Recursive processing"]
ProcessConfig --> Bean["@Bean methods<br/>→ BeanDefinition registration"]
ProcessConfig --> PS["@PropertySource<br/>→ PropertySource registration"]
ProcessConfig --> IC["@ImportResource<br/>→ XML import"]
ProcessConfig --> BR["BeanRegistrar<br/>→ Programmatic registration"]
Import --> IS["ImportSelector<br/>(Spring Boot auto-config)"]
Import --> IBR["ImportBeanDefinitionRegistrar"]
Import --> RecurseConfig["Another @Configuration<br/>→ Recurse"]
CS --> NewConfigs["Discovered @Configuration classes"]
NewConfigs --> CCP
The parser is recursive. When it encounters @ComponentScan, it triggers the classpath scanner, which discovers new @Configuration classes that need processing. When it encounters @Import, it follows the import chain recursively. This is how a single @SpringBootApplication annotation can pull in hundreds of auto-configuration classes.
ConfigurationClassParser handles @Conditional annotations by evaluating them before processing a configuration class. This is the mechanism that powers Spring Boot's conditional auto-configuration — @ConditionalOnClass, @ConditionalOnMissingBean, etc.
Deferred Import Processing: Spring Boot's Entry Point
The parser processes ImportSelector implementations in two ways:
- Regular
ImportSelector— processed immediately during the parsing phase. DeferredImportSelector— collected and processed after all other configuration classes.
Spring Boot's AutoConfigurationImportSelector implements DeferredImportSelector. This ensures that user-defined beans are registered first, so @ConditionalOnMissingBean checks can correctly detect whether the user has already provided a custom implementation.
This subtle ordering guarantee is what makes Spring Boot's "opinionated defaults with easy overrides" pattern possible. Without deferred processing, auto-configuration would race with user configuration and produce unpredictable results.
BeanRegistrar: Programmatic Bean Registration for the AOT Era
Spring 7.x introduces a new API for registering beans programmatically:
spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java#L22-L80
class MyBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean("foo", Foo.class);
registry.registerBean("bar", Bar.class, spec -> spec
.prototype()
.lazyInit()
.supplier(context -> new Bar(context.bean(Foo.class))));
}
}
Why add another registration mechanism? The answer is AOT (Ahead-of-Time) compilation, which we'll cover in detail in Part 6. Traditional @Bean methods rely on runtime reflection — the framework must reflectively invoke a method to create the bean. BeanRegistrar provides explicit, code-level bean creation via supplier() lambdas, which can be analyzed and optimized at build time.
flowchart LR
subgraph "@Configuration + @Bean"
A1["Runtime reflection"] --> A2["Invoke method reflectively"]
A2 --> A3["Return bean instance"]
end
subgraph "BeanRegistrar"
B1["Explicit supplier lambda"] --> B2["Direct constructor call"]
B2 --> B3["Return bean instance"]
end
A1 -.->|"AOT must generate<br/>reflection hints"| AOT1["AOT overhead"]
B1 -.->|"Already AOT-friendly<br/>no reflection needed"| AOT2["Zero AOT overhead"]
A BeanRegistrar is integrated into ConfigurationClassParser via @Import:
@Configuration
@Import(MyBeanRegistrar.class)
class MyConfiguration { }
Importantly, BeanRegistrar implementations are not Spring components — they have no-arg constructors and cannot use dependency injection. This constraint is intentional: it means they can be instantiated trivially during AOT processing.
Tip: The
BeanRegistrarJavadoc explicitly states that annotating an implementation with@Componentor returning it from@Beanwill register it as a bean but will not invoke itsregister()method. Only@Importtriggers registration.
The Synchronized Shutdown Safety Net
Notice that refresh() acquires a startupShutdownLock (line 583) and wraps the entire sequence in a try-finally block. If any step throws a RuntimeException or Error, the catch block at line 628 performs cleanup:
- Stops any already-started
Lifecyclebeans - Calls
destroyBeans()to clean up any singletons created before the failure - Calls
cancelRefresh()to reset the active flag
This ensures that a failed startup leaves no dangling resources — partially created database connections, thread pools, or network listeners are all properly shut down.
What's Next
We've now traced the complete startup sequence from refresh() through ConfigurationClassPostProcessor and into BeanRegistrar. In the next article, we'll explore what happens when those beans need cross-cutting behavior — how Spring's AOP proxy infrastructure transparently wraps beans with @Transactional, @Async, and other interceptors during the postProcessAfterInitialization phase we identified in Part 2.