Is Your Configuration Actually Bound? A Full‑Stack Breakdown of Spring Boot @ConfigurationProperties
The article explains the complete binding process of Spring Boot’s @ConfigurationProperties, covering configuration loading, property source merging, Binder execution, validation, registration, registration methods, naming conventions, complex type handling, metadata generation, and dynamic refresh, while providing practical code examples and troubleshooting tips.
What the binding chain actually does
Spring Boot configuration binding consists of four core stages:
Stage 1 – Configuration loading → building Environment : At startup the Environment loads all PropertySource s in order (command‑line args → env vars → application-{profile}.yml → application.yml → defaults) and stores every key‑value pair in the environment’s property source collection.
Stage 2 – Property source merging and priority handling: When the same key appears in multiple sources, the higher‑priority source overwrites the lower one, which explains why command‑line arguments can override YAML values and profile‑specific files can override generic ones.
Stage 3 – Binder execution: The Binder extracts properties with the configured prefix from the Environment, performs relaxed binding, type conversion, and reflection to set each field on the target POJO. This step handles nested objects, List, Map, enums, Duration, DataSize, etc.
Stage 4 – Validation and registration: If the configuration class is annotated with @Validated, JSR‑303 validation runs after binding; failures abort startup. Finally the bound object is registered as a Spring bean for injection.
In short: YAML → Environment property sources → Binder (prefix extraction & reflection) → Validation → Bean registration.</p> <p>Most "not bound" problems (≈90%) stem from stage 3.</p> </blockquote> <h2>Basic usage – three ways to register a configuration class</h2> <h3>1️⃣ <code>@Component + @ConfigurationProperties <code>@Data @Component @ConfigurationProperties(prefix = "app.oss") public class OssProperties { private String endpoint; private String accessKey; private String secretKey; private String bucketName; }</code> ✅ Simple, works out‑of‑the‑box. ❌ Couples the POJO to Spring, can become messy with many config classes. 2️⃣ @EnableConfigurationProperties (official recommendation) <code>@Data @ConfigurationProperties(prefix = "app.oss") public class OssProperties { private String endpoint; private String accessKey; private String secretKey; private String bucketName; }</code> <code>@SpringBootApplication @EnableConfigurationProperties(OssProperties.class) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }</code> ✅ POJO stays free of Spring annotations; the official starters use this pattern. 3️⃣ @ConfigurationPropertiesScan (Spring Boot 2.2+ – most elegant) <code>@SpringBootApplication @ConfigurationPropertiesScan(basePackages = "com.example.config") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }</code> <code>@Data @ConfigurationProperties(prefix = "app.oss") public class OssProperties { private String endpoint; private String accessKey; private String secretKey; private String bucketName; }</code> ✅ Zero‑intrusion, unified scanning, ideal when many configuration classes exist. Core rules – relaxed binding and naming conversion Spring Boot supports relaxed binding, allowing four naming styles for a Java field such as accessKey : access-key (kebab‑case) – recommended for YAML. accessKey (camelCase) – matches the Java field directly. access_key (snake_case) – compatible with legacy styles. ACCESS_KEY (UPPER_CASE) – mainly for environment variables. The prefix itself must be all‑lowercase kebab‑case; camelCase prefixes are invalid. <code>// ✅ correct @ConfigurationProperties(prefix = "app.oss-config") // ❌ incorrect – camelCase not allowed @ConfigurationProperties(prefix = "app.ossConfig")</code> Environment variables replace dots with underscores, drop hyphens, and are upper‑cased (e.g. app.oss.endpoint → APP_OSS_ENDPOINT ), which is why containerised deployments can override YAML seamlessly. Exact matching Relaxed binding does not perform fuzzy matching; the semantic name must correspond. accessKey matches access-key and access_key , but not accesskey (missing separator) or access-keys (extra “s”). Complex type binding 4.1 Nested objects <code>@Data @ConfigurationProperties(prefix = "app") public class AppProperties { private String name; private OssConfig oss; private SmsConfig sms; @Data public static class OssConfig { private String endpoint; private String bucket; } @Data public static class SmsConfig { private String provider; private String apiKey; } }</code> <code>app: name: my-application oss: endpoint: http://oss.example.com bucket: my-bucket sms: provider: aliyun api-key: sms-api-key</code> 4.2 List collections <code>@Data @ConfigurationProperties(prefix = "app.security") public class SecurityProperties { /** whitelist URL list */ private List<String> whiteList; /** allowed IP ranges */ private List<String> allowedIpRanges; }</code> <code>app: security: white-list: - /login - /register - /api/public/** allowed-ip-ranges: - 192.168.1.0/24 - 10.0.0.0/8</code> 4.3 Map key‑value pairs <code>@Data @ConfigurationProperties(prefix = "app.cache") public class CacheProperties { /** expiration time per cache name */ private Map<String, Long> expireTimes; /** multiple data‑source configs */ private Map<String, DataSourceConfig> dataSources; @Data public static class DataSourceConfig { private String url; private String username; private String password; } }</code> <code>app: cache: expire-times: user-cache: 3600 order-cache: 1800 token-cache: 7200 data-sources: master: url: jdbc:mysql://localhost:3306/master username: root password: 123456 slave: url: jdbc:mysql://localhost:3306/slave username: root password: 123456</code> 4.4 Enum binding <code>@Data @ConfigurationProperties(prefix = "app.upload") public class UploadProperties { /** storage type */ private StorageType storageType = StorageType.LOCAL; public enum StorageType { LOCAL, MINIO, OSS } }</code> YAML values local , LOCAL or Local all bind correctly. 4.5 Special types ( Duration , DataSize ) <code>@Data @ConfigurationProperties(prefix = "app.server") public class ServerProperties { /** connection timeout, e.g. 10s, 5m, 1h */ private Duration connectionTimeout = Duration.ofSeconds(10); /** max file size, e.g. 10MB, 1GB */ private DataSize maxFileSize = DataSize.ofMegabytes(10); }</code> <code>app: server: connection-timeout: 30s max-file-size: 50MB</code> Configuration validation – fail fast <code>@Data @ConfigurationProperties(prefix = "app.oss") @Validated public class OssProperties { @NotBlank(message = "OSS endpoint cannot be empty") private String endpoint; @NotBlank(message = "accessKey cannot be empty") private String accessKey; @NotBlank(message = "secretKey cannot be empty") private String secretKey; @NotBlank(message = "bucketName cannot be empty") private String bucketName; @Min(1) @Max(100) private Integer maxConnections = 10; @Valid private RetryConfig retry = new RetryConfig(); @Data public static class RetryConfig { @Min(0) private Integer maxRetries = 3; @NotNull private Duration retryInterval = Duration.ofSeconds(1); } }</code> If a property is invalid, startup aborts with a clear BindException showing the offending key, value and reason. Metadata generation & IDE assistance Add the optional processor dependency: <code><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency></code> Annotate fields with Javadoc‑style comments; after compilation Spring generates META-INF/spring-configuration-metadata.json , enabling IDEs to suggest property names, types, defaults and documentation while editing YAML. Dynamic configuration refresh For non‑stateful settings you can listen to EnvironmentChangeEvent or use Spring Cloud Config’s @RefreshScope together with the /actuator/refresh endpoint to re‑bind beans without restarting. <code>@Component @RequiredArgsConstructor @Slf4j public class ConfigRefreshListener { private final Environment environment; @EventListener public void onEnvironmentChange(EnvironmentChangeEvent event) { log.info("Configuration changed, keys: {}", event.getKeys()); // apply new values, e.g., adjust thread‑pool parameters } }</code> <code>@Data @Component @RefreshScope @ConfigurationProperties(prefix = "app.thread-pool") public class ThreadPoolProperties { private Integer coreSize; private Integer maxSize; private Integer queueCapacity; }</code> Note that only stateless properties can be refreshed; things like database URLs or ports still require a restart. Full summary @ConfigurationProperties may look like a simple annotation, but it triggers a complete configuration‑binding pipeline: loading YAML into Environment , merging property sources, Binder‑based reflection, type conversion, validation, metadata generation and optional dynamic refresh. Understanding each step eliminates guesswork, enables precise troubleshooting, and makes configuration a reliable foundation for Spring Boot applications.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
