2.1. What Is an Environment and Why You Need One
The Environment is a central Spring object responsible for two things:
- Property sources — where the configuration is read from (files, env vars, args).
- Profiles — which profiles are active.
The interface hierarchy:
Environment
└─ ConfigurableEnvironment
├─ StandardEnvironment (NONE)
├─ StandardServletEnvironment (SERVLET)
└─ StandardReactiveWebEnvironment (REACTIVE)
StandardServletEnvironment additionally mixes in ServletConfig and ServletContext as property sources.
2.2. Creating the Environment — getOrCreateEnvironment()
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
return this.applicationContextFactory.createEnvironment(
this.webApplicationType);
}
In ApplicationContextFactory (Boot 3):
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
Boot 4: the factory is gone — the Environment is created by the module (spring-boot-webmvc → StandardServletEnvironment).
2.3. MutablePropertySources — the Order of Precedence
Inside the Environment sits a MutablePropertySources — an ordered list of PropertySource objects. The one higher up wins.
The order for StandardServletEnvironment (top = highest priority):
| # | PropertySource | Example |
|---|---|---|
| 1 | commandLineArgs |
--server.port=9090 |
| 2 | servletConfigInitParams |
<init-param> |
| 3 | servletContextInitParams |
<context-param> |
| 4 | systemProperties |
-Dserver.port=9090 |
| 5 | systemEnvironment |
SERVER_PORT=9090 |
| 6 | random |
random.int, random.uuid
|
| 7 | Config resource 'application.yml' | the configuration file |
| 8 | defaultProperties |
SpringApplication.setDefaultProperties() |
Key point: @PropertySource, application-{profile}.yml, and spring.config.import entries are inserted between the system sources and defaultProperties, each at its own index.
You can verify it like this:
@Autowired
private ConfigurableEnvironment env;
env.getPropertySources().forEach(ps ->
System.out.println(ps.getName()));
2.4. configureEnvironment() — What Happens Before Files Are Read
protected void configureEnvironment(ConfigurableEnvironment environment,
String[] args) {
if (this.addCommandLineProperties) {
addCommandLineProperties(environment, args); // CommandLinePropertySource
}
configureProfiles(environment, args); // active profiles
}
What happens here:
SimpleCommandLinePropertySource parses --key=value.
Profiles are read from spring.profiles.active (if passed via args or system properties) before any files are loaded.
2.5. EnvironmentPostProcessor — the Extension Point
A key SPI. Loaded from META-INF/spring.factories:
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\
...
The interface:
public interface EnvironmentPostProcessor {
void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application);
}
Ordered via @Order. Invoked by listeners.environmentPrepared(), i.e., before the ApplicationContext exists.
A custom example — adding your own property source from a database:
public class DbPropertySourcePostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment env,
SpringApplication app) {
env.getPropertySources().addLast(
new MapPropertySource("dbConfig", loadFromDb()));
}
}
Registration:
# META-INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor=\
com.example.DbPropertySourcePostProcessor
2.6. Loading Configuration Files (Boot 2.4+)
This is the meatiest part. Since 2.4 the whole mechanism has been completely rewritten — ConfigDataEnvironmentPostProcessor was introduced.
The chain:
ConfigDataEnvironmentPostProcessor.postProcessEnvironment()
└─ ConfigDataEnvironment.processAndApply()
├─ ConfigDataLocationResolver (for each location)
│ └─ StandardConfigDataLocationResolver
├─ ConfigDataLoader (for each resource)
│ └─ StandardConfigDataLoader → PropertySource
└─ Environment.getPropertySources().addXxx(...)
Key classes:
-
ConfigDataEnvironment— the orchestrator. -
ConfigDataLocationResolver— turns the stringfile:./config/into aConfigDataResource. -
ConfigDataLoader— loads aConfigDataResourceintoConfigData(a set of property sources). -
ConfigData— an immutable container: a name + property sources.
What's searched by default
Default locations (in increasing order of precedence):
classpath:/classpath:/config/file:./file:./config/file:./config/*/
Files: application.properties / application.yml / application.yaml.
Plus profile-specific ones: application-{profile}.yml.
spring.config.*
| Property | What it does |
|---|---|
spring.config.name |
the base name (default: application) |
spring.config.location |
replaces the default locations |
spring.config.additional-location |
adds to the defaults |
spring.config.import |
imports other sources |
spring.config.on-not-found |
fail / ignore
|
Example:
spring:
config:
import:
- optional:file:./external.yml
- configtree:/run/secrets/
Prefixes:
optional: — don't fail if it's missing.
file: / classpath: — explicit protocol.
configtree: — a directory of files, where each file is a property (the file name is the key).
YAML multi-document:
# application.yml
server:
port: 8080
---
spring:
config:
activate:
on-profile: prod
server:
port: 9090
The second document is only activated in the prod profile.
Boot 4: the whole mechanism remains, but it's modular — spring-boot-config-data is a separate JAR.
2.7. Profiles
Activation
Methods (in decreasing order of precedence):
-
--spring.profiles.active=dev,metrics(args) -
-Dspring.profiles.active=dev(system property) -
SPRING_PROFILES_ACTIVE=dev(env var) -
spring.profiles.activeinapplication.yml -
SpringApplication.setAdditionalProfiles("dev")(programmatically)
Profile groups (2.4+)
spring:
profiles:
group:
"prod": "proddb,prodmq"
"dev": "devdb,devmq"
Activate prod → proddb and prodmq are enabled.
include / default
spring:
profiles:
include: common
default: local
-
include— adds profiles to the active ones. -
default— applies if nothing is activated.
Checking in code
@Profile("dev")
@Configuration
class DevConfig {}
@Autowired
private Environment env;
if (env.acceptsProfiles(Profiles.of("prod & !test"))) { ... }
Profiles.of() supports expression syntax: & (and), | (or), ! (not).
2.8. @ConfigurationProperties — Typed Access
Basic example
@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailProperties(
@NotBlank String host,
@Min(1) @Max(65535) int port,
Duration timeout,
List<String> recipients
) {}
Activation:
@SpringBootApplication
@ConfigurationPropertiesScan // ← scans all @ConfigurationProperties classes
public class App {}
Or explicitly:
@EnableConfigurationProperties(MailProperties.class)
How binding works
The chain:
ConfigurationPropertiesBindingPostProcessor (BeanPostProcessor)
└─ ConfigurationPropertiesBinder
├─ Binder (Spring Framework)
├─ PropertySources ← from the Environment
├─ ConversionService ← type conversion
└─ Validator ← JSR-380
ConfigurationPropertiesBindingPostProcessor is registered automatically via ConfigurationPropertiesAutoConfiguration.
Relaxed binding
Spring Boot matches keys in any style:
| Format in the file | Matches the field |
|---|---|
app.mail.host-name |
hostName |
app.mail.host_name |
hostName |
app.mail.hostName |
hostName |
APP_MAIL_HOSTNAME |
hostname |
The canonical format is kebab-case.
The evolution of @ConstructorBinding
- Boot 2.x — required for immutable (constructor-bound) beans.
- Boot 3.0+ — no longer needed for records and classes with a single constructor.
- Boot 3.x — still used when a class has multiple constructors.
- Boot 4 — effectively legacy, rarely used.
Validation
@ConfigurationProperties(prefix = "app.mail")
@Validated
public class MailProperties {
@NotBlank private String host;
@Min(1) private int port;
// getters/setters
}
At startup, if validation fails → BindValidationException → ApplicationFailedEvent.
Binding complex types
-
Duration—10s,5m,1h. -
DataSize—10MB,512KB. -
List<String> —app.mail.recipients=a,b,cor a YAML list. -
Map<String, X>— nested keys. - Enum — by name (case-insensitive).
Boot 3 vs Boot 4
| Aspect | Boot 3 | Boot 4 |
|---|---|---|
@ConstructorBinding |
Optional | Legacy |
| Module | spring-boot |
spring-boot-configuration-properties (extracted) |
| Record binding | Supported | Supported |
| AOT processing | Present | Improved (reflection-free by default) |
2.9. Diagram: a Property's Journey from File to Bean
application.yml
│
▼
StandardConfigDataLoader
│
▼
ConfigData (PropertySource "Config resource '...'")
│
▼
MutablePropertySources.addLast() ← inside the Environment
│
▼
Binder.bind(prefix, targetClass)
│ (via ConfigurationPropertiesBinder)
▼
MailProperties (bean)
2.10. Key Takeaways
- The
Environmentis created before theApplicationContext— which is why anEnvironmentPostProcessorcan do pretty much anything. -
ConfigDataEnvironmentPostProcessoris the entry point for loadingapplication.yml(since Boot 2.4). - The order of property sources decides everything — higher in the list = wins.
-
spring.config.importis the only proper way to plug in external configuration in Boot 3/4. -
@ConfigurationProperties+ records is the modern standard;@ConstructorBindingis no longer needed. - Profile groups (
spring.profiles.group.*) — handy for composition. - Boot 4: the configuration mechanism has been extracted into a separate module, spring-boot-config-data.
Top comments (0)