docs
This documentation has been enhanced for AI coding agents with comprehensive examples, complete API signatures, thread safety notes, error handling patterns, and production-ready usage patterns.
Package: org.springframework.boot.env
Module: org.springframework.boot:spring-boot
Since: 1.0.0
Spring Boot provides specialized PropertySource implementations for generating random values and reading configuration from directory trees (commonly used for Kubernetes ConfigMaps and Secrets).
Environment property sources extend Spring's property source abstraction with Boot-specific capabilities:
PropertySource that returns a random value for any property that starts with "random.". Useful for generating random ports, UUIDs, secrets, and test data.
package org.springframework.boot.env;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import java.util.Random;
/**
* PropertySource that returns a random value for any property that starts with "random.".
* Automatically added to the environment by Spring Boot.
*
* Thread Safety: Thread-safe. Uses ThreadLocalRandom internally.
*
* @since 1.0.0
*/
public class RandomValuePropertySource extends PropertySource<Random> {
/**
* Name of the random PropertySource.
* Value: "random"
*/
public static final String RANDOM_PROPERTY_SOURCE_NAME = "random";
/**
* Create a new RandomValuePropertySource with the default name "random".
*/
public RandomValuePropertySource() {
super(RANDOM_PROPERTY_SOURCE_NAME);
}
/**
* Create a new RandomValuePropertySource with the given name.
*
* @param name the name of the property source
*/
public RandomValuePropertySource(String name) {
super(name);
}
/**
* Add a RandomValuePropertySource to the given Environment.
* Adds after system environment sources but before application property sources.
*
* @param environment the environment to add the random property source to
*/
public static void addToEnvironment(ConfigurableEnvironment environment) {
addToEnvironment(environment.getPropertySources());
}
/**
* Add a RandomValuePropertySource to the given PropertySources.
* Adds after system sources for proper precedence.
*
* @param sources the property sources to add to
* @since 2.6.0
*/
public static void addToEnvironment(MutablePropertySources sources) {
// Implementation adds to appropriate position in source list
}
@Override
public Object getProperty(String name) {
// Returns random value if name starts with "random.", else null
}
}Supported Random Properties:
random.int: Random integer value (full int range)random.int(max): Random integer between 0 (inclusive) and max (exclusive)random.int[min,max]: Random integer between min (inclusive) and max (exclusive)random.long: Random long value (full long range)random.long(max): Random long between 0 (inclusive) and max (exclusive)random.long[min,max]: Random long between min (inclusive) and max (exclusive)random.uuid: Random UUID string (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)random.* (any other): Random 32-character hex stringpackage com.example.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* Configuration using random values.
*/
@Component
public class RandomConfigExample {
// Random UUID for instance ID
@Value("${random.uuid}")
private String instanceId;
// Random integer
@Value("${random.int}")
private int randomNumber;
// Random long
@Value("${random.long}")
private long randomLong;
// Random hex string
@Value("${random.value}")
private String randomHex;
public void printValues() {
System.out.println("Instance ID: " + instanceId);
System.out.println("Random number: " + randomNumber);
System.out.println("Random long: " + randomLong);
System.out.println("Random hex: " + randomHex);
}
}package com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Configuration properties with ranged random values.
*/
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
// Will be set to random port between 8000-9000
private int serverPort;
// Random thread pool size between 10-100
private int threadPoolSize;
// Random session timeout between 60-3600 seconds
private long sessionTimeout;
public int getServerPort() {
return serverPort;
}
public void setServerPort(int serverPort) {
this.serverPort = serverPort;
}
public int getThreadPoolSize() {
return threadPoolSize;
}
public void setThreadPoolSize(int threadPoolSize) {
this.threadPoolSize = threadPoolSize;
}
public long getSessionTimeout() {
return sessionTimeout;
}
public void setSessionTimeout(long sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
}application.properties:
# Random port between 8000-9000
app.server-port=${random.int[8000,9000]}
# Random thread pool size between 10-100
app.thread-pool-size=${random.int[10,100]}
# Random session timeout between 60-3600 seconds
app.session-timeout=${random.long[60,3600]}
# Random instance ID
app.instance-id=${random.uuid}
# Random API secret
app.api-secret=${random.value}package com.example;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
/**
* Test using random ports to avoid conflicts.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
"server.port=${random.int[8000,9000]}",
"management.server.port=${random.int[9000,10000]}",
"spring.datasource.url=jdbc:h2:mem:testdb-${random.uuid}"
})
public class RandomPortTest {
@Value("${local.server.port}")
private int serverPort;
@Value("${management.server.port}")
private int managementPort;
@Test
public void testWithRandomPorts() {
System.out.println("Test server running on port: " + serverPort);
System.out.println("Management running on port: " + managementPort);
// Test logic here
}
}package com.example.util;
import org.springframework.boot.env.RandomValuePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.StandardEnvironment;
/**
* Programmatic access to random values.
*/
public class RandomValueExample {
public void useRandomValues() {
ConfigurableEnvironment environment = new StandardEnvironment();
// Add random property source
RandomValuePropertySource.addToEnvironment(environment);
// Access random values
String uuid = environment.getProperty("random.uuid");
Integer randomInt = environment.getProperty("random.int", Integer.class);
Integer portNumber = environment.getProperty("random.int[1024,65536]", Integer.class);
String randomHex = environment.getProperty("random.value");
System.out.println("UUID: " + uuid);
System.out.println("Random int: " + randomInt);
System.out.println("Port number: " + portNumber);
System.out.println("Random hex: " + randomHex);
}
public void generateMultipleUUIDs() {
ConfigurableEnvironment environment = new StandardEnvironment();
RandomValuePropertySource.addToEnvironment(environment);
// Each access generates a NEW random value
String uuid1 = environment.getProperty("random.uuid");
String uuid2 = environment.getProperty("random.uuid");
String uuid3 = environment.getProperty("random.uuid");
System.out.println("UUID 1: " + uuid1);
System.out.println("UUID 2: " + uuid2);
System.out.println("UUID 3: " + uuid3);
// All three will be different
assert !uuid1.equals(uuid2);
assert !uuid2.equals(uuid3);
}
}PropertySource backed by a directory tree where each file represents a property value. Primarily used for Kubernetes ConfigMaps and Secrets mounted as volumes.
package org.springframework.boot.env;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.boot.origin.OriginLookup;
import java.nio.file.Path;
import java.io.InputStream;
/**
* PropertySource backed by a directory tree that contains files for each value.
* Each file name becomes a property key, and file content becomes the property value.
*
* File Structure Rules:
* - Directory names and file names become property keys with dots as separators
* - Underscores in names are converted to dots
* - Symlinks are followed (important for Kubernetes)
*
* Thread Safety: Immutable by default. Use ALWAYS_READ option for dynamic updates.
*
* @since 2.4.0
*/
public class ConfigTreePropertySource extends EnumerablePropertySource<Path>
implements PropertySourceInfo, OriginLookup<String> {
/**
* Create a new ConfigTreePropertySource instance.
* Uses default options (cached reads, original case names, no auto-trim).
*
* @param name the name of the property source
* @param sourceDirectory the underlying source directory (must exist)
* @throws IllegalArgumentException if sourceDirectory doesn't exist or isn't readable
*/
public ConfigTreePropertySource(String name, Path sourceDirectory) {
super(name, sourceDirectory);
}
/**
* Create a new ConfigTreePropertySource instance with options.
*
* @param name the name of the property source
* @param sourceDirectory the underlying source directory
* @param options the property source options (varargs)
*/
public ConfigTreePropertySource(String name, Path sourceDirectory, Option... options) {
super(name, sourceDirectory);
}
@Override
public String[] getPropertyNames() {
// Returns all property keys derived from directory structure
}
@Override
public Value getProperty(String name) {
// Returns file content as Value, or null if file doesn't exist
}
@Override
public Origin getOrigin(String name) {
// Returns file location for error messages
}
@Override
public boolean isImmutable() {
// Returns true unless ALWAYS_READ option is set
return true;
}
/**
* Property source options for customizing behavior.
*/
public enum Option {
/**
* Always read the value of the file when accessing the property value.
* When this option is not set, the property source will cache the value
* when it's first read.
*
* Use Case: Enable when files can change at runtime (rare).
*/
ALWAYS_READ,
/**
* Use the lowercase directory name when building the property name.
* Converts all property keys to lowercase.
*
* Use Case: Normalize keys for case-insensitive matching.
*/
USE_LOWERCASE_NAMES,
/**
* Automatically trim trailing new line characters from property values.
* Removes \n and \r\n from end of file content.
*
* Use Case: Clean up ConfigMap values that often have trailing newlines.
*/
AUTO_TRIM_TRAILING_NEW_LINE
}
/**
* Property value that can be read as an InputStream or CharSequence.
* Allows lazy loading and efficient handling of large files.
*/
public interface Value extends InputStreamSource, CharSequence, OriginProvider {
/**
* Get the value as a string.
* Reads the entire file content into a String.
*
* @return the value as a string
*/
String toString();
@Override
InputStream getInputStream() throws IOException;
}
}Filesystem:
/etc/config/
├── database.url → database.url
├── database.username → database.username
├── database.password → database.password
├── app/
│ ├── name → app.name
│ └── version → app.version
├── features/
│ ├── feature-a.enabled → features.feature-a.enabled
│ └── feature-b.timeout → features.feature-b.timeout
└── credentials/
└── api_key → credentials.api-key (underscore → dash)ConfigMap Definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
database.url: "jdbc:postgresql://postgres:5432/mydb"
database.username: "myuser"
database.password: "changeme"
app.name: "MyApplication"
app.version: "1.0.0"
features.feature-a.enabled: "true"
features.feature-b.timeout: "30"Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
env:
# Tell Spring Boot to import config tree
- name: SPRING_CONFIG_IMPORT
value: "optional:configtree:/etc/config/"
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-configSpring Boot Configuration:
# application.properties
spring.config.import=optional:configtree:/etc/config/package com.example.config;
import org.springframework.boot.env.ConfigTreePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.StandardEnvironment;
import java.nio.file.Paths;
/**
* Load configuration from directory tree.
*/
public class ConfigTreeExample {
public void loadFromDirectory() {
ConfigurableEnvironment environment = new StandardEnvironment();
// Create property source from directory
ConfigTreePropertySource propertySource = new ConfigTreePropertySource(
"k8s-config",
Paths.get("/etc/config")
);
// Add to environment
environment.getPropertySources().addLast(propertySource);
// Access properties
String dbUrl = environment.getProperty("database.url");
String appName = environment.getProperty("app.name");
String featureEnabled = environment.getProperty("features.feature-a.enabled");
System.out.println("Database URL: " + dbUrl);
System.out.println("App Name: " + appName);
System.out.println("Feature A Enabled: " + featureEnabled);
}
public void useWithOptions() {
// Create with options
ConfigTreePropertySource propertySource = new ConfigTreePropertySource(
"secrets",
Paths.get("/etc/secrets"),
ConfigTreePropertySource.Option.ALWAYS_READ, // Re-read on each access
ConfigTreePropertySource.Option.AUTO_TRIM_TRAILING_NEW_LINE // Clean up newlines
);
// Properties will be re-read on each access (ALWAYS_READ)
// and trailing newlines will be automatically removed
}
public void listAllProperties() {
ConfigTreePropertySource source = new ConfigTreePropertySource(
"config",
Paths.get("/etc/config")
);
// Get all property names
String[] propertyNames = source.getPropertyNames();
System.out.println("Available properties:");
for (String name : propertyNames) {
Object value = source.getProperty(name);
System.out.printf(" %s = %s%n", name, value);
}
}
}Secret Definition:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
database.password: "super-secret-password"
api.key: "api-key-value"
jwt.secret: "jwt-signing-secret"Deployment with Secret:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
env:
- name: SPRING_CONFIG_IMPORT
value: "optional:configtree:/etc/secrets/"
volumeMounts:
- name: secrets-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secrets-volume
secret:
secretName: db-credentialsApplication Code:
package com.example.security;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* Access secrets loaded from Kubernetes.
*/
@Component
public class CredentialsManager {
// Loaded from /etc/secrets/database.password
@Value("${database.password}")
private String dbPassword;
// Loaded from /etc/secrets/api.key
@Value("${api.key}")
private String apiKey;
// Loaded from /etc/secrets/jwt.secret
@Value("${jwt.secret}")
private String jwtSecret;
public String getDatabasePassword() {
return dbPassword;
}
public String getApiKey() {
return apiKey;
}
public String getJwtSecret() {
return jwtSecret;
}
}MapPropertySource containing default properties contributed directly to a SpringApplication. By convention, the DefaultPropertiesPropertySource is always the last property source in the Environment, ensuring that application properties always take precedence over defaults.
package org.springframework.boot.env;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
/**
* MapPropertySource containing default properties contributed directly to a
* SpringApplication. By convention, the DefaultPropertiesPropertySource is always
* the last property source in the Environment.
*
* @since 2.4.0
*/
public class DefaultPropertiesPropertySource extends MapPropertySource {
/**
* The name of the 'default properties' property source.
* Value: "defaultProperties"
*/
public static final String NAME = "defaultProperties";
/**
* Create a new DefaultPropertiesPropertySource with the given Map source.
*
* @param source the source map
*/
public DefaultPropertiesPropertySource(Map<String, Object> source);
/**
* Return true if the given source is named 'defaultProperties'.
*
* @param propertySource the property source to check
* @return true if the name matches
*/
public static boolean hasMatchingName(PropertySource<?> propertySource);
/**
* Create a new DefaultPropertiesPropertySource instance if the provided
* source is not empty.
*
* @param source the Map source
* @param action the action used to consume the DefaultPropertiesPropertySource
*/
public static void ifNotEmpty(Map<String, Object> source,
Consumer<DefaultPropertiesPropertySource> action);
/**
* Add a new DefaultPropertiesPropertySource or merge with an existing one.
* If a default properties source already exists, the new properties are merged.
*
* @param source the Map source
* @param sources the existing sources
* @since 2.4.4
*/
public static void addOrMerge(Map<String, Object> source,
MutablePropertySources sources);
/**
* Move the 'defaultProperties' property source so that it's the last source
* in the given ConfigurableEnvironment.
*
* @param environment the environment to update
*/
public static void moveToEnd(ConfigurableEnvironment environment);
/**
* Move the 'defaultProperties' property source so that it's the last source
* in the given MutablePropertySources.
*
* @param propertySources the property sources to update
*/
public static void moveToEnd(MutablePropertySources propertySources);
}SpringApplication.setDefaultProperties()import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.HashMap;
import java.util.Map;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
// Set default properties
Map<String, Object> defaultProperties = new HashMap<>();
defaultProperties.put("server.port", 8080);
defaultProperties.put("logging.level.root", "INFO");
defaultProperties.put("spring.application.name", "my-app");
defaultProperties.put("app.feature.enabled", false);
app.setDefaultProperties(defaultProperties);
app.run(args);
}
}import org.springframework.boot.env.DefaultPropertiesPropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import java.util.HashMap;
import java.util.Map;
public class PropertySourceManager {
public void addDefaultProperties(ConfigurableEnvironment environment) {
Map<String, Object> defaults = new HashMap<>();
defaults.put("app.timeout", 30);
defaults.put("app.retries", 3);
// Add or merge with existing default properties
MutablePropertySources sources = environment.getPropertySources();
DefaultPropertiesPropertySource.addOrMerge(defaults, sources);
}
public void ensureDefaultPropertiesAtEnd(ConfigurableEnvironment environment) {
// Ensure default properties are at the end (lowest priority)
DefaultPropertiesPropertySource.moveToEnd(environment);
}
public boolean hasDefaultProperties(ConfigurableEnvironment environment) {
MutablePropertySources sources = environment.getPropertySources();
return sources.stream()
.anyMatch(DefaultPropertiesPropertySource::hasMatchingName);
}
}import org.springframework.boot.env.DefaultPropertiesPropertySource;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import java.util.HashMap;
import java.util.Map;
public class DefaultPropertiesInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment env = applicationContext.getEnvironment();
Map<String, Object> defaults = new HashMap<>();
// Add environment-specific defaults
if (env.acceptsProfiles(org.springframework.core.env.Profiles.of("dev"))) {
defaults.put("logging.level.root", "DEBUG");
defaults.put("spring.h2.console.enabled", true);
} else if (env.acceptsProfiles(org.springframework.core.env.Profiles.of("prod"))) {
defaults.put("logging.level.root", "WARN");
defaults.put("server.compression.enabled", true);
}
// Add defaults only if not empty
DefaultPropertiesPropertySource.ifNotEmpty(defaults, propertySource -> {
env.getPropertySources().addLast(propertySource);
});
}
}Default properties have the lowest priority in Spring Boot's property source ordering:
1. Command line arguments (highest priority)
2. SPRING_APPLICATION_JSON
3. ServletConfig init parameters
4. ServletContext init parameters
5. JNDI attributes
6. Java System properties
7. OS environment variables
8. RandomValuePropertySource
9. application.properties/yml
10. @PropertySource annotations
11. Default properties (lowest priority) <-- DefaultPropertiesPropertySourceSPI (Service Provider Interface) for loading property sources from resources. Implementations are discovered through SpringFactoriesLoader and used to load configuration from various file formats.
package org.springframework.boot.env;
import java.io.IOException;
import java.util.List;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
/**
* Strategy interface located through SpringFactoriesLoader and used to load a
* PropertySource.
*
* Implementations are registered in META-INF/spring.factories and are automatically
* discovered and used by Spring Boot to load configuration files.
*
* @since 1.0.0
*/
public interface PropertySourceLoader {
/**
* Returns the file extensions that the loader supports (excluding the '.').
*
* @return the file extensions (e.g., ["properties", "xml"])
*/
String[] getFileExtensions();
/**
* Load the resource into one or more property sources. Implementations may either
* return a list containing a single source, or in the case of a multi-document
* format such as yaml a source for each document in the resource.
*
* @param name the root name of the property source. If multiple documents are loaded
* an additional suffix should be added to the name for each source loaded.
* @param resource the resource to load
* @return a list of property sources
* @throws IOException if the source cannot be loaded
*/
List<PropertySource<?>> load(String name, Resource resource) throws IOException;
}spring.factoriesSpring Boot provides two built-in implementations:
.properties and .xml files.yml and .yaml filespackage com.example.config;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* PropertySourceLoader for JSON configuration files.
*/
public class JsonPropertySourceLoader implements PropertySourceLoader {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String[] getFileExtensions() {
// Support .json files
return new String[] { "json" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
try (InputStream inputStream = resource.getInputStream()) {
// Parse JSON into a Map
@SuppressWarnings("unchecked")
Map<String, Object> properties = objectMapper.readValue(inputStream, Map.class);
// Flatten nested JSON into dot-notation properties
Map<String, Object> flatProperties = flattenMap(properties);
// Return as a single PropertySource
return Collections.singletonList(
new MapPropertySource(name, flatProperties)
);
}
}
private Map<String, Object> flattenMap(Map<String, Object> source) {
Map<String, Object> result = new HashMap<>();
flattenMap("", source, result);
return result;
}
private void flattenMap(String prefix, Map<String, Object> source,
Map<String, Object> result) {
for (Map.Entry<String, Object> entry : source.entrySet()) {
String key = prefix.isEmpty() ? entry.getKey() : prefix + "." + entry.getKey();
Object value = entry.getValue();
if (value instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> nestedMap = (Map<String, Object>) value;
flattenMap(key, nestedMap, result);
} else {
result.put(key, value);
}
}
}
}Create or update src/main/resources/META-INF/spring.factories:
# PropertySourceLoader implementations
org.springframework.boot.env.PropertySourceLoader=\
com.example.config.JsonPropertySourceLoaderpackage com.example.config;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* PropertySourceLoader for multi-document configuration files.
* Documents are separated by "---" on a line by itself.
*/
public class MultiDocumentPropertySourceLoader implements PropertySourceLoader {
private static final String DOCUMENT_SEPARATOR = "---";
@Override
public String[] getFileExtensions() {
return new String[] { "conf" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
List<PropertySource<?>> propertySources = new ArrayList<>();
List<Map<String, Object>> documents = parseDocuments(resource);
for (int i = 0; i < documents.size(); i++) {
String documentName = documents.size() == 1
? name
: name + " (document #" + i + ")";
propertySources.add(new MapPropertySource(documentName, documents.get(i)));
}
return propertySources;
}
private List<Map<String, Object>> parseDocuments(Resource resource) throws IOException {
List<Map<String, Object>> documents = new ArrayList<>();
Map<String, Object> currentDocument = new HashMap<>();
try (InputStream is = resource.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.trim().equals(DOCUMENT_SEPARATOR)) {
// Start new document
if (!currentDocument.isEmpty()) {
documents.add(currentDocument);
currentDocument = new HashMap<>();
}
} else if (line.contains("=")) {
// Parse property
String[] parts = line.split("=", 2);
if (parts.length == 2) {
currentDocument.put(parts[0].trim(), parts[1].trim());
}
}
}
// Add last document
if (!currentDocument.isEmpty()) {
documents.add(currentDocument);
}
}
return documents;
}
}Once registered, your custom loader is automatically used:
# File: application.json
{
"server": {
"port": 8080
},
"app": {
"name": "My Application",
"version": "1.0.0"
}
}Spring Boot will automatically discover and use JsonPropertySourceLoader to load application.json.
For more advanced configuration loading (including import support), consider implementing ConfigDataLoader instead of PropertySourceLoader.
Built-in implementation of PropertySourceLoader that loads .yml and .yaml files into property sources. Supports multi-document YAML files.
package org.springframework.boot.env;
import java.io.IOException;
import java.util.List;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
/**
* Strategy to load '.yml' (or '.yaml') files into a PropertySource.
*
* Supports multi-document YAML files separated by "---".
* Requires SnakeYAML library on the classpath.
*
* @since 1.0.0
*/
public class YamlPropertySourceLoader implements PropertySourceLoader {
/**
* Returns the file extensions supported by this loader.
*
* @return ["yml", "yaml"]
*/
@Override
public String[] getFileExtensions();
/**
* Load the YAML resource into one or more property sources.
* Multi-document YAML files result in multiple property sources.
*
* @param name the root name of the property source
* @param resource the YAML resource to load
* @return a list of property sources (one per YAML document)
* @throws IOException if the source cannot be loaded
* @throws IllegalStateException if SnakeYAML is not on the classpath
*/
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException;
}---org.yaml:snakeyaml on classpathspring.factories# application.yml
server:
port: 8080
ssl:
enabled: true
spring:
application:
name: my-app
profiles:
active: dev
app:
features:
- feature-a
- feature-b
settings:
timeout: 30
retries: 3This creates a single PropertySource with properties like:
server.port = 8080server.ssl.enabled = truespring.application.name = "my-app"app.features[0] = "feature-a"app.features[1] = "feature-b"# application.yml
# Document 1: Default profile
server:
port: 8080
---
# Document 2: Development profile
spring:
config:
activate:
on-profile: dev
server:
port: 8081
logging:
level:
root: DEBUG
---
# Document 3: Production profile
spring:
config:
activate:
on-profile: prod
server:
port: 80
logging:
level:
root: WARNThis creates three PropertySource instances:
application.yml (document #0) - default propertiesapplication.yml (document #1) - dev profile propertiesapplication.yml (document #2) - prod profile propertiesimport org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.util.List;
public class YamlConfigLoader {
public void loadYamlConfig() throws IOException {
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
Resource resource = new ClassPathResource("config/custom-config.yml");
List<PropertySource<?>> propertySources = loader.load("customConfig", resource);
for (PropertySource<?> propertySource : propertySources) {
System.out.println("Loaded: " + propertySource.getName());
// Add to environment or use properties
}
}
}Add SnakeYAML to your pom.xml:
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>Or build.gradle:
implementation 'org.yaml:snakeyaml'import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.util.List;
public class SafeYamlLoader {
public List<PropertySource<?>> loadYamlSafely(Resource resource) {
YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
try {
return loader.load("config", resource);
} catch (IllegalStateException e) {
if (e.getMessage().contains("snakeyaml")) {
System.err.println("SnakeYAML library not found on classpath");
throw new RuntimeException("Please add org.yaml:snakeyaml dependency", e);
}
throw e;
} catch (IOException e) {
System.err.println("Failed to load YAML: " + resource.getDescription());
throw new RuntimeException("YAML loading failed", e);
}
}
}Built-in implementation of PropertySourceLoader that loads .properties and .xml files into property sources.
package org.springframework.boot.env;
import java.io.IOException;
import java.util.List;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
/**
* Strategy to load '.properties' files into a PropertySource.
* Also supports XML properties files (.xml).
*
* @since 1.0.0
*/
public class PropertiesPropertySourceLoader implements PropertySourceLoader {
/**
* Returns the file extensions supported by this loader.
*
* @return ["properties", "xml"]
*/
@Override
public String[] getFileExtensions();
/**
* Load the properties resource into one or more property sources.
*
* @param name the root name of the property source
* @param resource the properties resource to load
* @return a list of property sources
* @throws IOException if the source cannot be loaded
*/
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException;
}.properties and .xml property files.properties files (separated by #---)spring.factories# application.properties
server.port=8080
server.ssl.enabled=true
spring.application.name=my-app
spring.profiles.active=dev
# List syntax
app.features[0]=feature-a
app.features[1]=feature-b
# Map syntax
app.settings.timeout=30
app.settings.retries=3<!-- application.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<entry key="server.port">8080</entry>
<entry key="spring.application.name">my-app</entry>
<entry key="app.timeout">30</entry>
</properties># application.properties
# Document 1: Default properties
server.port=8080
app.name=My Application
#---
# Document 2: Additional properties
spring.profiles.active=dev
logging.level.root=DEBUGimport org.springframework.boot.env.PropertiesPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.util.List;
public class PropertiesConfigLoader {
public void loadPropertiesConfig() throws IOException {
PropertiesPropertySourceLoader loader = new PropertiesPropertySourceLoader();
// Load .properties file
Resource propsResource = new FileSystemResource("config/app.properties");
List<PropertySource<?>> propsSources = loader.load("appConfig", propsResource);
// Load .xml file
Resource xmlResource = new FileSystemResource("config/app.xml");
List<PropertySource<?>> xmlSources = loader.load("xmlConfig", xmlResource);
// Process loaded property sources
propsSources.forEach(ps -> {
System.out.println("Loaded properties from: " + ps.getName());
});
xmlSources.forEach(ps -> {
System.out.println("Loaded XML properties from: " + ps.getName());
});
}
}# Escaping special characters
path.windows=C:\\Users\\myuser\\documents
path.unix=/home/myuser/documents
# Unicode escaping
message.japanese=\u3053\u3093\u306B\u3061\u306F
# Line continuation
long.property=This is a very long property value \
that spans multiple lines \
for better readability
# Spaces in keys (not recommended, but supported)
property\ with\ spaces=value| Feature | Properties | YAML |
|---|---|---|
| Syntax | key=value | key: value |
| Hierarchy | Dot notation | Indentation |
| Lists | Indexed [0], [1] | Dash syntax |
| Multi-doc | #--- separator | --- separator |
| Comments | # or ! | # only |
| Type Safety | Strings only | Native types |
| Readability | Good for flat | Better for nested |
.properties files.properties filesInterface providing metadata about a PropertySource for optimization hints.
package org.springframework.boot.env;
import org.springframework.lang.Nullable;
/**
* Interface that can be implemented by a PropertySource to provide additional
* information about itself, such as whether it is immutable or has a prefix.
*
* @since 4.0.0
*/
public interface PropertySourceInfo {
/**
* Return whether this property source is immutable (values will not change).
* Immutable sources can be cached more aggressively.
*
* @return true if the property source is immutable
*/
default boolean isImmutable() {
return false;
}
/**
* Return a prefix that should be applied when adding this property source
* to an environment, or null if no prefix is needed.
* The prefix is automatically prepended to all property keys.
*
* @return the prefix or null
*/
default @Nullable String getPrefix() {
return null;
}
}Custom PropertySource with Info:
package com.example.config;
import org.springframework.boot.env.PropertySourceInfo;
import org.springframework.core.env.PropertySource;
import java.util.Collections;
import java.util.Map;
/**
* Custom property source with metadata.
*/
public class ImmutablePropertySource extends PropertySource<Map<String, Object>>
implements PropertySourceInfo {
public ImmutablePropertySource(String name, Map<String, Object> source) {
super(name, Collections.unmodifiableMap(source));
}
@Override
public boolean isImmutable() {
return true; // Values will not change - optimize caching
}
@Override
public String getPrefix() {
return "app.config"; // All properties prefixed with "app.config."
}
@Override
public Object getProperty(String name) {
return this.source.get(name);
}
}While ConfigTreePropertySource is immutable by default, you can enable hot reload:
package com.example.config;
import org.springframework.boot.env.ConfigTreePropertySource;
import org.springframework.cloud.context.refresh.ContextRefresher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.nio.file.Paths;
/**
* Hot reload ConfigMaps when they change.
* Note: Requires spring-cloud-context dependency.
*/
@Component
public class ConfigMapHotReloader {
private final ContextRefresher contextRefresher;
public ConfigMapHotReloader(ContextRefresher contextRefresher) {
this.contextRefresher = contextRefresher;
}
/**
* Check for ConfigMap changes every 30 seconds.
* Kubernetes updates ConfigMap files atomically via symlinks.
*/
@Scheduled(fixedDelay = 30000)
public void refreshConfig() {
// Trigger context refresh
contextRefresher.refresh();
System.out.println("Configuration refreshed from ConfigMap");
}
}package com.example.config;
import org.springframework.boot.env.ConfigTreePropertySource;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Safe configuration loading with error handling.
*/
public class SafeConfigLoader {
public ConfigTreePropertySource loadConfigSafely(String directory) {
Path configPath = Paths.get(directory);
// Verify directory exists
if (!Files.exists(configPath)) {
throw new IllegalStateException(
"Configuration directory not found: " + directory +
". Ensure ConfigMap is mounted correctly."
);
}
if (!Files.isDirectory(configPath)) {
throw new IllegalStateException(
"Configuration path is not a directory: " + directory
);
}
if (!Files.isReadable(configPath)) {
throw new IllegalStateException(
"Configuration directory is not readable: " + directory +
". Check pod security context and volume permissions."
);
}
try {
return new ConfigTreePropertySource("k8s-config", configPath);
} catch (Exception e) {
throw new IllegalStateException(
"Failed to load configuration from: " + directory, e
);
}
}
}RandomValuePropertySource:
ConfigTreePropertySource:
# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb-${random.uuid}
spring.datasource.username=sa-${random.uuid}
server.port=${random.int[8000,9000]}
management.server.port=${random.int[9000,10000]}# Base ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-prod
data:
spring.profiles.active: "production"
app.name: "MyApp"
app.environment: "production"
---
# Overlay ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-stage
data:
spring.profiles.active: "staging"
app.environment: "staging"// Random value support
import org.springframework.boot.env.RandomValuePropertySource;
// Config tree support
import org.springframework.boot.env.ConfigTreePropertySource;
import org.springframework.boot.env.ConfigTreePropertySource.Option;
import org.springframework.boot.env.ConfigTreePropertySource.Value;
// Property source info
import org.springframework.boot.env.PropertySourceInfo;
// Spring Framework
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.boot.origin.OriginLookup;
import org.springframework.boot.origin.Origin;
// Java NIO
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
// IO
import java.io.InputStream;
import java.io.IOException;// GOOD - random values for testing
# application-test.properties
server.port=${random.int[8000,9000]}
test.database=${random.uuid}
// AVOID - random values in production (use explicit configuration)
# application-prod.properties
# server.port=${random.int[8000,9000]} # BAD - unpredictable ports in prod
server.port=8080 # GOOD - explicit portRationale: Random values are excellent for tests to avoid port conflicts, but production needs predictable, documented configuration.
// RECOMMENDED - trim trailing newlines from Kubernetes ConfigMaps/Secrets
ConfigTreePropertySource propertySource = new ConfigTreePropertySource(
"k8s-config",
Paths.get("/etc/config"),
ConfigTreePropertySource.Option.AUTO_TRIM_TRAILING_NEW_LINE // Important!
);Rationale: Kubernetes automatically adds trailing newlines to mounted files. Trimming prevents subtle bugs when comparing strings or using values in URLs.
# application.yml
spring:
config:
import: optional:configtree:/etc/config/
# ^^^^^^^^ Important - makes it optionalRationale: Makes the application start locally without Kubernetes mounts while still loading config when deployed.
/etc/config/
├── database/
│ ├── url
│ ├── username
│ └── password
├── cache/
│ ├── ttl
│ └── max-size
└── feature-flags/
├── new-ui-enabled
└── beta-features-enabledRationale: Subdirectories create natural property hierarchies (database.url, cache.ttl) making configuration more organized and maintainable.
// For secrets that can be rotated without restart
ConfigTreePropertySource propertySource = new ConfigTreePropertySource(
"secrets",
Paths.get("/etc/secrets"),
ConfigTreePropertySource.Option.ALWAYS_READ // Re-read on every access
);Rationale: Enables secret rotation without restarting the application, improving security and availability.
// GOOD - constrained random port range
server.port=${random.int[8080,8090]} // 10 possible values
// AVOID - too wide range
server.port=${random.int[1024,65535]} // 64,511 possibilities (harder to track)Rationale: Narrower ranges make debugging and port management easier while still providing randomness.
# Random values for test isolation
# DO NOT use in production - these change on every restart
test.instance.id=${random.uuid}
test.server.port=${random.int[9000,10000]}
test.database.name=testdb_${random.value}Rationale: Clear documentation prevents accidental production use and helps team members understand the configuration.
# Default value if random property fails
app.session.id=${random.uuid:default-session-id}
app.port=${random.int[8000,9000]:8080}Rationale: Provides fallback behavior if random value generation fails or in environments where it's disabled.
Problem: Files with dots in names can cause unexpected property hierarchies.
/etc/config/
└── spring.datasource.url # Single file
# Creates property: spring.datasource.url
# NOT: spring -> datasource -> urlConfusion: Expected nested properties but got flat property with dots in name.
Solution: Use subdirectories for hierarchies:
/etc/config/
└── spring/
└── datasource/
└── url
# Creates: spring.datasource.url (proper hierarchy)Best Practice: Reserve dots for actual property paths, use directories for nesting.
Problem: Expecting different random values per property access.
// WRONG expectation
@Value("${random.int}")
private int value1; // e.g., 12345
@Value("${random.int}")
private int value2; // Still 12345 (same value!)Why: Spring evaluates property placeholders once during bean creation.
Solution: Generate random values programmatically if you need different values:
@Configuration
public class RandomConfig {
@Bean
public Random random() {
return new Random();
}
}
@Component
public class MyService {
private final Random random;
public MyService(Random random) {
this.random = random;
}
public void doSomething() {
int value1 = random.nextInt(); // Different each time
int value2 = random.nextInt(); // Different from value1
}
}Problem: Application fails to start locally because /etc/config doesn't exist.
// In Spring Boot application
spring.config.import=configtree:/etc/config/ // FAILS locallyError:
FileNotFoundException: /etc/config (No such file or directory)Solution: Use optional: prefix:
spring:
config:
import: optional:configtree:/etc/config/Or profile-specific config:
spring:
config:
import: configtree:/etc/config/ # Default (fails if missing)
---
spring:
config:
activate:
on-profile: local
import: # Empty - skip configtree locallyProblem: Kubernetes adds trailing newlines to mounted files, breaking URLs and keys.
# File: /etc/config/api-url
https://api.example.com
↵ # <- Invisible trailing newline
# Property value: "https://api.example.com\n"
# API call fails: "https://api.example.com\n/users" (invalid URL)Solution: Always use AUTO_TRIM_TRAILING_NEW_LINE:
ConfigTreePropertySource propertySource = new ConfigTreePropertySource(
"config",
Paths.get("/etc/config"),
ConfigTreePropertySource.Option.AUTO_TRIM_TRAILING_NEW_LINE
);Or manually trim in code:
@Value("${api.url}")
private String apiUrl;
@PostConstruct
public void init() {
apiUrl = apiUrl.trim(); // Remove trailing whitespace
}Problem: Using random UUID for persistent identifiers.
# WRONG - changes on every restart
app.instance.id=${random.uuid}
# Database: store record with instance.id
# After restart: new UUID, can't find old records!Solution: Generate once and persist, or use stable identifiers:
@Configuration
public class InstanceIdConfig {
@Bean
public String instanceId() throws IOException {
Path idFile = Paths.get("data/instance-id.txt");
if (Files.exists(idFile)) {
return Files.readString(idFile).trim();
}
String newId = UUID.randomUUID().toString();
Files.createDirectories(idFile.getParent());
Files.writeString(idFile, newId);
return newId;
}
}Or use stable ID from environment:
app.instance.id=${HOSTNAME:${random.uuid}}Problem: Kubernetes uses symlinks for ConfigMaps, causing issues with certain file operations.
/etc/config/
└── ..data -> ..2024_01_01_12_00_00_123456789/
└── database-urlIssue: Some file reading libraries don't follow symlinks.
Solution: ConfigTreePropertySource handles this automatically, but if reading manually:
// WRONG - may not follow symlinks
File file = new File("/etc/config/database-url");
String value = Files.readString(file.toPath());
// CORRECT - explicitly follow symlinks
Path path = Paths.get("/etc/config/database-url");
String value = Files.readString(path.toRealPath()); // Resolves symlinksProblem: Each instance gets different random values, causing inconsistent behavior.
# Instance 1
app.partition.id=${random.int[1,10]} # Gets 3
# Instance 2
app.partition.id=${random.int[1,10]} # Gets 7
# Result: Instances can't communicate properlySolution: Use instance-specific environment variables:
# Kubernetes Deployment
env:
- name: APP_PARTITION_ID
valueFrom:
fieldRef:
fieldPath: metadata.annotations['partition-id']
# application.yml
app:
partition:
id: ${APP_PARTITION_ID}Problem: Not understanding that ConfigTree has lower precedence than application.yml.
# /etc/config/database/url
jdbc:postgresql://prod-db:5432/proddb
# application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
# Actual value used: jdbc:h2:mem:testdb (application.yml wins!)Solution: Check property source order:
@Component
public class PropertyDebugger implements ApplicationRunner {
@Autowired
private ConfigurableEnvironment env;
@Override
public void run(ApplicationArguments args) {
PropertySource<?> source = env.getPropertySources()
.get("configtree:/etc/config");
if (source != null) {
System.out.println("ConfigTree position: " +
env.getPropertySources().indexOf(source));
}
}
}Or use specific import order:
spring:
config:
import:
- optional:configtree:/etc/config/ # Loaded first (lower precedence)
- optional:file:./config/app.yml # Loaded second (higher precedence)Problem: ConfigTree scans entire directory tree on startup.
/etc/config/
├── 1000 files in root
├── subdir1/ (500 files)
├── subdir2/ (500 files)
└── subdir3/ (1000 files)
# Startup time: +5 seconds!Solution: Use selective imports:
spring:
config:
import:
- optional:configtree:/etc/config/database/ # Only database configs
- optional:configtree:/etc/config/cache/ # Only cache configsOr use lazy loading with ALWAYS_READ:
// Don't read all files at startup
ConfigTreePropertySource.Option.ALWAYS_READProblem: Using invalid range syntax for random values.
# WRONG - invalid syntax
app.id=${random.int[1-100]} # Dash instead of comma
app.id=${random.int[1, 100]} # Space after comma
app.id=${random.int(1,100)} # Parentheses instead of brackets
# CORRECT
app.id=${random.int[1,100]} # Comma, no spaces, square bracketsError:
IllegalArgumentException: Could not resolve placeholder 'random.int[1-100]'Solution: Always use exact syntax: ${random.int[min,max]} with no spaces.
Allows for customization of the application's Environment prior to the application context being refreshed.
package org.springframework.boot;
import org.springframework.core.env.ConfigurableEnvironment;
/**
* Allows for customization of the application's Environment prior to the
* application context being refreshed.
*
* EnvironmentPostProcessor implementations must be registered in META-INF/spring.factories
* using the fully qualified name of this class as the key.
*
* Implementations may implement Ordered interface or use @Order annotation
* if they wish to be invoked in specific order.
*
* Constructor parameters (optional):
* - DeferredLogFactory - Factory for loggers with output deferred until the application
* has been fully prepared
* - ConfigurableBootstrapContext - Bootstrap context for storing expensive objects
*
* Thread Safety: Implementations should be stateless and thread-safe.
*
* @since 4.0.0
*/
@FunctionalInterface
public interface EnvironmentPostProcessor {
/**
* Post-process the given environment.
* Called after property sources have been loaded but before the application
* context is refreshed.
*
* @param environment the environment to post-process
* @param application the application to which the environment belongs
*/
void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);
}Example - Adding Custom Property Source:
package com.example.env;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.HashMap;
import java.util.Map;
/**
* Adds computed properties to environment.
*/
public class CustomEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Map<String, Object> customProps = new HashMap<>();
// Add computed properties
String appName = environment.getProperty("spring.application.name", "app");
customProps.put("app.display-name", appName.toUpperCase());
customProps.put("app.environment", getEnvironmentType(environment));
// Add as high-priority property source
environment.getPropertySources().addFirst(
new MapPropertySource("customProperties", customProps)
);
}
private String getEnvironmentType(ConfigurableEnvironment environment) {
String[] profiles = environment.getActiveProfiles();
if (profiles.length == 0) {
return "development";
}
return profiles[0];
}
}Registration (META-INF/spring.factories):
org.springframework.boot.EnvironmentPostProcessor=\
com.example.env.CustomEnvironmentPostProcessorExample - Loading External Configuration:
package com.example.env;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.IOException;
import java.util.List;
/**
* Loads additional YAML configuration files.
*/
public class YamlEnvironmentPostProcessor implements EnvironmentPostProcessor {
private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Resource resource = new ClassPathResource("custom-config.yml");
if (resource.exists()) {
try {
List<PropertySource<?>> sources = loader.load("customConfig", resource);
for (PropertySource<?> source : sources) {
environment.getPropertySources().addLast(source);
}
} catch (IOException e) {
throw new IllegalStateException("Failed to load custom-config.yml", e);
}
}
}
}MapPropertySource that provides origin tracking for property values.
package org.springframework.boot.env;
import java.util.Map;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginLookup;
import org.springframework.core.env.MapPropertySource;
/**
* OriginLookup backed by a Map containing OriginTrackedValues.
* Provides both property values and their origins (source file, line number).
*
* Used internally by Spring Boot's property loading mechanism to track
* where each property value came from.
*
* Thread Safety: Thread-safe if the underlying map is immutable.
*
* @since 2.0.0
*/
public final class OriginTrackedMapPropertySource extends MapPropertySource
implements PropertySourceInfo, OriginLookup<String> {
/**
* Create a new OriginTrackedMapPropertySource instance.
*
* @param name the property source name
* @param source the underlying map source containing OriginTrackedValue objects
*/
public OriginTrackedMapPropertySource(String name, Map source);
/**
* Create a new OriginTrackedMapPropertySource instance.
*
* @param name the property source name
* @param source the underlying map source
* @param immutable if the underlying source is immutable and guaranteed not to change
*/
public OriginTrackedMapPropertySource(String name, Map source, boolean immutable);
/**
* Get the property value, unwrapping OriginTrackedValue if necessary.
*
* @param name the property name
* @return the property value (unwrapped)
*/
@Override
public @Nullable Object getProperty(String name);
/**
* Get the origin of the specified property.
*
* @param name the property name
* @return the origin or null if not tracked
*/
@Override
public @Nullable Origin getOrigin(String name);
/**
* Return whether this property source is immutable.
*
* @return true if immutable
*/
@Override
public boolean isImmutable();
}Example - Tracking Property Origins:
package com.example.config;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginLookup;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.stereotype.Component;
/**
* Utility to track property value origins for debugging.
*/
@Component
public class PropertyOriginTracker {
private final Environment environment;
public PropertyOriginTracker(Environment environment) {
this.environment = environment;
}
/**
* Find where a property value came from.
*
* @param propertyName the property name
* @return description of the property's origin
*/
public String findOrigin(String propertyName) {
for (PropertySource<?> propertySource : ((ConfigurableEnvironment) environment).getPropertySources()) {
if (propertySource instanceof OriginLookup<?> originLookup) {
Origin origin = originLookup.getOrigin(propertyName);
if (origin != null) {
return String.format("Property '%s' defined in %s",
propertyName,
origin
);
}
}
if (propertySource.containsProperty(propertyName)) {
return String.format("Property '%s' defined in %s (no origin tracking)",
propertyName,
propertySource.getName()
);
}
}
return String.format("Property '%s' not found", propertyName);
}
/**
* Log all property sources and their origins.
*/
public void logAllPropertySources() {
((ConfigurableEnvironment) environment).getPropertySources().forEach(ps -> {
System.out.println("PropertySource: " + ps.getName());
System.out.println(" Type: " + ps.getClass().getSimpleName());
System.out.println(" Origin tracking: " + (ps instanceof OriginLookup));
if (ps instanceof OriginTrackedMapPropertySource tracked) {
System.out.println(" Immutable: " + tracked.isImmutable());
}
});
}
}Origin Output Example:
Property 'server.port' defined in class path resource [application.yml] (document #0) - line 3, column 8
Property 'spring.datasource.url' defined in Config resource 'class path resource [application.properties]' via location 'optional:classpath:/' - line 15, column 25