Read OSS

How Spring Boots Up: The refresh() Sequence and @Configuration Class Processing

Advanced

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 a DefaultListableBeanFactory and registers an AnnotatedBeanDefinitionReader.
  • GenericWebApplicationContext — for web applications, typically created by Spring Boot's SpringApplication.

The 13-Step refresh() Sequence

The refresh() method is the single most important method in Spring Framework. Let's examine it in full:

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java#L582-L662

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-process and spring.context.refresh startup 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:

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java#L133-L139

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:

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java#L44-L75

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:

  1. Regular ImportSelector — processed immediately during the parsing phase.
  2. 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 BeanRegistrar Javadoc explicitly states that annotating an implementation with @Component or returning it from @Bean will register it as a bean but will not invoke its register() method. Only @Import triggers 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:

  1. Stops any already-started Lifecycle beans
  2. Calls destroyBeans() to clean up any singletons created before the failure
  3. 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.