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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gip4srpzMwjgoba6uEfbr5
This commit is contained in:
52
configuration-properties/scripts/demo-env-binding.sh
Executable file
52
configuration-properties/scripts/demo-env-binding.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# The matrix re-asked the expensive way: one real JVM per spelling, the value supplied by the
|
||||
# operating system or by -D, and read back through both @ConfigurationProperties and @Value.
|
||||
#
|
||||
# This script exists because the in-process matrix was wrong twice before it was right. A
|
||||
# synthesised property source is not a running Spring Boot application, and when the two
|
||||
# disagree the running application wins.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
|
||||
RUN=(java -jar "$JAR" --spring.profiles.active=envprobe
|
||||
--spring.main.web-application-type=none --spring.main.banner-mode=off
|
||||
--logging.level.root=OFF)
|
||||
|
||||
run_env() {
|
||||
echo "\$ $1=secret-value java -jar $JAR"
|
||||
env -u DEMO_RELAXED_API_KEY -u DEMO_RELAXED_APIKEY "$1=secret-value" "${RUN[@]}" 2>&1 \
|
||||
| clean | grep -E "^(env-var-set|@Config|@Value|Environment)"
|
||||
echo
|
||||
}
|
||||
|
||||
run_sysprop() {
|
||||
echo "\$ java -D$1=secret-value -jar $JAR"
|
||||
env -u DEMO_RELAXED_API_KEY -u DEMO_RELAXED_APIKEY \
|
||||
java "-D$1=secret-value" -jar "$JAR" --spring.profiles.active=envprobe \
|
||||
--spring.main.web-application-type=none --spring.main.banner-mode=off \
|
||||
--logging.level.root=OFF 2>&1 \
|
||||
| clean | grep -E "^(@Config|@Value|Environment)"
|
||||
echo
|
||||
}
|
||||
|
||||
{
|
||||
echo "== real process, real environment: binding demo.relaxed.api-key =="
|
||||
echo
|
||||
echo "canonical property : demo.relaxed.api-key"
|
||||
echo "(MISS) means the property was not found and the declared default was used"
|
||||
echo
|
||||
echo "--- as an operating-system environment variable ---"
|
||||
echo
|
||||
run_env DEMO_RELAXED_API_KEY
|
||||
run_env DEMO_RELAXED_APIKEY
|
||||
echo "--- as a JVM system property (-D), i.e. the 'map' rows of the matrix ---"
|
||||
echo
|
||||
run_sysprop demo.relaxed.api-key
|
||||
run_sysprop demo.relaxed.apiKey
|
||||
run_sysprop demo.relaxed.apikey
|
||||
run_sysprop demo.relaxed.api.key
|
||||
echo "The last one is not a spelling of the property. api.key is two name elements;"
|
||||
echo "api-key is one. Nothing relaxed will ever join them."
|
||||
} > docs/output/02-env-var-binding.txt 2>&1
|
||||
cat docs/output/02-env-var-binding.txt
|
||||
51
configuration-properties/scripts/demo-metadata-generation.sh
Executable file
51
configuration-properties/scripts/demo-metadata-generation.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Does spring-boot-configuration-processor actually run?
|
||||
#
|
||||
# Two builds of the SAME sources with the SAME processor jar, differing only in how the
|
||||
# processor is declared to the compiler. On JDK 23+ that difference decides whether
|
||||
# META-INF/spring-configuration-metadata.json exists at all -- and neither build fails.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
|
||||
WORK="${TMPDIR:-/tmp}/configprops-metadata-ab"
|
||||
rm -rf "$WORK"; mkdir -p "$WORK/a" "$WORK/b"
|
||||
|
||||
PROC_JAR=$(find ~/.m2/repository/org/springframework/boot/spring-boot-configuration-processor \
|
||||
-name 'spring-boot-configuration-processor-*.jar' | sort | tail -1)
|
||||
CP=$("$MVN" -B -o -q dependency:build-classpath -Dmdep.outputFile=/dev/stdout 2>/dev/null | tail -1)
|
||||
|
||||
SOURCES=$(find src/main/java -name '*.java')
|
||||
|
||||
{
|
||||
echo "== is the annotation processor discovered? =="
|
||||
echo
|
||||
java -version 2>&1 | clean | head -1
|
||||
echo "processor jar: $(basename "$PROC_JAR")"
|
||||
echo
|
||||
echo "A) processor on the classpath, javac defaults -- what an <optional> dependency gives you"
|
||||
echo "\$ javac -cp <deps>:spring-boot-configuration-processor.jar -d a \$SOURCES"
|
||||
javac -nowarn -cp "$PROC_JAR:$CP" -d "$WORK/a" $SOURCES 2>&1 | clean | head -3 || true
|
||||
echo " spring-configuration-metadata.json files produced: \
|
||||
$(find "$WORK/a" -name 'spring-configuration-metadata.json' | wc -l)"
|
||||
echo
|
||||
echo "B) identical, plus -proc:full"
|
||||
echo "\$ javac -proc:full -cp <deps>:spring-boot-configuration-processor.jar -d b \$SOURCES"
|
||||
javac -nowarn -proc:full -cp "$PROC_JAR:$CP" -d "$WORK/b" $SOURCES 2>&1 | clean | head -3 || true
|
||||
echo " spring-configuration-metadata.json files produced: \
|
||||
$(find "$WORK/b" -name 'spring-configuration-metadata.json' | wc -l)"
|
||||
echo
|
||||
echo "Both compilations succeed. Only one of them has metadata."
|
||||
echo
|
||||
echo "== what this project's pom does instead =="
|
||||
echo "The processor is declared as an <annotationProcessorPath> on maven-compiler-plugin,"
|
||||
echo "which puts it on javac's --processor-path where discovery is not disabled:"
|
||||
echo
|
||||
echo "\$ mvn clean package && ls target/classes/META-INF/"
|
||||
ls target/classes/META-INF/ 2>/dev/null || echo "(run mvn package first)"
|
||||
echo
|
||||
echo "== the generated metadata, first entries =="
|
||||
python3 -m json.tool target/classes/META-INF/spring-configuration-metadata.json 2>/dev/null \
|
||||
| head -45
|
||||
} > docs/output/05-metadata-generation.txt 2>&1
|
||||
cat docs/output/05-metadata-generation.txt
|
||||
9
configuration-properties/scripts/demo-relaxed-matrix.sh
Executable file
9
configuration-properties/scripts/demo-relaxed-matrix.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# The relaxed-binding matrix, generated by binding each spelling rather than by hand.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
java -jar "$JAR" --spring.profiles.active=probe --spring.main.web-application-type=none \
|
||||
2>&1 | clean | sed -n '/== relaxed binding matrix ==/,/MISS = the property/p' \
|
||||
> docs/output/01-relaxed-matrix.txt
|
||||
cat docs/output/01-relaxed-matrix.txt
|
||||
22
configuration-properties/scripts/demo-validation.sh
Executable file
22
configuration-properties/scripts/demo-validation.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# What a failing @Validated @ConfigurationProperties actually prints at startup.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
{
|
||||
echo "== startup with demo.validated.* deliberately out of range =="
|
||||
echo "\$ java -jar $JAR --spring.profiles.active=badvalidation"
|
||||
echo
|
||||
java -jar "$JAR" --spring.profiles.active=badvalidation \
|
||||
--spring.main.web-application-type=none 2>&1 | clean \
|
||||
| sed -n '/APPLICATION FAILED TO START/,/^Update your application/p'
|
||||
echo
|
||||
echo
|
||||
echo "All four violations are reported at once, each with the file and line that supplied"
|
||||
echo "the value. The process refused to start rather than serving traffic with a pool size"
|
||||
echo "of 4000."
|
||||
echo
|
||||
echo "Note demo.validated.pool.size has no Origin line. It is a nested record reached"
|
||||
echo "through @Valid, and the binder tracks origins per bound property, not per constraint."
|
||||
} > docs/output/04-validation-failure.txt 2>&1
|
||||
cat docs/output/04-validation-failure.txt
|
||||
34
configuration-properties/scripts/demo-value-vs-binding.sh
Executable file
34
configuration-properties/scripts/demo-value-vs-binding.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Side by side: what the binder produced and what @Value produced, from one running process.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
{
|
||||
echo "== @ConfigurationProperties vs @Value, same application, same application.yaml =="
|
||||
echo
|
||||
scripts/run.sh > /dev/null
|
||||
echo "\$ curl -s localhost:8080/diag/bound"
|
||||
curl -s "http://127.0.0.1:${APP_PORT}/diag/bound" | python3 -m json.tool
|
||||
echo
|
||||
echo "demo.mail.recipients is a YAML block list. The binder produced both elements."
|
||||
echo "@Value produced an empty list -- placeholder resolution has no concept of a YAML"
|
||||
echo "sequence, so \${demo.mail.recipients:} fell through to its own empty default."
|
||||
echo
|
||||
echo "== the same list written as a comma-separated string, at higher precedence =="
|
||||
scripts/run.sh "" --demo.mail.recipients=x@e.com,y@e.com,z@e.com > /dev/null
|
||||
echo "\$ ./scripts/run.sh \"\" --demo.mail.recipients=x@e.com,y@e.com,z@e.com"
|
||||
curl -s "http://127.0.0.1:${APP_PORT}/diag/bound" \
|
||||
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(" binder :", d["mail"]["recipients"]); print(" @Value :", d["fromValueAnnotation"]["recipients"])'
|
||||
echo
|
||||
echo "Both see it. A comma-separated string is the one list shape @Value understands, and"
|
||||
echo "the command-line source outranks the YAML file for the binder as well."
|
||||
echo
|
||||
echo "== where did that value come from? =="
|
||||
echo "\$ curl -s 'localhost:8080/diag/origin?name=demo.mail.recipients'"
|
||||
curl -s "http://127.0.0.1:${APP_PORT}/diag/origin?name=demo.mail.recipients" | python3 -m json.tool
|
||||
echo
|
||||
echo "\$ curl -s 'localhost:8080/diag/origin?name=demo.mail.host'"
|
||||
curl -s "http://127.0.0.1:${APP_PORT}/diag/origin?name=demo.mail.host" | python3 -m json.tool
|
||||
scripts/stop.sh
|
||||
} > docs/output/03-value-vs-binding.txt 2>&1
|
||||
cat docs/output/03-value-vs-binding.txt
|
||||
16
configuration-properties/scripts/demo-versions.sh
Executable file
16
configuration-properties/scripts/demo-versions.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Exact versions every other transcript in this directory was produced against.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
{
|
||||
echo "== versions =="
|
||||
java -version 2>&1 | clean
|
||||
echo
|
||||
"$MVN" -B -o -q dependency:tree 2>/dev/null \
|
||||
| grep -E "spring-boot:jar|spring-core:jar|spring-context:jar|hibernate-validator:jar|jakarta.validation-api:jar" \
|
||||
| sed 's/^\[INFO\] //' || true
|
||||
echo
|
||||
echo "spring-boot-starter-parent: $(grep -A2 '<artifactId>spring-boot-starter-parent' pom.xml | grep '<version>' | sed 's/.*<version>\(.*\)<\/version>.*/\1/')"
|
||||
} > docs/output/00-versions.txt 2>&1
|
||||
cat docs/output/00-versions.txt
|
||||
13
configuration-properties/scripts/env.sh
Executable file
13
configuration-properties/scripts/env.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared environment. Point JAVA_HOME at a JDK 25 (or newer) installation.
|
||||
: "${JAVA_HOME:?set JAVA_HOME to a JDK 25+ installation}"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
MVN="${MVN:-mvn}"
|
||||
JAR="target/configuration-properties-1.0.0.jar"
|
||||
APP_MAIN="com.ankurm.configprops.ConfigBindingApplication"
|
||||
APP_PORT="${APP_PORT:-8080}"
|
||||
|
||||
# Strip environment noise that is an artefact of the machine, not of Spring:
|
||||
# the JVM prints a JAVA_TOOL_OPTIONS banner to stderr on every launch when a proxy
|
||||
# truststore is configured, and it would otherwise end up in every committed transcript.
|
||||
clean() { grep -v "Picked up JAVA_TOOL_OPTIONS" | grep -v "^OpenJDK 64-Bit Server VM warning"; }
|
||||
16
configuration-properties/scripts/run-all.sh
Executable file
16
configuration-properties/scripts/run-all.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate every transcript under docs/output/.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
|
||||
"$MVN" -B -q package -DskipTests
|
||||
|
||||
for demo in versions relaxed-matrix env-binding value-vs-binding validation metadata-generation; do
|
||||
echo "=== $demo ==="
|
||||
"scripts/demo-$demo.sh" > /dev/null
|
||||
done
|
||||
scripts/stop.sh
|
||||
echo
|
||||
echo "regenerated:"
|
||||
ls -1 docs/output/
|
||||
34
configuration-properties/scripts/run.sh
Executable file
34
configuration-properties/scripts/run.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the application and block until it answers. Extra arguments are passed to the app,
|
||||
# so a scenario can add --demo.mail.recipients=a,b,c without a new profile.
|
||||
# ./scripts/run.sh # defaults
|
||||
# ./scripts/run.sh csvlist # a profile
|
||||
# ./scripts/run.sh "" --demo.x=y # no profile, one override
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
source scripts/env.sh
|
||||
PROFILES="${1:-}"; shift || true
|
||||
LOG="${LOG:-/tmp/configprops-demo.log}"
|
||||
PIDFILE="${PIDFILE:-target/app.pid}"
|
||||
|
||||
scripts/stop.sh
|
||||
|
||||
ARGS=(-jar "$JAR")
|
||||
[ -n "$PROFILES" ] && ARGS+=("--spring.profiles.active=$PROFILES")
|
||||
ARGS+=("$@")
|
||||
|
||||
setsid nohup java "${ARGS[@]}" > "$LOG" 2>&1 < /dev/null &
|
||||
echo $! > "$PIDFILE"
|
||||
|
||||
for _ in $(seq 1 60); do
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${APP_PORT}/diag/bound" || true)
|
||||
[ "$code" = "200" ] && exit 0
|
||||
# If the JVM died -- most often because the port was still held -- fail fast and loudly
|
||||
# instead of letting curl answer from a process started by an earlier scenario.
|
||||
kill -0 "$(cat "$PIDFILE")" 2>/dev/null || { echo "JVM exited during startup:" >&2
|
||||
tail -25 "$LOG" >&2; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
echo "application did not answer; tail of $LOG:" >&2
|
||||
tail -40 "$LOG" >&2
|
||||
exit 1
|
||||
28
configuration-properties/scripts/stop.sh
Executable file
28
configuration-properties/scripts/stop.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stop the demo application.
|
||||
#
|
||||
# This uses a PID file rather than a pattern match, deliberately. `pkill -f spring-boot`
|
||||
# matches the shell that is running the script and takes the terminal with it. Even a
|
||||
# careful-looking `ps | grep '[c]onfiguration-properties'` matches the shell's own command
|
||||
# line whenever that string appears in the command you just typed -- which it does, because
|
||||
# you typed the jar name. Killing a recorded PID cannot misfire.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
PIDFILE="${PIDFILE:-target/app.pid}"
|
||||
|
||||
if [ -f "$PIDFILE" ]; then
|
||||
pid=$(cat "$PIDFILE")
|
||||
# Confirm the PID is still ours before signalling it: PIDs are reused.
|
||||
if [ -n "$pid" ] && grep -qa "configuration-properties" "/proc/$pid/cmdline" 2>/dev/null; then
|
||||
kill -9 "$pid" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "$PIDFILE"
|
||||
fi
|
||||
|
||||
# Killing the process is not the same as the socket closing, and a stale listener looks
|
||||
# exactly like your configuration change having had no effect.
|
||||
for _ in $(seq 1 40); do
|
||||
if ! (exec 3<>/dev/tcp/127.0.0.1/"${APP_PORT:-8080}") 2>/dev/null; then break; fi
|
||||
sleep 0.25
|
||||
done
|
||||
exec 3<&- 2>/dev/null || true
|
||||
Reference in New Issue
Block a user