What this post covers
This is the twelfth post in the "Digging into Spring's Source" series. With Part 11 we finished the main path through the container (Phase 1) and AOP (Phase 2). This post is U08, the last unit of Phase 1, which the map in Part 1 left off the main path. Part 7 said that ApplicationContext inherits a few more things besides the BeanFactory family of interfaces. This time we look at three of those few.
There are three questions to answer.
- When
publishEvent()is called, who finds the listeners, in what order are they called, and when does an@EventListenermethod become a listener? - Where does
Environmentlook for a configuration value, in what order, and how does a profile get switched on? - How does a single string such as
"classpath:…"become aResourceobject that does not care whether it is a file, a class path entry or a URL?
| Item | Details |
|---|---|
| Source | Spring Framework 7.1.0-SNAPSHOT |
| Commit | f447f3c310 (2026-10-01, main branch) |
| Modules read most | spring-context (context, context.event), spring-core (core.env, core.io, core.io.support) |
| Classes read most | AbstractApplicationContext, SimpleApplicationEventMulticaster, EventListenerMethodProcessor, AbstractEnvironment, PropertySourcesPropertyResolver, DefaultResourceLoader |
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.
Three topics share one post, so this post keeps to the main thread. For each topic it goes just far enough that someone seeing it for the first time can tell "what is where, and why it is built that way". What was left out is named at the end, under "What this post leaves out".
Words to know first
An event is an object that says "this just happened". The sender does not know who receives it, and the receiver does not know who sent it. The receiver is called a listener. In Spring the method that sends an event is publishEvent(), the receiving interface is ApplicationListener, and the annotation you put on a receiving method is @EventListener.
A multicaster is the object that hands one event to many listeners. The context does not run the listeners itself; it delegates to this object. In the source it is the ApplicationEventMulticaster interface and the SimpleApplicationEventMulticaster class.
Environment is the object that describes "what surroundings is this program running in". It holds two things: properties, name-value pairs such as app.name=…, and profiles, names given to kinds of surroundings such as dev and prod. It is the object that @Profile asked through Environment.matchesProfiles() in Part 9.
A PropertySource is one "source" of properties. The system properties are one, the environment variables are another, one configuration file is another. Environment keeps these sources in an ordered list, and the order is the precedence.
A Resource is a handle to "something that can be read". It may be a file, an entry on the class path inside a JAR, or a URL, but the caller reads it through one method, getInputStream(). The thing that takes a string ("classpath:a.properties") and turns it into a Resource is a ResourceLoader.
The twelve steps of refresh() are the order in which the container starts up. This post revisits ① prepareRefresh, ③ prepareBeanFactory, ⑧ initApplicationEventMulticaster, ⑩ registerListeners, ⑪ finishBeanFactoryInitialization and ⑫ finishRefresh. The whole sequence is in Part 7.
1. What ApplicationContext adds on top of BeanFactory
The declaration of the ApplicationContext interface is one line spread over two.
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
spring-context/src/main/java/org/springframework/context/ApplicationContext.java, lines 59–60
Of the six interfaces, ListableBeanFactory and HierarchicalBeanFactory are the BeanFactory family from Part 3. The other four are the "additions", and this post covers three of them.
Grey is covered elsewhere or not at all; blue is the three topics of this post.
| Interface | What it adds | Section |
|---|---|---|
ApplicationEventPublisher |
publishEvent(Object) |
2, 3 |
EnvironmentCapable |
getEnvironment() |
4 |
ResourcePatternResolver (→ ResourceLoader) |
getResource(String), getResources(String) |
5 |
MessageSource |
getMessage(…) — internationalised messages |
not covered |
On the implementation side is AbstractApplicationContext. Its declaration shows that it extends DefaultResourceLoader (lines 145–146): the context is itself a resource loader. Events are delegated to the multicaster, pattern lookups to a ResourcePatternResolver, and the Environment is kept in a field (line 224, line 233, lines 338–352).
The reason a bean can simply @Autowired any of the three is in one place too. Step ③ of refresh(), prepareBeanFactory(), registers the context itself as the injection candidate for the types ResourceLoader, ApplicationEventPublisher and ApplicationContext (lines 748–751). The Environment is registered as a singleton named "environment" (lines 764–766).
2. Events: the journey of one publishEvent call
Event and publisher — the interfaces are tiny
ApplicationEvent is an abstract class extending Java's standard EventObject; its constructor just takes "who raised it" (source) and records the time (line 31, lines 47–50). The sending interface, ApplicationEventPublisher, has essentially one method, publishEvent(Object) (line 88); the overload taking an ApplicationEvent is a default method that forwards to it (lines 64–66). The receiving interface, ApplicationListener, also has one method, onApplicationEvent(E) (lines 43–49). The generic E says which event the listener cares about.
publishEvent — wrap, park or send, and pass to the parent
Both public publishEvent() methods of the context funnel into one protected method (lines 381–396). Here is its body, with the type-hint handling cut to // ....
protected void publishEvent(Object event, @Nullable ResolvableType typeHint) {
Assert.notNull(event, "Event must not be null");
ResolvableType eventType = null;
// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent applEvent) {
applicationEvent = applEvent;
eventType = typeHint;
}
else {
// ...
applicationEvent = new PayloadApplicationEvent<>(this, event, payloadType);
}
// ...
// 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);
}
// Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext abstractApplicationContext) {
abstractApplicationContext.publishEvent(event, typeHint);
}
else {
this.parent.publishEvent(event);
}
}
}
Yellow is storage (the set and the cache), blue is procedure.
Three things to remember.
- A plain object becomes an event too. Anything that is not an
ApplicationEventis wrapped in aPayloadApplicationEvent(line 39; the original comes back out viagetPayload(), line 80). So you can hand arecordstraight topublishEvent(). - If there is no multicaster yet, the event is parked.
earlyApplicationEventsis created as an empty set in step ① ofrefresh(),prepareRefresh()(line 702), and set back tonullin step ⑩,registerListeners()(section 3). Events published in between go into the set and leave all at once in step ⑩. - The parent context gets it too. If contexts are nested parent-child, an event published in the child also reaches the parent's listeners. Not the other way round.
The multicaster — picks the listeners and calls them in the same thread
The place that actually calls listeners is SimpleApplicationEventMulticaster.multicastEvent().
@Override
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : ResolvableType.forInstance(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null && listener.supportsAsyncExecution()) {
try {
executor.execute(() -> invokeListener(listener, event));
}
catch (RejectedExecutionException ex) {
// Probably on shutdown -> invoke listener locally instead
invokeListener(listener, event);
}
}
else {
invokeListener(listener, event);
}
}
}
Unless you set an executor it is null, so the else branch runs and the listeners execute one after another in the very thread that published the event. The class Javadoc says so too: "By default, all listeners are invoked in the calling thread" (lines 41–44). invokeListener() wraps the call in an error handler if one is set, otherwise it simply calls listener.onApplicationEvent(event) (lines 162–180). In step ⑧ the context uses a bean named "applicationEventMulticaster" if there is one, otherwise it creates this class and registers it under that name (lines 854–871).
getApplicationListeners(event, type) is where "the listeners for this event" are chosen. It lives in the parent class AbstractApplicationEventMulticaster and first checks a cache (retrieverCache) keyed by event type and source type (line 72, lines 188–223). On a miss it filters every registered listener with supportsEvent(), sorts them with AnnotationAwareOrderComparator.sort() and stores the result in the cache (lines 233–327, the sort at line 315). That sort is where @Order decides listener order. Adding or removing a listener clears the whole cache (line 114).
supportsEvent() asks the listener directly if it is a GenericApplicationListener; for a plain ApplicationListener<E> it wraps it in an adapter that reads the generic E and compares (lines 393–399). SmartApplicationListener (line 37) and its child GenericApplicationListener (line 41) are extended interfaces that answer "which events do you want" and "what order" through methods such as supportsEventType() and getOrder() instead of generics. The @EventListener adapter in section 3 implements the latter.
Easy to get wrong
publishEvent()returns only when the listeners are done. With the default set-up everything runs synchronously in the same thread. A slow listener makes the publisher wait just as long, and an exception thrown by a listener propagates up to the code that calledpublishEvent()(wheninvokeListener()has no error handler it throws as is).- There is one multicaster per context. Passing the event on to the parent is done by the context's
publishEvent(), not by the multicaster. - The line between "send now" and "park it" is step ⑩. Section 3 shows the trap this creates, with an example.
3. When and how listeners get registered
There are three roads into the multicaster, and they are taken at different moments.
The numbers are the twelve steps of Part 7. The red box is the trap of this section.
Road 1: ApplicationListener beans — step ⑩, registerListeners()
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);
}
}
}
Beans that implement ApplicationListener are registered here by name. Only the name is recorded, not the bean, because — as the comment says — the post-processors have not all been applied yet. Then the events parked since step ① are flushed. At this moment the only listeners the multicaster knows are the ApplicationListener beans registered by name.
Road 2: catching inner beans too — ApplicationListenerDetector from step ③
Looking up by name with getBeanNamesForType() sees top-level beans only. So step ③, prepareBeanFactory(), registers a BeanPostProcessor called ApplicationListenerDetector (line 754); every time a bean is created, in the after-initialisation hook, it adds any singleton that is an ApplicationListener to the multicaster as an object (lines 75–95). A listener registered both by name and as an object is reconciled into one when the list is built (AbstractApplicationEventMulticaster lines 261–298). It is the "slip in a post-processor" technique from Part 6.
Road 3: @EventListener methods — EventListenerMethodProcessor at the end of step ⑪
@EventListener is handled by two beans that AnnotationConfigUtils from Part 8 registers alongside the other annotation processors: EventListenerMethodProcessor and DefaultEventListenerFactory (AnnotationConfigUtils lines 192–202).
One method becomes one listener object.
EventListenerMethodProcessor implements SmartInitializingSingleton (lines 65–66). That interface's afterSingletonsInstantiated() is called once by preInstantiateSingletons() in step ⑪, after every singleton has been built (DefaultListableBeanFactory lines 1142–1150). Inside, it walks every bean name to find its class (lines 108–150), looks for methods annotated with @EventListener on each class, has the factory create a listener per method, and adds it with context.addApplicationListener() (lines 182–195).
DefaultEventListenerFactory accepts every method (supportsMethod always returns true) and creates an ApplicationListenerMethodAdapter (lines 50–57). That adapter is "a listener wrapped around a method" and implements GenericApplicationListener (line 75). Its constructor reads several values up front, three of which matter here (lines 112–126): the event types it wants (the annotation's classes if given, otherwise the type of the first parameter, lines 128–151), the condition string, and the method's @Order (lowest precedence if absent, lines 153–156).
When an event arrives, processEvent() runs.
public void processEvent(ApplicationEvent event) {
@Nullable Object[] args = resolveArguments(event);
if (shouldHandle(event, args)) {
Object result = doInvoke(args);
if (result != null) {
handleResult(result);
}
else {
logger.trace("No result object given - no result to handle");
}
}
}
resolveArguments(): if the method parameter is not anApplicationEventand the event is aPayloadApplicationEvent, it takes the object out and passes that (lines 309–326). What section 2 wrapped is unwrapped here, which is why a method like@EventListener void on(Greeting g)can take a plain type directly.shouldHandle(): if there is acondition, it is evaluated as SpEL and afalsemeans the method is not called (lines 291–301). SpEL itself is the topic of Part 15.handleResult(): if the method returned something, that something is published as a new event. For an array or a collection, each element is published separately (lines 328–371). The annotation's Javadoc says the same (EventListenerlines 48–51).
The four events the context publishes about itself
The context publishes events when its own state changes. All four classes extend ApplicationContextEvent (line 30), and the source is the context itself.
| Event | Published from | Where |
|---|---|---|
ContextRefreshedEvent |
the last line of step ⑫, finishRefresh() |
line 1017 |
ContextStartedEvent |
start() |
lines 1590–1593 |
ContextStoppedEvent |
stop() |
lines 1596–1599 |
ContextClosedEvent |
the first thing doClose() does, before beans are destroyed |
line 1191 |
If you want to do something "once every bean is ready", listening for ContextRefreshedEvent is the standard way: the singletons are all built in step ⑪ and the event goes out at the end of step ⑫.
Example 1: both kinds of listener, order, thread, return value
We publish one event class (OrderPlacedEvent) and one plain record (Greeting). The listeners are an ApplicationListener bean (@Order(2)) and several @EventListener methods (one with @Order(1), one with a condition, one with a return value, one receiving the four context events). To try publishing in step ⑤, there is also a BeanFactoryPostProcessor bean. The full code is ex/u08/EventExample.java; here is the core.
// An example written for this post (not Spring's code)
@Order(2)
@Component
static class OrderListener implements ApplicationListener<OrderPlacedEvent> {
@Override
public void onApplicationEvent(OrderPlacedEvent event) {
System.out.println(" [ApplicationListener @Order(2)] order=" + event.getOrderId()
+ " thread=" + Thread.currentThread().getName());
}
}
@Component
static class AnnotatedListeners {
@Order(1)
@EventListener
public Greeting onOrder(OrderPlacedEvent event) {
System.out.println(" [@EventListener @Order(1)] order=" + event.getOrderId()
+ " thread=" + Thread.currentThread().getName());
return new Greeting("order " + event.getOrderId() + " received"); // the return value becomes a new event
}
@EventListener
public void onGreeting(Greeting greeting) { // receives the plain object as is
System.out.println(" [@EventListener Greeting] " + greeting.text());
}
@EventListener(condition = "#event.orderId == 'A-2'")
public void onSecondOrderOnly(OrderPlacedEvent event) {
System.out.println(" [@EventListener condition] order=" + event.getOrderId());
}
// ... plus onContextEvent, which takes the four context events via classes and prints the class name
}
// A bean that publishes in step 5 of refresh() — before the multicaster exists
@Component
static class EarlyPublisher implements BeanFactoryPostProcessor, ApplicationEventPublisherAware {
// ... publisher is received through ApplicationEventPublisherAware
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
System.out.println("publishEvent(EARLY) from BeanFactoryPostProcessor");
publisher.publishEvent(new OrderPlacedEvent(this, "EARLY"));
}
}
main creates the context, publishes OrderPlacedEvent twice (A-1, A-2) and Greeting once, then calls start(), stop() and close(). The output:
main thread=main
publishEvent(EARLY) from BeanFactoryPostProcessor
[ApplicationListener @Order(2)] order=EARLY thread=main
[context event] ContextRefreshedEvent
publishEvent(OrderPlacedEvent A-1)
[@EventListener @Order(1)] order=A-1 thread=main
[@EventListener Greeting] order A-1 received
[ApplicationListener @Order(2)] order=A-1 thread=main
publishEvent(OrderPlacedEvent A-2)
[@EventListener @Order(1)] order=A-2 thread=main
[@EventListener Greeting] order A-2 received
[ApplicationListener @Order(2)] order=A-2 thread=main
[@EventListener condition] order=A-2
publishEvent(Greeting) - plain object
[@EventListener Greeting] hello
start() / stop() / close()
[context event] ContextStartedEvent
[context event] ContextStoppedEvent
[context event] ContextClosedEvent
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 output confirms the following.
- Everything is on the
mainthread. No executor was set, so this is theelsebranch of section 2. - The
@Order(1)method runs before the@Order(2)class. Both kinds of listener are sorted together in one list. - The returned
Greetingbecame an event immediately, and was handled before the@Order(2)listener. The momenthandleResult()callspublishEvent(),multicastEvent()runs once more, while the loop over the original event's listeners is paused. Those are the lines with the extra indentation. - The conditional listener fired only for
A-2. It has no@Order, so it comes last. - The
EARLYevent reached only theApplicationListenerbean. There is no@EventListener @Order(1)line and noGreetingline for it. The reason is the red box in the diagram: an event published in step ⑤ is flushed in step ⑩,registerListeners(), but@EventListenermethods only become listeners at the end of step ⑪. - The four context events appeared one each at the end of
refresh(),start(),stop()andclose().
Easy to get wrong
@EventListeneris only complete "once the container is fully up". That is point 5 above. An event published while beans are still being created silently misses@EventListenermethods. Such events must be received by a bean that implementsApplicationListener.- An
@EventListenermethod without@Ordergoes last. The default ofresolveOrder()isLOWEST_PRECEDENCE. The same holds for anApplicationListenerclass without@Order. - If the bean carrying
@EventListeneris wrapped in a proxy, the methods are looked up on the original class, not the proxy. That is whyafterSingletonsInstantiated()usesAutoProxyUtils.determineTargetClass()to find the target class (line 116). Proxies are in Part 10.
4. Environment: where do configuration values come from?
Three layers of interface
public interface Environment extends PropertyResolver {
spring-core/src/main/java/org/springframework/core/env/Environment.java, line 72
| Interface | Job | Typical methods |
|---|---|---|
PropertyResolver |
reading properties | getProperty(key), getProperty(key, default), getRequiredProperty(key), resolvePlaceholders(text) (lines 30–110) |
Environment |
the above plus profiles | getActiveProfiles(), getDefaultProfiles(), matchesProfiles(…) (lines 87–115) |
ConfigurableEnvironment |
the above, made changeable | setActiveProfiles(…), getPropertySources(), getSystemProperties(), merge(parent) (lines 72–157) |
The implementation a context creates by default is StandardEnvironment (AbstractApplicationContext lines 350–352). Its parent AbstractEnvironment does nearly all the work; StandardEnvironment itself only adds two sources.
The PropertySource list — order is precedence
AbstractEnvironment boils down to two fields: the source list, a MutablePropertySources, and the PropertySourcesPropertyResolver that searches that list for values (lines 109–111, lines 135–139). The constructor ends by calling customizePropertySources(), which StandardEnvironment fills in like this.
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
spring-core/src/main/java/org/springframework/core/env/StandardEnvironment.java, lines 95–101
"systemProperties" (System.getProperties(), AbstractEnvironment lines 440–441) goes first, "systemEnvironment" (the OS environment variables) second. MutablePropertySources is a thin wrapper around one CopyOnWriteArrayList (line 43); addFirst() inserts at the front and addLast() at the back (lines 103–118). As their Javadoc says, "highest precedence" and "lowest precedence": earlier wins. A PropertySource itself is an abstract class that holds a name and the underlying source object, with getProperty(name) as its only abstract method (line 62, line 128).
The lookup walks the list from the front and returns the first value found.
protected <T> @Nullable T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
if (this.propertySources != null) {
for (PropertySource<?> propertySource : this.propertySources) {
// ...
Object value = propertySource.getProperty(key);
if (value != null) {
if (resolveNestedPlaceholders) {
if (value instanceof String string) {
value = resolveNestedPlaceholders(string);
}
// ...
}
logKeyFound(key, propertySource, value);
return convertValueIfNecessary(value, targetValueType);
}
}
}
// ...
return null;
}
Each yellow box is one PropertySource. The list is the same as in example 2.
If the value found is a string, resolveNestedPlaceholders() resolves any ${…} inside the value again, and at the end the value is converted to the requested type (conversion is the topic of Part 14). The similar-looking #{…} is not a placeholder but the expression language SpEL, covered in Part 15.
The rules for ${…} live in AbstractPropertyResolver: prefix ${, suffix }, default-value separator : and escape character \ by default (lines 96–102; the constants are in SystemPropertyUtils lines 39–51), with the actual string work delegated to PropertyPlaceholderHelper (lines 280–287). resolvePlaceholders() leaves unresolvable placeholders as they are; resolveRequiredPlaceholders() throws (lines 245–258). The :fallback in ${app.missing:fallback} means "use this if missing".
Where @PropertySource slots in
When the ConfigurationClassParser of Part 8 meets a @PropertySource (lines 314–320), it is eventually PropertySourceProcessor.addPropertySource() that puts the file into the Environment's list: the first file goes at the back (addLast), each following file goes in front of the previous one (addBefore) (lines 136–143). So among files the later-processed one wins, but every file sits behind the system properties and environment variables. That addLast is why a value given as -Dapp.name=… beats the configuration file.
Profiles — just one set of strings in the end
The storage for profiles is a single Set<String> called activeProfiles (line 105). When it is read (doGetActiveProfiles()) and the set is empty, the property spring.profiles.active is looked up once, split on commas and used to fill it (lines 272–283). The key that doGetActiveProfilesProperty() reads is the constant ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active" (line 77, lines 290–292), and since that goes through the same PropertySource list, -Dspring.profiles.active=dev works. The default profiles, used when no profile is active, come from spring.profiles.default, and failing that there is a single name, "default" (line 88, line 100).
The matchesProfiles("dev & !prod") that @Profile calls in Part 9 parses the expression with Profiles.of(...) and passes it to acceptsProfiles(Profiles) (Environment lines 113–115), which asks isProfileActive() about each name in the expression (lines 397–412). The parser is a small class that tokenises on the five characters (, ), &, | and ! (ProfilesParser lines 55–59).
Who resolves @Value("${…}")?
Part 5 said that the string in @Value goes through resolveEmbeddedValue() (DefaultListableBeanFactory lines 1675–1683). That method merely applies the StringValueResolvers registered with the bean factory, one after another (AbstractBeanFactory lines 966–978); the question is who puts one there. The answer is at the top of step ⑪, finishBeanFactoryInitialization().
// 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));
}
If nobody registered one, the context registers Environment.resolvePlaceholders() as a lambda. That is why @Value("${app.name}") resolves against the Environment even without a PropertySourcesPlaceholderConfigurer bean. PropertySourcesPlaceholderConfigurer is a BeanFactoryPostProcessor that assembles its own PropertySources (the Environment plus the files you name) and registers that resolver (line 69, lines 130–157); in this commit there is no code in spring-context that registers it by default (it is not in AnnotationConfigUtils, and example 2 counts zero such beans). The comment's "such as a PropertySourcesPlaceholderConfigurer bean" refers to the case where you register one yourself.
Example 2: when a file and a system property overlap, profiles, defaults
This is ex/u08/EnvironmentExample.java. The configuration file src/main/resources/ex/u08/app.properties has three lines.
app.name=from-properties-file
app.only.in.file=only-here
app.greeting=Hello, ${app.name}!
// An example written for this post (not Spring's code)
@Configuration
@PropertySource("classpath:ex/u08/app.properties") // adds the file to the Environment as a PropertySource
static class AppConfig {
@Bean @Profile("dev") String devOnly() { return "dev bean"; }
@Bean @Profile("prod") String prodOnly() { return "prod bean"; }
}
@Component
static class Settings {
@Value("${app.name}") String name; // a key in both the file and the system properties
@Value("${app.missing:fallback}") String missing; // a key found nowhere — after ':' is the default
@Value("${app.greeting}") String greeting; // a key whose value contains another ${…}
}
public static void main(String[] args) {
System.setProperty("app.name", "from-system-property"); // same key as the file, as a system property
var context = new AnnotationConfigApplicationContext();
context.getEnvironment().setActiveProfiles("dev"); // switch the profile on before refresh()
context.register(AppConfig.class, Settings.class);
context.refresh();
// ... then print the lines below
}
active profiles = [dev]
property sources (first wins):
systemProperties <PropertiesPropertySource>
systemEnvironment <SystemEnvironmentPropertySource>
class path resource [ex/u08/app.properties] <ResourcePropertySource>
app.name = from-system-property
app.only.in.file = only-here
containsBean(devOnly) = true, containsBean(prodOnly) = false
matchesProfiles("dev & !prod") = true
@Value name = from-system-property
@Value missing = fallback
@Value greeting = Hello, from-system-property!
PropertySourcesPlaceholderConfigurer beans = 0
This example was also run on Spring Framework 7.0.9. The list is the two sources of StandardEnvironment plus the one from @PropertySource, and for the shared key app.name the system property, being earlier, won. The ${app.name} inside app.greeting was also resolved to the system property value (nested resolution), and the missing key fell back to the default after :. The @Profile("prod") bean was not registered.
Easy to get wrong
- Profiles must be switched on before
refresh(). The@Profiledecision happens in step ⑤, while the blueprints are read (Part 9). That is why the example does not pass the classes to the constructor but callsregister()andrefresh()separately.-Dspring.profiles.active=devdoes it without code. - Environment variable names may not match as written.
SystemEnvironmentPropertySourceis a class that also findsapp.nameunder forms such asAPP_NAME(line 68). The exact conversion rules were not checked for this post. - With a parent context the Environments are merged.
setParent()merges the parent's Environment into the child's withmerge()(AbstractApplicationContextlines 532–540).
5. Resource: one face for files, the class path and URLs
The interface
public interface Resource extends InputStreamSource {
spring-core/src/main/java/org/springframework/core/io/Resource.java, line 59
InputStreamSource is a one-method interface, getInputStream() (lines 42–57). Resource adds "is it there" (exists(), line 67), "where is it" (getURL(), getURI(), getFile(), lines 113–132), "how big is it" (contentLength()), "what is it called" (getFilename(), getDescription(), lines 236–245) and conveniences such as reading it whole with getContentAsString(charset) (line 201). The implementations differ by "where it reads from".
| Class | Reads from | What exists() does |
Where |
|---|---|---|---|
ClassPathResource |
the class path (a directory or inside a JAR) | whether the class loader can find a URL | line 47, lines 153–155 |
FileSystemResource |
a file system path | File.exists() / Files.exists() |
line 63, lines 168–170 |
UrlResource |
a java.net.URL |
a file: URL as a file, otherwise opens a connection |
line 49, AbstractFileResolvingResource from line 49 |
FileUrlResource |
a file: URL (child of UrlResource, writable too) |
as above | line 47 |
ByteArrayResource |
a byte array in memory | always true |
line 45, lines 83–85 |
From string to Resource — DefaultResourceLoader.getResource()
ResourceLoader is an interface with just getResource(String) and getClassLoader() (line 43, line 98, line 110), and it holds the prefix constants "classpath:" and "classpath*:" (line 50, line 68). The default implementation, DefaultResourceLoader, picks the class by looking at the string in getResource().
@Override
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
// ...
}
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else if (location.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
return new ClassPathAllResource(location.substring(CLASSPATH_ALL_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
URL url = ResourceUtils.toURL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
return getResourceByPath(location);
}
}
}
spring-core/src/main/java/org/springframework/core/io/DefaultResourceLoader.java, lines 154–185
The questions are asked top to bottom and the first match wins. The classpath*: branch is left out of the picture.
The order is the rule. ① Any plugged-in ProtocolResolver gets the first chance (line 64, lines 118–120); ② a leading / goes to getResourceByPath(); ③ classpath: gives a ClassPathResource; ④ classpath*: gives a ClassPathAllResource (new in this commit, marked @since 7.1, lines 203–208); ⑤ anything else is first tried as a URL — file: becomes a FileUrlResource, the rest a UrlResource — and if that fails it goes back to getResourceByPath(). The default getResourceByPath() returns a ClassPathContextResource, which looks on the class path (lines 198–200, line 297). The method is protected so that subclasses such as a web context can change what "a path without a prefix" means.
Finding several — ResourcePatternResolver and classpath*:
ResourcePatternResolver is ResourceLoader plus one method, getResources(String) (line 58, line 69). The implementation, PathMatchingResourcePatternResolver, searches all of the module path and the class path when getResources() gets a classpath*: string, doing a pattern search if the rest contains * or ? and otherwise collecting every entry with that name. For any other string it does a pattern search only if there is a pattern after the prefix, and otherwise returns a single getResource() result in an array (lines 365–397). Its single-resource getResource() delegates straight to the ResourceLoader it was given (lines 356–362).
The connection to the context is three lines. AbstractApplicationContext creates new PathMatchingResourcePatternResolver(this) in its constructor (lines 251–253, lines 514–516) and hands getResources() to it.
@Override
public Resource[] getResources(String locationPattern) throws IOException {
return this.resourcePatternResolver.getResources(locationPattern);
}
Because this was passed in, whenever the pattern resolver needs a single resource it comes back to the context's (that is, DefaultResourceLoader's) getResource(). The context does "find one" itself and delegates "find many". The @ComponentScan of Part 8 uses exactly this getResources("classpath*:…/**/*.class") to sweep up class files.
Example 3: which class do four strings become?
This is ex/u08/ResourceExample.java. It creates a context without refresh() and only calls getResource().
// An example written for this post (not Spring's code)
var context = new AnnotationConfigApplicationContext(); // getResource works without refresh()
String[] locations = {
"classpath:ex/u08/app.properties",
"file:./build.gradle.kts",
"https://example.invalid/app.properties", // a host that does not exist
"ex/u08/app.properties" // no prefix
};
for (String location : locations) {
Resource resource = context.getResource(location);
System.out.println(location + "\n -> " + resource.getClass().getSimpleName()
+ ", exists=" + resource.exists());
}
// ... then read the first line of the classpath: resource with getContentAsString,
// and print the count, file names and classes from getResources("classpath*:ex/u08/*.properties")
classpath:ex/u08/app.properties
-> ClassPathResource, exists=true
file:./build.gradle.kts
-> FileUrlResource, exists=true
https://example.invalid/app.properties
-> UrlResource, exists=false
ex/u08/app.properties
-> ClassPathContextResource, exists=true
first line: # ex.u08 example properties
classpath*:ex/u08/*.properties -> 2 resources
app.properties <FileSystemResource>
extra.properties <FileSystemResource>
This example was also run on Spring Framework 7.0.9. The four strings went down the four branches of the diagram, and the classpath*: pattern found the two .properties files under src/main/resources/ex/u08/. exists() on https://example.invalid/… tried to connect, failed, and returned false (.invalid is a domain that cannot exist).
Easy to get wrong
- With a
file:prefix you get aFileUrlResource, not aFileSystemResource.getResource()tries URL parsing first.FileSystemResourceshows up when a pattern search walks a directory itself (doFindPathMatchingFileResourcesline 1012) or when younewone directly. The last two lines of the output are that case. - A path without a prefix is not "a file in the current directory". In the default context it is read from the class path (
ClassPathContextResource). To read a file, addfile:. exists()is not always free. For an HTTP URL,UrlResource.exists()really sends aHEADrequest (AbstractFileResolvingResourcefrom line 49).
What this post leaves out
Names and locations only. setTaskExecutor() and ApplicationListener.supportsAsyncExecution() for asynchronous listeners (ApplicationListener lines 59–61). The ErrorHandler that swallows an exception thrown by one listener (SimpleApplicationEventMulticaster line 56). What happens when an @EventListener method returns a CompletionStage or a reactive type (ApplicationListenerMethodAdapter lines 328–347). @TransactionalEventListener, which receives after a transaction has ended (spring-tx, Part 17). The context events added in 7.0, ContextPausedEvent and ContextRestartedEvent (package context.event). MessageSource (internationalised messages) and initMessageSource(). validateRequiredProperties() on Environment, and CommandLinePropertySource, which turns command-line arguments into a PropertySource. The path by which a web context overrides initPropertySources() to slot in servlet configuration (AbstractApplicationContext lines 710–712). ResourceEditor, which lets @Value("classpath:…") go straight into a Resource field (line 734). doFindPathMatchingJarResources(), which searches inside JARs (line 865).
Recap
| Concept | In one line |
|---|---|
ApplicationContext |
the BeanFactory family + ApplicationEventPublisher + EnvironmentCapable + ResourcePatternResolver + MessageSource |
publishEvent() |
wraps anything that is not an ApplicationEvent, parks it if there is no multicaster yet, otherwise sends it, and passes it to the parent too |
| Multicaster | caches the listener list per event type, sorts it by @Order and calls each in turn. Synchronously in the publishing thread unless an executor is set |
ApplicationListener beans |
registered by name in step ⑩, registerListeners(). Parked events go out then |
@EventListener |
at the end of step ⑪, afterSingletonsInstantiated() turns each method into an ApplicationListenerMethodAdapter. A return value is a new event |
| Context events | Refreshed (step ⑫), Started, Stopped, Closed (before beans are destroyed) |
Environment |
properties (PropertyResolver) + profiles. Default implementation StandardEnvironment |
PropertySource list |
earlier wins. System properties → environment variables → @PropertySource files |
${key:default} |
PropertyPlaceholderHelper looks it up in the list and resolves any ${…} inside the value too |
| Profiles | one Set<String>. If empty, spring.profiles.active is read. The @Profile decision is made during refresh() |
@Value("${…}") |
if no resolver was registered, step ⑪ registers Environment.resolvePlaceholders() as the default |
Resource |
exists(), getURL(), getFile() on top of getInputStream(). Implementations differ by where they read |
getResource(String) |
/ → path, classpath: → ClassPathResource, a URL → UrlResource/FileUrlResource, otherwise path |
getResources(String) |
the context delegates to PathMatchingResourcePatternResolver. classpath*: means the whole class path |
What to look at next
That completes Phase 1 (the container, U01–U08) and Phase 2 (AOP, U09–U10) of the map in Part 1. Next come the four posts of Phase 3 (foundation technologies). The map called it the layer you "do not read in order but pull out when stuck", and after ten posts of naming its classes and moving on, quite a few have piled up. In this post alone: ResolvableType, which compares a listener's generic type; AnnotatedElementUtils, which finds @EventListener; convertValueIfNecessary(), which turns a property value into the requested type; and the SpEL that evaluates condition.
- Next post: Digging into Spring's Source 13: ResolvableType and MergedAnnotations — Reading Types and Annotations
- Then: Part 14: Type Conversion and BeanWrapper, Part 15: SpEL, Part 16: Utilities and AOT
- Earlier posts this one builds on: Part 7: The Twelve Steps of refresh(), Part 8: @Configuration and @ComponentScan, Part 9: @Conditional, @Profile, @Import, Part 5: Dependency Injection Resolution
- Where the series starts: Part 1: How I Read It with AI, and the Map