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
77 lines
3.4 KiB
Bash
Executable File
77 lines
3.4 KiB
Bash
Executable File
#!/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/profiles-and-config-1.0.0.jar"
|
|
APP_MAIN="com.ankurm.profiles.ProfilesApplication"
|
|
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"; }
|
|
|
|
# Start the demo jar detached, record its PID, and block until it answers.
|
|
# Extra arguments are passed to the application. Environment variables for the run are
|
|
# passed by setting them on the call: `APP_ENV="A=1 B=2" start_app --spring.profiles.active=x`
|
|
start_app() {
|
|
stop_app
|
|
mkdir -p target
|
|
# Deliberately NOT setsid: setsid forks when it is not already a process-group leader,
|
|
# so $! would be the PID of a process that exits immediately and the JVM would survive
|
|
# every later stop_app. A surviving JVM keeps the port, the next scenario fails to bind,
|
|
# and curl answers from the previous scenario -- which reads exactly like the
|
|
# configuration change under test having had no effect. Three wrong findings in this
|
|
# repository came from that before it was tracked down.
|
|
if [ -n "${APP_ENV:-}" ]; then
|
|
# shellcheck disable=SC2086
|
|
env $APP_ENV nohup java -jar "$JAR" "$@" > /tmp/profiles-demo.log 2>&1 < /dev/null &
|
|
else
|
|
nohup java -jar "$JAR" "$@" > /tmp/profiles-demo.log 2>&1 < /dev/null &
|
|
fi
|
|
echo $! > target/app.pid
|
|
for _ in $(seq 1 60); do
|
|
curl -s -o /dev/null "http://127.0.0.1:${APP_PORT}/precedence" 2>/dev/null && return 0
|
|
kill -0 "$(cat target/app.pid)" 2>/dev/null || { echo "JVM exited during startup:"
|
|
tail -20 /tmp/profiles-demo.log; return 1; }
|
|
sleep 1
|
|
done
|
|
echo "application did not answer"; tail -20 /tmp/profiles-demo.log; return 1
|
|
}
|
|
|
|
# Stop it by recorded PID. Never by pattern: `ps | grep <jar name>` also matches the shell
|
|
# running the script, because the jar name is on that shell's own command line.
|
|
stop_app() {
|
|
if [ -f target/app.pid ]; then
|
|
pid=$(cat target/app.pid)
|
|
if [ -n "$pid" ] && grep -qa "profiles-and-config" "/proc/$pid/cmdline" 2>/dev/null; then
|
|
kill -9 "$pid" 2>/dev/null || true
|
|
wait "$pid" 2>/dev/null || true # reap, so bash prints no "Killed" notice
|
|
fi
|
|
rm -f target/app.pid
|
|
fi
|
|
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
|
|
}
|
|
|
|
# Print the precedence report for one property, compactly.
|
|
report() {
|
|
curl -s "http://127.0.0.1:${APP_PORT}/precedence?name=$1" | python3 -c '
|
|
import json,sys
|
|
d=json.load(sys.stdin)
|
|
print(" active profiles :", ", ".join(d["activeProfiles"]) or "(none)")
|
|
print(" effective value :", d["effectiveValue"])
|
|
for h in d["holders"]:
|
|
src=h["source"]
|
|
for noisy,short in (("Config resource \x27class path resource [","file "),
|
|
("\x27 via location \x27optional:classpath:/\x27}","")):
|
|
src=src.replace(noisy,short)
|
|
src=src.replace("OriginTrackedMapPropertySource {name=","").replace("]","")
|
|
print(" %d. %-34s <- %s" % (h["rank"], h["value"], src.strip()))
|
|
print(" holders that lost:", d["shadowedCount"])'
|
|
}
|