Three new article modules: configuration binding, profiles and config data, Spring AOP
configuration-properties/ @ConfigurationProperties vs @Value on Spring Boot 4.1.1. The relaxed-binding matrix is generated by binding each spelling rather than transcribed, and re-checked against real processes -- the in-process probe was wrong twice before it was right. Records the three findings that came out of it: @Value does get relaxed resolution inside Spring Boot (Boot attaches ConfigurationPropertySources), the configuration processor silently stops generating metadata on JDK 23+ when declared as a plain dependency, and @Valid is not what makes nested constraints run. profiles-and-config/ Precedence, profiles, spring.config.import and config trees. /precedence reports every source holding a property in rank order with file and line, which turns "my profile file had no effect" into a two-line answer. Also pins the counterintuitive one: an imported file outranks the file that imported it. spring-aop/ Designators, proxy types, and aspects that do not fire. One advice per supported designator so the reference table is generated from real matches; all fourteen unsupported designators fed to the parser. Two corrections to the reference documentation: unsupported designators throw UnsupportedPointcutPrimitiveException (extends RuntimeException, not IllegalArgumentException), and spring-boot-starter-aop was renamed to spring-boot-starter-aspectj in Boot 4. 19 contract tests across the three modules, 15 captured transcripts, all regenerated by scripts/run-all.sh. Verified on Spring Boot 4.1.1, Spring Framework 7.0.9, JDK 25.0.4.1. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Gip4srpzMwjgoba6uEfbr5
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
||||
package com.ankurm.configprops.props;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
|
||||
/**
|
||||
* Constructor binding with a record — the shape this article recommends.
|
||||
*
|
||||
* <p>A record has exactly one canonical constructor, so Spring Boot uses constructor binding
|
||||
* without any annotation. {@code @ConstructorBinding} is only needed to disambiguate when a
|
||||
* type has more than one candidate constructor.
|
||||
*
|
||||
* <p>A record cannot declare field initialisers, so "default value" has to be expressed with
|
||||
* {@link DefaultValue}. Leaving it off does not give you {@code null} for a primitive-like
|
||||
* type — it gives you a binding failure. See {@code docs/04-records-and-defaults.md}.
|
||||
*
|
||||
* @param host SMTP host. No default: absent means the application must not start.
|
||||
* @param port SMTP port, defaulted rather than left null.
|
||||
* @param timeout Bound from an ISO-8601 duration or a suffixed form such as {@code 30s}.
|
||||
* @param retries Nested record, bound from {@code demo.mail.retries.*}.
|
||||
* @param recipients Bound from a YAML list, an indexed property list, or a comma-separated string.
|
||||
* @param headers Bound from arbitrary sub-keys under {@code demo.mail.headers.*}.
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "demo.mail")
|
||||
public record MailProperties(
|
||||
String host,
|
||||
@DefaultValue("587") int port,
|
||||
@DefaultValue("30s") Duration timeout,
|
||||
@DefaultValue RetryProperties retries,
|
||||
@DefaultValue List<String> recipients,
|
||||
@DefaultValue Map<String, String> headers) {
|
||||
|
||||
/**
|
||||
* Nested record. {@code @DefaultValue} on the enclosing component means this is
|
||||
* instantiated with its own defaults when {@code demo.mail.retries.*} is absent entirely,
|
||||
* rather than binding to {@code null}.
|
||||
*/
|
||||
public record RetryProperties(
|
||||
@DefaultValue("3") int maxAttempts,
|
||||
@DefaultValue("2s") Duration backoff) {
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.ankurm.configprops.props;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
|
||||
/**
|
||||
* One property, bound by the relaxed binder, used to prove which spellings actually work.
|
||||
*
|
||||
* <p>The canonical form of this property is {@code demo.relaxed.api-key}. The relaxed binder
|
||||
* accepts several spellings of that name; {@code @Value} accepts exactly one. The matrix in
|
||||
* {@code docs/02-relaxed-binding.md} is generated by
|
||||
* {@code com.ankurm.configprops.web.RelaxedBindingProbe}, not written by hand.
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "demo.relaxed")
|
||||
public record RelaxedProperties(@DefaultValue("(unset)") String apiKey) {
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.ankurm.configprops.props;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* Validation is the capability {@code @Value} does not have at all.
|
||||
*
|
||||
* <p>{@code @Validated} plus JSR-380 constraints turns a bad value into a startup failure with
|
||||
* a readable report, instead of a {@code NumberFormatException} somewhere in a request three
|
||||
* hours later. The failure text this produces is captured in
|
||||
* {@code docs/output/04-validation-failure.txt}.
|
||||
*
|
||||
* <p>{@code @Valid} on the nested component is written here because it is conventional, but
|
||||
* on this path it is not what makes the nested constraints run. Spring Boot's
|
||||
* {@code ValidationBindHandler} validates every object the binder finishes constructing, so
|
||||
* {@code demo.validated.pool.size} is checked with or without it. The rule that nested types
|
||||
* need {@code @Valid} comes from ordinary bean validation, where cascading is opt-in, and it
|
||||
* gets repeated about {@code @ConfigurationProperties} where it does not apply.
|
||||
* {@code BindingContractTests.nestedConstraintsFireWithoutValid} pins this. See
|
||||
* {@code docs/05-validation.md}.
|
||||
*/
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "demo.validated")
|
||||
public record ValidatedProperties(
|
||||
@NotBlank String name,
|
||||
@Min(1) @Max(65535) int port,
|
||||
@Pattern(regexp = "https?://.*") @DefaultValue("http://localhost") String endpoint,
|
||||
@Valid @DefaultValue Pool pool) {
|
||||
|
||||
public record Pool(@Min(1) @Max(100) @DefaultValue("10") int size) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user