or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

admin-jmx.mdansi-support.mdaot-native-image.mdapplication-info.mdavailability.mdbootstrap.mdbootstrapping.mdbuilder.mdcloud-platform.mdconfiguration-annotations.mdconfiguration-data.mdconfiguration-properties.mdconversion.mddiagnostics.mdenvironment-property-sources.mdindex.mdjson-support.mdlifecycle-events.mdlogging.mdorigin-tracking.mdresource-loading.mdretry-support.mdssl-tls.mdstartup-metrics.mdsupport.mdsystem-utilities.mdtask-execution.mdthreading.mdutilities.mdvalidation.mdweb-support.md
tile.json

environment-property-sources.mddocs/

Environment Property Sources

Quick Reference

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).

Overview

Environment property sources extend Spring's property source abstraction with Boot-specific capabilities:

  • RandomValuePropertySource: Generates random values for testing and runtime configuration
  • ConfigTreePropertySource: Reads properties from filesystem directory trees (Kubernetes integration)
  • PropertySourceInfo: Metadata interface for property source optimization

Core Components

RandomValuePropertySource

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 string

Usage Examples

Basic Random Values

package 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);
    }
}

Ranged Random Values

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}

Testing with Random Values

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
    }
}

Programmatic Access

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);
    }
}

ConfigTreePropertySource

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;
    }
}

Directory Structure Mapping

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)

Kubernetes ConfigMap Integration

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-config

Spring Boot Configuration:

# application.properties
spring.config.import=optional:configtree:/etc/config/

Using ConfigTreePropertySource

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);
        }
    }
}

Kubernetes Secrets Integration

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-credentials

Application 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;
    }
}

DefaultPropertiesPropertySource

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.

Class Definition

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);
}

Key Features

  • Lowest Priority: Always positioned as the last property source in the environment
  • Fallback Values: Provides default values when no other property source supplies a value
  • Merge Support: Can merge with existing default properties instead of replacing
  • Programmatic Configuration: Set via SpringApplication.setDefaultProperties()
  • Convention-Based: Automatically recognized by name "defaultProperties"

Usage Examples

Setting Default Properties

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);
    }
}

Programmatic Access and Manipulation

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);
    }
}

Conditional Default Properties

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);
        });
    }
}

Priority Order

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) <-- DefaultPropertiesPropertySource

Use Cases

  • Sensible Defaults: Provide fallback values for all configuration properties
  • Development Convenience: Set development-friendly defaults programmatically
  • Testing: Override defaults in tests without modifying configuration files
  • Documentation: Serve as executable documentation of all configurable properties
  • Backward Compatibility: Provide default values for new properties in upgrades

PropertySourceLoader

SPI (Service Provider Interface) for loading property sources from resources. Implementations are discovered through SpringFactoriesLoader and used to load configuration from various file formats.

Interface Definition

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;
}

Key Features

  • SPI Mechanism: Automatically discovered via spring.factories
  • Multi-Format Support: Can handle different file formats (properties, YAML, XML, custom)
  • Multi-Document Support: Can return multiple property sources from a single file
  • Extensible: Add custom file format support by implementing this interface
  • Automatic Integration: Used by Spring Boot's configuration loading mechanism

Built-in Implementations

Spring Boot provides two built-in implementations:

  1. PropertiesPropertySourceLoader: Loads .properties and .xml files
  2. YamlPropertySourceLoader: Loads .yml and .yaml files

Custom Implementation Example

package 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);
            }
        }
    }
}

Registration via spring.factories

Create or update src/main/resources/META-INF/spring.factories:

# PropertySourceLoader implementations
org.springframework.boot.env.PropertySourceLoader=\
com.example.config.JsonPropertySourceLoader

Advanced Example: Multi-Document Loader

package 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;
    }
}

Usage in Application

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.

Integration with ConfigDataLoader

For more advanced configuration loading (including import support), consider implementing ConfigDataLoader instead of PropertySourceLoader.

YamlPropertySourceLoader

Built-in implementation of PropertySourceLoader that loads .yml and .yaml files into property sources. Supports multi-document YAML files.

Class Definition

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;
}

Key Features

  • Multi-Document Support: Separates YAML documents with ---
  • Origin Tracking: Tracks where each property value came from
  • Type Preservation: Maintains YAML type information (lists, maps, etc.)
  • SnakeYAML Dependency: Requires org.yaml:snakeyaml on classpath
  • Automatic Registration: Registered in Spring Boot's spring.factories

Usage Examples

Single Document YAML

# 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: 3

This creates a single PropertySource with properties like:

  • server.port = 8080
  • server.ssl.enabled = true
  • spring.application.name = "my-app"
  • app.features[0] = "feature-a"
  • app.features[1] = "feature-b"

Multi-Document YAML

# 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: WARN

This creates three PropertySource instances:

  1. application.yml (document #0) - default properties
  2. application.yml (document #1) - dev profile properties
  3. application.yml (document #2) - prod profile properties

Programmatic YAML Loading

import 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
        }
    }
}

Dependency Requirement

Add SnakeYAML to your pom.xml:

<dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
</dependency>

Or build.gradle:

implementation 'org.yaml:snakeyaml'

Error Handling

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);
        }
    }
}

PropertiesPropertySourceLoader

Built-in implementation of PropertySourceLoader that loads .properties and .xml files into property sources.

Class Definition

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;
}

Key Features

  • Dual Format Support: Loads both .properties and .xml property files
  • Origin Tracking: Tracks where each property value came from
  • Multi-Document Support: Supports multi-document .properties files (separated by #---)
  • Standard Format: Uses Java's standard properties file format
  • Automatic Registration: Registered in Spring Boot's spring.factories

Usage Examples

Standard Properties File

# 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

XML Properties File

<!-- 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>

Multi-Document 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=DEBUG

Programmatic Loading

import 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());
        });
    }
}

Special Characters and Escaping

# 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

Comparison: Properties vs YAML

FeaturePropertiesYAML
Syntaxkey=valuekey: value
HierarchyDot notationIndentation
ListsIndexed [0], [1]Dash syntax
Multi-doc#--- separator--- separator
Comments# or !# only
Type SafetyStrings onlyNative types
ReadabilityGood for flatBetter for nested

Use Cases

  • Simple Configuration: Flat key-value pairs
  • Legacy Systems: Working with existing .properties files
  • XML Properties: Loading XML-based configuration
  • Tool Compatibility: Many tools generate .properties files
  • No Dependencies: Works without additional libraries

PropertySourceInfo

Interface 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);
    }
}

Production Best Practices

Kubernetes ConfigMap Hot Reload

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");
    }
}

Error Handling

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
            );
        }
    }
}

Thread Safety

RandomValuePropertySource:

  • Thread-safe: Uses ThreadLocalRandom internally
  • Non-caching: Each property access generates a new random value
  • Concurrent access: Safe for multiple threads to access simultaneously

ConfigTreePropertySource:

  • Immutable by default: Property values cached on first read
  • Thread-safe: Immutable state allows safe concurrent access
  • ALWAYS_READ option: Not thread-safe if files change during read

Common Patterns

Testing with Random Databases

# 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]}

Multi-Environment ConfigMaps

# 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"

Import Statements

// 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;

Best Practices

1. Random Values - Use for Non-Production Environments

// 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 port

Rationale: Random values are excellent for tests to avoid port conflicts, but production needs predictable, documented configuration.

2. ConfigTree - Always Use AUTO_TRIM_TRAILING_NEW_LINE

// 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.

3. Use Optional ConfigTree Import

# application.yml
spring:
  config:
    import: optional:configtree:/etc/config/
    #       ^^^^^^^^ Important - makes it optional

Rationale: Makes the application start locally without Kubernetes mounts while still loading config when deployed.

4. Namespace ConfigTree Properties with Subdirectories

/etc/config/
  ├── database/
  │   ├── url
  │   ├── username
  │   └── password
  ├── cache/
  │   ├── ttl
  │   └── max-size
  └── feature-flags/
      ├── new-ui-enabled
      └── beta-features-enabled

Rationale: Subdirectories create natural property hierarchies (database.url, cache.ttl) making configuration more organized and maintainable.

5. Use ALWAYS_READ for Dynamic Secrets

// 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.

6. Validate Random Port Ranges

// 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.

7. Document Random Property Usage

# 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.

8. Combine Random Values with Meaningful Defaults

# 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.

Common Pitfalls

1. ConfigTree File Names with Dots - Property Collision

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 -> url

Confusion: 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.

2. Random Values Not Random Per Request

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
    }
}

3. ConfigTree Without Kubernetes - Missing Directory

Problem: Application fails to start locally because /etc/config doesn't exist.

// In Spring Boot application
spring.config.import=configtree:/etc/config/  // FAILS locally

Error:

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 locally

4. Trailing Newlines in ConfigTree Values

Problem: 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
}

5. Random UUID Not Stable Across Restarts

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}}

6. ConfigTree Symlinks Not Followed

Problem: Kubernetes uses symlinks for ConfigMaps, causing issues with certain file operations.

/etc/config/
  └── ..data -> ..2024_01_01_12_00_00_123456789/
      └── database-url

Issue: 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 symlinks

7. Random Values in Multi-Instance Deployments

Problem: 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 properly

Solution: 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}

8. ConfigTree Property Precedence Confusion

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)

9. Large ConfigTree Directories - Performance Impact

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 configs

Or use lazy loading with ALWAYS_READ:

// Don't read all files at startup
ConfigTreePropertySource.Option.ALWAYS_READ

10. Mixing random.int and random.long Range Syntax

Problem: 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 brackets

Error:

IllegalArgumentException: Could not resolve placeholder 'random.int[1-100]'

Solution: Always use exact syntax: ${random.int[min,max]} with no spaces.

Environment Post-Processing

EnvironmentPostProcessor

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.CustomEnvironmentPostProcessor

Example - 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);
            }
        }
    }
}

OriginTrackedMapPropertySource

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

Related Documentation

  • Configuration Data
  • Configuration Properties
  • Cloud Platform Support