3.1. What Is an ApplicationContext and Why There Are Several
ApplicationContext is an extended BeanFactory that adds:
- ApplicationEvent support (publish/subscribe)
- MessageSource (i18n)
- ResourceLoader (file access)
- Automatic registration of BeanPostProcessors / BeanFactoryPostProcessors
- Context hierarchy (parent/child)
In Spring Boot there are three basic context types — one per WebApplicationType:
| WebApplicationType | Context class | Module (Boot 4) |
|---|---|---|
| SERVLET | AnnotationConfigServletWebServerApplicationContext | spring-boot-webmvc |
| REACTIVE | AnnotationConfigReactiveWebServerApplicationContext | spring-boot-webflux |
| NONE | AnnotationConfigApplicationContext | spring-boot (core) |
The key difference between AnnotationConfigServletWebServerApplicationContext and a plain AnnotationConfigApplicationContext is that the former knows how to create and manage an embedded web server (Tomcat/Jetty). It extends ServletWebServerApplicationContext, which implements WebServerApplicationContext and contains methods like createWebServer() and getWebServer().
3.2. createApplicationContext() — Choosing the Context
This step in SpringApplication.run() looks like this:
protected ConfigurableApplicationContext createApplicationContext() {
return this.applicationContextFactory.create(this.webApplicationType);
}
ApplicationContextFactory is a functional interface (since Spring Boot 2.4) responsible for creating the context based on the application type:
@FunctionalInterface
public interface ApplicationContextFactory {
ConfigurableApplicationContext create(WebApplicationType webApplicationType);
// ... factory methods of(...)
}
The default implementation (ApplicationContextFactory.DEFAULT) looks roughly like this:
ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
switch (webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
} catch (Exception ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext instance, "
+ "you may need a custom ApplicationContextFactory", ex);
}
};
How to customize it
// Approach 1: via SpringApplication
SpringApplication app = new SpringApplication(MyApp.class);
app.setApplicationContextFactory(ctx -> new MyCustomContext());
// Approach 2: via SpringApplicationBuilder
new SpringApplicationBuilder(MyApp.class)
.contextFactory(ApplicationContextFactory.of(MyCustomContext::new))
.run(args);
ApplicationContextFactory.of(Supplier<ConfigurableApplicationContext>) creates a factory that simply calls the supplied Supplier.
Gotcha. The context you return must be "raw" — Spring Boot itself will call
setEnvironment(),prepareContext(), andrefresh(). If you return an already refreshed context, you'll end up with double initialization.
3.3. prepareContext() — Preparing the Context Before refresh
This is the busiest step. Here is its full skeleton (Spring Boot 3.x):
private void prepareContext(DefaultBootstrapContext bootstrapContext,
ConfigurableApplicationContext context,
ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments,
Banner printedBanner) {
// 1. Set the Environment
context.setEnvironment(environment);
// 2. Post-process the context (bean name generator, resource loader, ConversionService)
postProcessApplicationContext(context);
// 3. Apply the ApplicationContextInitializers
applyInitializers(context);
// 4. Publish ApplicationContextInitializedEvent
listeners.contextPrepared(context);
// 5. Log startup info and profiles
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 6. Register Boot-specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
// 7. Configure allowBeanDefinitionOverriding
if (beanFactory instanceof DefaultListableBeanFactory dlbf) {
dlbf.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
// 8. Lazy initialization
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(
new LazyInitializationBeanFactoryPostProcessor());
}
// 9. Load all sources (the main @Configuration class, etc.)
Set<Object> sources = getAllSources();
load(context, sources.toArray(new Object[0]));
// 10. Publish ApplicationPreparedEvent
listeners.contextLoaded(context);
}
We briefly touched on this step in Part 1 — now let's break down the key steps in depth.
Step 1: context.setEnvironment(environment)
The Environment prepared in Part 2 is handed over to the context. From this moment on, EnvironmentAware beans and @Value annotations can read properties.
Step 2: postProcessApplicationContext()
protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
if (this.beanNameGenerator != null) {
context.getBeanFactory().registerSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,
this.beanNameGenerator);
}
if (this.resourceLoader != null) {
if (context instanceof GenericApplicationContext gac) {
gac.setResourceLoader(this.resourceLoader);
}
context.getBeanFactory().registerResolvableDependency(
ResourceLoader.class, this.resourceLoader);
}
if (this.addConversionService) {
context.getBeanFactory().setConversionService(
ApplicationConversionService.getSharedInstance());
}
}
Three things:
- BeanNameGenerator — the bean naming strategy (default:
AnnotationBeanNameGenerator). - ResourceLoader — where to read resources from (default:
DefaultResourceLoader). - ConversionService —
ApplicationConversionServicefor type conversion (used by@ConfigurationPropertiesand@Value).
Step 3: applyInitializers() — Extension Point #1
protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(
initializer.getClass(), ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context,
"Unable to call initializer.");
initializer.initialize(context);
}
}
ApplicationContextInitializer is an SPI interface from Spring Framework:
@FunctionalInterface
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
void initialize(C applicationContext);
}
It is invoked before refresh(), while the context is still empty. Typical scenarios:
- Programmatically activating profiles:
context.getEnvironment().addActiveProfile("dev") - Registering a
PropertySourcefrom a non-standard source - Setting a parent context
Registration via spring.factories:
# META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer=\
com.example.MyInitializer
Programmatic registration:
SpringApplication app = new SpringApplication(MyApp.class);
app.addInitializers(ctx -> ctx.getEnvironment().addActiveProfile("metrics"));
Note: the built-in Spring Boot
ApplicationContextInitializers (e.g.,ConfigurationWarningsApplicationContextInitializer,ContextIdApplicationContextInitializer) are loaded in theSpringApplicationconstructor, not here.applyInitializers()invokes both them and the user-added ones.
Step 4: listeners.contextPrepared()
ApplicationContextInitializedEvent is published. At this point the context has been created, the Environment is set, and the initializers have been applied — but no beans have been loaded yet. This is the last chance to tamper with the context without risking breaking the dependency graph.
Step 5: logging the profiles
protected void logStartupProfileInfo(ConfigurableApplicationContext context) {
Log log = getApplicationLog();
if (log.isInfoEnabled()) {
String[] activeProfiles = context.getEnvironment().getActiveProfiles();
// ... prints: "The following 1 profile is active: \"dev\""
}
}
This is the exact line you see in the logs at startup:
The following 1 profile is active: "dev"
Step 6: registering singleton beans
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
beanFactory.registerSingleton("springBootBanner", printedBanner);
These objects are not @Components — they are registered manually. Internally, registerSingleton() calls addSingleton() in DefaultSingletonBeanRegistry, i.e., it puts the object straight into the singleton cache, bypassing the regular bean lifecycle.
This means:
- They cannot be proxied by a
BeanPostProcessor - They don't go through
@PostConstruct/InitializingBean - But they can be injected via
@Autowired ApplicationArguments
Step 7: allowBeanDefinitionOverriding
if (beanFactory instanceof DefaultListableBeanFactory dlbf) {
dlbf.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
The default since Boot 2.1 is false. If two @Bean methods define a bean with the same name → BeanDefinitionOverrideException. You can allow it via spring.main.allow-bean-definition-overriding=true.
Step 8: getAllSources() + load() — entering the world of BeanDefinitions
protected Set<Object> getAllSources() {
Set<Object> allSources = new LinkedHashSet<>();
if (!CollectionUtils.isEmpty(this.primarySources)) {
allSources.addAll(this.primarySources);
}
if (!CollectionUtils.isEmpty(this.sources)) {
allSources.addAll(this.sources);
}
return Collections.unmodifiableSet(allSources);
}
primarySources is your @SpringBootApplication class. Additional sources can be added via SpringApplicationBuilder.sources(...).
Then — load():
protected void load(ApplicationContext context, Object[] sources) {
BeanDefinitionLoader loader = createBeanDefinitionLoader(
getBeanDefinitionRegistry(context), sources);
// ...
loader.load();
}
Step 9: listeners.contextLoaded()
ApplicationPreparedEvent is published. The context is ready for refresh().
3.4. BeanDefinitionLoader — the Facade for Bean Loading
This is the key class that turns your @Configuration classes into BeanDefinitions. It's not part of Spring Framework — it belongs to Spring Boot.
What it can do
BeanDefinitionLoader is a facade over three readers:
| Source | Reader |
|---|---|
Class<?> (your @Configuration) |
AnnotatedBeanDefinitionReader |
Resource (an XML file) |
XmlBeanDefinitionReader |
Package (a base package) |
ClassPathBeanDefinitionScanner |
CharSequence (a string) |
Tries Class → Resource → Package |
How it's structured
class BeanDefinitionLoader {
private final BeanDefinitionRegistry registry;
private final AnnotatedBeanDefinitionReader annotatedReader;
private final XmlBeanDefinitionReader xmlReader;
private final ClassPathBeanDefinitionScanner scanner;
BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
this.registry = registry;
this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
this.xmlReader = new XmlBeanDefinitionReader(registry);
this.scanner = new ClassPathBeanDefinitionScanner(registry, false);
// ... setEnvironment, setResourceLoader
}
}
Note that the ClassPathBeanDefinitionScanner is created with the flag false — no default include filters. This means it does not scan for @Components automatically until you call scan(). The actual scanning happens later, when ConfigurationClassPostProcessor processes the @ComponentScan from your @SpringBootApplication.
How load() works
int load() {
int count = 0;
for (Object source : this.sources) {
count += load(source);
}
return count;
}
private int load(Object source) {
if (source instanceof Class<?> clazz) return load(clazz);
if (source instanceof Resource res) return load(res);
if (source instanceof Package pkg) return load(pkg);
if (source instanceof CharSequence cs) return load(cs);
throw new IllegalArgumentException("Invalid source type " + source.getClass());
}
For your @SpringBootApplication class, load(Class<?>) gets called:
private int load(Class<?> source) {
if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
// ... Groovy
}
if (isComponent(source)) {
this.annotatedReader.register(source);
return 1;
}
return 0;
}
In other words, if the class is marked @Component (and @SpringBootApplication includes @Component via meta-annotations), it is registered through AnnotatedBeanDefinitionReader. An AnnotatedGenericBeanDefinition is created, which stores the annotation metadata.
Important: at this point only the
BeanDefinitionfor the@SpringBootApplicationclass itself is created. All the other beans (services, controllers,@Beanmethods) will be registered later — duringrefresh(), whenConfigurationClassPostProcessorprocesses@ComponentScan,@Import,@Bean, and auto-configuration.
3.5. Diagram: from createApplicationContext() to ApplicationPreparedEvent
run()
│
├─ createApplicationContext()
│ └─ ApplicationContextFactory.create(webApplicationType)
│ ├─ SERVLET → AnnotationConfigServletWebServerApplicationContext
│ ├─ REACTIVE → AnnotationConfigReactiveWebServerApplicationContext
│ └─ NONE → AnnotationConfigApplicationContext
│
├─ context.setApplicationStartup()
│
└─ prepareContext()
│
├─ context.setEnvironment(environment)
│
├─ postProcessApplicationContext()
│ ├─ BeanNameGenerator
│ ├─ ResourceLoader
│ └─ ConversionService
│
├─ applyInitializers()
│ └─ ApplicationContextInitializer.initialize(context)
│ (ConfigurationWarnings, ContextId, custom ones)
│
├─ listeners.contextPrepared()
│ └─ ApplicationContextInitializedEvent
│
├─ logStartupProfileInfo()
│
├─ beanFactory.registerSingleton("springApplicationArguments")
├─ beanFactory.registerSingleton("springBootBanner")
│
├─ setAllowBeanDefinitionOverriding()
│
├─ getAllSources() → {MyApplication.class}
│
├─ load(context, sources)
│ └─ BeanDefinitionLoader.load()
│ └─ AnnotatedBeanDefinitionReader.register(MyApplication.class)
│ → BeanDefinition for @SpringBootApplication
│
└─ listeners.contextLoaded()
└─ ApplicationPreparedEvent
│
▼
refreshContext() ← Part 5
3.6. Spring Boot 3 vs Spring Boot 4: What Changed
ApplicationContextFactory — removed
In Spring Boot 3, ApplicationContextFactory is a public interface, part of the spring-boot core. In Spring Boot 4, it has been removed. Instead, a modular system is at work: each module (spring-boot-webmvc, spring-boot-webflux, spring-boot-core) registers its own context via META-INF/spring/...imports and spring.factories.
AbstractApplicationContextFactory is marked as @Deprecated(since="6.0", forRemoval=true) and is scheduled for removal. In other words, a transition period: it's still there in 3.x, gone in 4.x.
prepareContext() — almost unchanged
The logic of prepareContext() has stayed the same in Boot 4. The only thing that changed is where the context comes from — in Boot 3, ApplicationContextFactory was a field of SpringApplication, while in Boot 4 the context is created via the modular ApplicationContextFactory shipped with the corresponding starter.
BeanDefinitionLoader — unchanged
This class hasn't changed in either 3.x or 4.x. It's still package-private, still a facade over the three readers.
Starters
| Boot 3 | Boot 4 |
|---|---|
spring-boot-starter-web |
spring-boot-starter-webmvc |
spring-boot-starter-webflux |
spring-boot-starter-webflux (unchanged) |
3.7. Key Takeaways
-
ApplicationContextFactoryis the context-selection strategy. In Boot 3 it's a public interface; in Boot 4 it's removed in favor of modularity. -
prepareContext()is 10 steps, each of which can be extended: Initializer, BeanNameGenerator, ResourceLoader, ConversionService. -
ApplicationContextInitializeris the only place where you can intervene in the context before any beans are loaded. -
BeanDefinitionLoaderdoesn't create beans — it only registers theBeanDefinitionfor the primary source. The rest is the job ofConfigurationClassPostProcessorinsiderefresh(). -
springApplicationArgumentsandspringBootBannerare registered as singletons directly, bypassingBeanPostProcessors. -
ApplicationPreparedEventis the last event beforerefresh(). After it, the context is "frozen" against external changes. - Boot 4:
ApplicationContextFactoryis gone, the context is determined by the modules.prepareContext()andBeanDefinitionLoaderare unchanged.
Top comments (0)