diff --git a/boot42-preview/README.md b/boot42-preview/README.md new file mode 100644 index 0000000..b399bba --- /dev/null +++ b/boot42-preview/README.md @@ -0,0 +1,16 @@ +# boot42-preview + +Companion module for the ankurm.com article *Spring Boot 4.2 Preview: AMQP 1.0, Buildpack Image Cache and What to Test Now*. +Everything runs against **4.2.0-M2** (a milestone, not GA) and, where a comparison matters, against **4.1.1** (GA). + +| Path | What it is | +|---|---| +| `pom.xml` / `pom-41.xml` | the same project on 4.2.0-M2 and on 4.1.1 (a `${revision}` parent does not resolve from a repository, hence two files) | +| `src/test/.../ForwardedHeadersTest` | what the container believes under each forwarded-header strategy | +| `src/test/.../BindingOrderTest` | Set and Map iteration order from a properties file | +| `src/broken/UsesRestTemplate.java` | not built; compiled by the facts script to capture the deprecation warnings | +| `src/main/.../RestClientReplacement` | the replacement, compiled clean | +| `examples/build-image-cache-fragment.xml` | **not executed** (needs Docker and a registry) | +| `output/` | every transcript quoted in the article; `./scripts/run-all.sh` regenerates them | + +There is deliberately no `docs/` folder: the explanations live in the article. diff --git a/boot42-preview/examples/build-image-cache-fragment.xml b/boot42-preview/examples/build-image-cache-fragment.xml new file mode 100644 index 0000000..986ce0d --- /dev/null +++ b/boot42-preview/examples/build-image-cache-fragment.xml @@ -0,0 +1,15 @@ + + + org.springframework.boot + spring-boot-maven-plugin + + + + + registry.example.com/team/demo-build-cache:latest + + + + + diff --git a/boot42-preview/output/01-forwarded-headers-on-4.1.1.txt b/boot42-preview/output/01-forwarded-headers-on-4.1.1.txt new file mode 100644 index 0000000..08e0613 --- /dev/null +++ b/boot42-preview/output/01-forwarded-headers-on-4.1.1.txt @@ -0,0 +1,10 @@ +# 01-forwarded-headers (Spring Boot 4.1.1, JDK 25) +none/x-forwarded scheme=http secure=false host=localhost port= +none/rfc7239 scheme=http secure=false host=localhost port= +native/x-forwarded scheme=https secure=true host=shop.example.com port=443 +native/rfc7239 scheme=http secure=false host=localhost port= +framework/x-forwarded scheme=https secure=true host=shop.example.com port=443 +framework/rfc7239 scheme=https secure=true host=shop.example.com port=443 +framework+standard/x-forwarded scheme=https secure=true host=shop.example.com port=443 +framework+standard/rfc7239 scheme=https secure=true host=shop.example.com port=443 +header-format=forwarded -> started (property ignored) diff --git a/boot42-preview/output/01-forwarded-headers.txt b/boot42-preview/output/01-forwarded-headers.txt new file mode 100644 index 0000000..8a88539 --- /dev/null +++ b/boot42-preview/output/01-forwarded-headers.txt @@ -0,0 +1,10 @@ +# 01-forwarded-headers (Spring Boot 4.2.0-M2, JDK 25) +none/x-forwarded scheme=http secure=false host=localhost port= +none/rfc7239 scheme=http secure=false host=localhost port= +native/x-forwarded scheme=https secure=true host=shop.example.com port=443 +native/rfc7239 scheme=http secure=false host=localhost port= +framework/x-forwarded scheme=https secure=true host=shop.example.com port=443 +framework/rfc7239 scheme=http secure=false host=localhost port= +framework+standard/x-forwarded scheme=http secure=false host=localhost port= +framework+standard/rfc7239 scheme=https secure=true host=shop.example.com port=443 +header-format=forwarded -> IllegalArgumentException: No enum constant org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.HeaderFormat.forwarded diff --git a/boot42-preview/output/02-set-and-map-binding-order-on-4.1.1.txt b/boot42-preview/output/02-set-and-map-binding-order-on-4.1.1.txt new file mode 100644 index 0000000..ee452cd --- /dev/null +++ b/boot42-preview/output/02-set-and-map-binding-order-on-4.1.1.txt @@ -0,0 +1,6 @@ +# 02-set-and-map-binding-order (Spring Boot 4.1.1, JDK 25) +file order : zulu, alpha, mike, bravo, yankee, charlie +Set class : java.util.LinkedHashSet +Set order : [zulu, alpha, mike, bravo, yankee, charlie] +Map class : java.util.LinkedHashMap +Map key order : [zulu, alpha, mike, bravo, yankee, charlie] diff --git a/boot42-preview/output/02-set-and-map-binding-order.txt b/boot42-preview/output/02-set-and-map-binding-order.txt new file mode 100644 index 0000000..b0076f8 --- /dev/null +++ b/boot42-preview/output/02-set-and-map-binding-order.txt @@ -0,0 +1,6 @@ +# 02-set-and-map-binding-order (Spring Boot 4.2.0-M2, JDK 25) +file order : zulu, alpha, mike, bravo, yankee, charlie +Set class : java.util.LinkedHashSet +Set order : [zulu, alpha, mike, bravo, yankee, charlie] +Map class : java.util.LinkedHashMap +Map key order : [zulu, alpha, mike, bravo, yankee, charlie] diff --git a/boot42-preview/output/03-amqp-starters.txt b/boot42-preview/output/03-amqp-starters.txt new file mode 100644 index 0000000..58108e4 --- /dev/null +++ b/boot42-preview/output/03-amqp-starters.txt @@ -0,0 +1,24 @@ +# 03-amqp-starters (POM descriptions and contents straight from Maven Central) +spring-boot-starter-amqp 4.1.1 HTTP 200 "Starter for using Spring AMQP and Rabbit MQ" -> spring-boot-amqp +spring-boot-starter-amqp 4.2.0-M2 HTTP 200 "Starter for generic AMQP 1.0 support" -> spring-boot-amqp +spring-boot-starter-rabbitmq 4.1.1 HTTP 404 (does not exist) +spring-boot-starter-rabbitmq 4.2.0-M2 HTTP 200 "Starter for using RabbitMQ messaging and streaming broker" -> spring-boot-rabbitmq +spring-boot-starter-amqp-rabbitmq 4.1.1 HTTP 404 (does not exist) +spring-boot-starter-amqp-rabbitmq 4.2.0-M2 HTTP 200 "Starter for using Spring AMQP with Rabbit MQ over AMQP 1.0 protocol" -> spring-boot-amqp-rabbitmq + +# auto-configuration each module registers (AutoConfiguration.imports) +--- spring-boot-amqp 4.1.1 + org.springframework.boot.amqp.autoconfigure.RabbitAutoConfiguration + org.springframework.boot.amqp.autoconfigure.health.RabbitHealthContributorAutoConfiguration + org.springframework.boot.amqp.autoconfigure.metrics.RabbitMetricsAutoConfiguration +--- spring-boot-amqp 4.2.0-M2 + org.springframework.boot.amqp.autoconfigure.AmqpAutoConfiguration + +--- spring-boot-rabbitmq 4.2.0-M2 + org.springframework.boot.rabbitmq.autoconfigure.RabbitAutoConfiguration + org.springframework.boot.rabbitmq.autoconfigure.health.RabbitHealthContributorAutoConfiguration + org.springframework.boot.rabbitmq.autoconfigure.metrics.RabbitMetricsAutoConfiguration +--- spring-boot-amqp-rabbitmq 4.2.0-M2 + org.springframework.boot.amqp.rabbitmq.autoconfigure.AmqpRabbitAutoConfiguration + org.springframework.boot.amqp.rabbitmq.autoconfigure.health.AmqpRabbitHealthContributorAutoConfiguration + diff --git a/boot42-preview/output/04-buildpack-cache-classes.txt b/boot42-preview/output/04-buildpack-cache-classes.txt new file mode 100644 index 0000000..928b2ee --- /dev/null +++ b/boot42-preview/output/04-buildpack-cache-classes.txt @@ -0,0 +1,50 @@ +# 04-buildpack-cache-classes (javap, spring-boot-buildpack-platform and spring-boot-maven-plugin) +--- buildpack-platform 4.1.1: classes matching Cache + Cache$Bind.class + Cache$Format.class + Cache$Volume.class + Cache.class +--- buildpack-platform 4.2.0-M2: classes matching Cache + Cache.class + ImageCache.class + LocalCache$Bind.class + LocalCache$Volume.class + LocalCache.class +--- javap Cache (4.2.0-M2) +Compiled from "Cache.java" +public interface Cache { + public default LocalCache$Volume getVolume(); + public default LocalCache$Bind getBind(); + public default ImageCache getImage(); + public static LocalCache volume(java.lang.String); + public static LocalCache volume(VolumeName); + public static LocalCache bind(java.lang.String); + public static ImageCache image(java.lang.String); +} +--- javap org.springframework.boot.maven.CacheInfo (4.2.0-M2) +Compiled from "CacheInfo.java" +public class CacheInfo { + public CacheInfo(); + public void setVolume(LocalCacheInfo$VolumeCacheInfo); + public void setBind(LocalCacheInfo$BindCacheInfo); + public void setImage(CacheInfo$ImageCacheInfo); + Cache asCache(); + static CacheInfo fromVolume(LocalCacheInfo$VolumeCacheInfo); + static CacheInfo fromBind(LocalCacheInfo$BindCacheInfo); + static CacheInfo fromImage(CacheInfo$ImageCacheInfo); +} +--- javap org.springframework.boot.maven.LocalCacheInfo (4.2.0-M2) +Compiled from "LocalCacheInfo.java" +public class LocalCacheInfo { + public LocalCacheInfo(); + LocalCacheInfo(LocalCache); + public void setVolume(LocalCacheInfo$VolumeCacheInfo); + public void setBind(LocalCacheInfo$BindCacheInfo); + LocalCache asCache(); + static LocalCacheInfo fromVolume(LocalCacheInfo$VolumeCacheInfo); + static LocalCacheInfo fromBind(LocalCacheInfo$BindCacheInfo); +} +--- javap -p Image, fields mentioning cache/workspace (4.2.0-M2) + LocalCacheInfo buildWorkspace; + CacheInfo buildCache; + LocalCacheInfo launchCache; diff --git a/boot42-preview/output/05-resttemplate-deprecations.txt b/boot42-preview/output/05-resttemplate-deprecations.txt new file mode 100644 index 0000000..302ac14 --- /dev/null +++ b/boot42-preview/output/05-resttemplate-deprecations.txt @@ -0,0 +1,22 @@ +# 05-resttemplate-deprecations (javac -Xlint:deprecation on src/broken/UsesRestTemplate.java) +--- Spring Boot 4.1.1 +--- Spring Boot 4.2.0-M2 +UsesRestTemplate.java:8: warning: [removal] RestTemplate in org.springframework.web.client has been deprecated and marked for removal + RestTemplate viaBuilder(RestTemplateBuilder builder) { + ^ +UsesRestTemplate.java:8: warning: [removal] RestTemplateBuilder in org.springframework.boot.restclient has been deprecated and marked for removal + RestTemplate viaBuilder(RestTemplateBuilder builder) { + ^ +UsesRestTemplate.java:12: warning: [removal] TestRestTemplate in org.springframework.boot.resttestclient has been deprecated and marked for removal + TestRestTemplate viaTest(TestRestTemplate t) { + ^ +UsesRestTemplate.java:12: warning: [removal] TestRestTemplate in org.springframework.boot.resttestclient has been deprecated and marked for removal + TestRestTemplate viaTest(TestRestTemplate t) { + ^ +UsesRestTemplate.java:16: warning: [removal] RestTemplate in org.springframework.web.client has been deprecated and marked for removal + RestTemplate plain() { + ^ +UsesRestTemplate.java:17: warning: [removal] RestTemplate in org.springframework.web.client has been deprecated and marked for removal + return new RestTemplate(); + ^ +6 warnings diff --git a/boot42-preview/output/06-restclient-replacement.txt b/boot42-preview/output/06-restclient-replacement.txt new file mode 100644 index 0000000..144a81a --- /dev/null +++ b/boot42-preview/output/06-restclient-replacement.txt @@ -0,0 +1,2 @@ +# 06-restclient-replacement (javac -Xlint:deprecation on the RestClient version, Spring Boot 4.2.0-M2) +javac finished; lines above this one are warnings (none means a clean compile) diff --git a/boot42-preview/pom-41.xml b/boot42-preview/pom-41.xml new file mode 100644 index 0000000..b1c8dfe --- /dev/null +++ b/boot42-preview/pom-41.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.1.1 + + + + com.ankurm + boot42-preview-on-41 + 1.0.0 + boot42-preview + Spring Boot 4.2 milestone preview: what to test now, run against 4.2.0-M2 and 4.1.1 + + + 25 + 4.1.1 + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-restclient + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + + + + + maven-surefire-plugin + + + ${boot.label} + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/boot42-preview/pom.xml b/boot42-preview/pom.xml new file mode 100644 index 0000000..e93aa7d --- /dev/null +++ b/boot42-preview/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.2.0-M2 + + + + com.ankurm + boot42-preview + 1.0.0 + boot42-preview + Spring Boot 4.2 milestone preview: what to test now, run against 4.2.0-M2 and 4.1.1 + + + 25 + 4.2.0-M2 + + + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springframework.boot + spring-boot-starter-restclient + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + + + + + maven-surefire-plugin + + + ${boot.label} + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/boot42-preview/scripts/capture-facts.sh b/boot42-preview/scripts/capture-facts.sh new file mode 100755 index 0000000..72d8e46 --- /dev/null +++ b/boot42-preview/scripts/capture-facts.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# Facts read from jars and POMs on Maven Central: nothing here needs a running application. +set -euo pipefail +cd "$(dirname "$0")/.." +M="$HOME/.m2/repository/org/springframework/boot" +CEN=https://repo1.maven.org/maven2/org/springframework/boot +W=$(mktemp -d) +for v in 4.1.1 4.2.0-M2; do for a in spring-boot-maven-plugin spring-boot-buildpack-platform spring-boot-amqp spring-boot-amqp-rabbitmq spring-boot-rabbitmq; do + mvn -B -q dependency:get -Dartifact=org.springframework.boot:$a:$v >/dev/null 2>&1 || true +done; done + +{ + echo "# 03-amqp-starters (POM descriptions and contents straight from Maven Central)" + for a in spring-boot-starter-amqp spring-boot-starter-rabbitmq spring-boot-starter-amqp-rabbitmq; do + for v in 4.1.1 4.2.0-M2; do + for try in 1 2 3 4 5; do + code=$(curl -s -o "$W/p.xml" -w '%{http_code}' "$CEN/$a/$v/$a-$v.pom") + [ "$code" = 429 ] || break + sleep $((try * 3)) + done + if [ "$code" = 200 ]; then + d=$(grep -m1 '' "$W/p.xml" | sed 's/ *<[^>]*>//g') + dep=$(grep -E 'spring-boot-(amqp|rabbitmq|amqp-rabbitmq)' "$W/p.xml" | sed 's/ *<[^>]*>//g' | paste -sd, -) + printf '%-34s %-10s HTTP %s "%s" -> %s\n' "$a" "$v" "$code" "$d" "$dep" + else + printf '%-34s %-10s HTTP %s (does not exist)\n' "$a" "$v" "$code" + fi + done + done + echo + echo "# auto-configuration each module registers (AutoConfiguration.imports)" + for a in spring-boot-amqp spring-boot-rabbitmq spring-boot-amqp-rabbitmq; do + for v in 4.1.1 4.2.0-M2; do + j="$M/$a/$v/$a-$v.jar" + if [ -f "$j" ]; then + echo "--- $a $v"; unzip -p "$j" META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 2>/dev/null | sed 's/^/ /' || true; echo + fi + done + done +} > output/03-amqp-starters.txt + +{ + echo "# 04-buildpack-cache-classes (javap, spring-boot-buildpack-platform and spring-boot-maven-plugin)" + for v in 4.1.1 4.2.0-M2; do + echo "--- buildpack-platform $v: classes matching Cache" + unzip -l "$M/spring-boot-buildpack-platform/$v/spring-boot-buildpack-platform-$v.jar" | awk '{print $4}' | grep -E 'build/.*Cache' | sed 's#.*/build/# #' + done + mkdir -p "$W/bp" + unzip -q -o "$M/spring-boot-buildpack-platform/4.2.0-M2/spring-boot-buildpack-platform-4.2.0-M2.jar" 'org/springframework/boot/buildpack/platform/build/*Cache*' -d "$W/bp" + unzip -q -o "$M/spring-boot-maven-plugin/4.2.0-M2/spring-boot-maven-plugin-4.2.0-M2.jar" 'org/springframework/boot/maven/*' -d "$W/bp" + echo "--- javap Cache (4.2.0-M2)" + javap -cp "$W/bp" org.springframework.boot.buildpack.platform.build.Cache 2>/dev/null | sed 's/org.springframework.boot.buildpack.platform.build.//g; s/org.springframework.boot.buildpack.platform.docker.type.//g' + for c in 'org.springframework.boot.maven.CacheInfo' 'org.springframework.boot.maven.LocalCacheInfo'; do + echo "--- javap $c (4.2.0-M2)" + javap -cp "$W/bp" "$c" 2>/dev/null | sed 's/org.springframework.boot.maven.//g; s/org.springframework.boot.buildpack.platform.build.//g' + done + echo "--- javap -p Image, fields mentioning cache/workspace (4.2.0-M2)" + javap -p -cp "$W/bp" org.springframework.boot.maven.Image 2>/dev/null | grep -E '(Cache|buildWorkspace)[a-zA-Z]* [a-zA-Z]+;$' | sed 's/org.springframework.boot.maven.//g' +} > output/04-buildpack-cache-classes.txt + +{ + echo "# 05-resttemplate-deprecations (javac -Xlint:deprecation on src/broken/UsesRestTemplate.java)" + for pom in pom-41.xml pom.xml; do + mvn -B -q -f $pom dependency:build-classpath -Dmdep.outputFile="$W/cp.txt" >/dev/null 2>&1 + label=$([ $pom = pom.xml ] && echo 4.2.0-M2 || echo 4.1.1) + echo "--- Spring Boot $label" + mkdir -p "$W/out-$label" + javac -Xlint:deprecation -d "$W/out-$label" -cp "$(cat "$W/cp.txt")" src/broken/UsesRestTemplate.java 2>&1 | grep -v JAVA_TOOL | sed 's#^src/broken/##' || true + done +} > output/05-resttemplate-deprecations.txt + +{ + echo "# 06-restclient-replacement (javac -Xlint:deprecation on the RestClient version, Spring Boot 4.2.0-M2)" + mvn -B -q dependency:build-classpath -Dmdep.outputFile="$W/cp.txt" >/dev/null 2>&1 + mkdir -p "$W/out-rc" + javac -Xlint:deprecation -Xlint:removal -d "$W/out-rc" -cp "$(cat "$W/cp.txt")" src/main/java/com/ankurm/boot42/RestClientReplacement.java 2>&1 | grep -v JAVA_TOOL | sed 's#^src/main/java/com/ankurm/boot42/##' || true + echo "javac finished; lines above this one are warnings (none means a clean compile)" +} > output/06-restclient-replacement.txt diff --git a/boot42-preview/scripts/run-all.sh b/boot42-preview/scripts/run-all.sh new file mode 100755 index 0000000..4a4d8a3 --- /dev/null +++ b/boot42-preview/scripts/run-all.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Regenerates every file under output/. Needs JDK 25 and Maven 3.9 with network access to Maven Central. +# ./scripts/run-all.sh +# 01-02 come from the test suite (run on 4.2.0-M2, then on 4.1.1 via pom-41.xml, which writes the *-on-4.1.1 files). +# 03-06 come from jars, POMs and javac. Port numbers are random, so the transcripts print "". +set -euo pipefail +cd "$(dirname "$0")/.." +mvn -B test +mvn -B -f pom-41.xml test +./scripts/capture-facts.sh +ls -1 output diff --git a/boot42-preview/src/broken/UsesRestTemplate.java b/boot42-preview/src/broken/UsesRestTemplate.java new file mode 100644 index 0000000..2caf6f5 --- /dev/null +++ b/boot42-preview/src/broken/UsesRestTemplate.java @@ -0,0 +1,19 @@ +import org.springframework.boot.restclient.RestTemplateBuilder; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.web.client.RestTemplate; + +/** Not part of the build: compiled by scripts/capture-facts.sh so the compiler's own words are committed. */ +class UsesRestTemplate { + + RestTemplate viaBuilder(RestTemplateBuilder builder) { + return builder.build(); + } + + TestRestTemplate viaTest(TestRestTemplate t) { + return t; + } + + RestTemplate plain() { + return new RestTemplate(); + } +} diff --git a/boot42-preview/src/main/java/com/ankurm/boot42/Boot42PreviewApplication.java b/boot42-preview/src/main/java/com/ankurm/boot42/Boot42PreviewApplication.java new file mode 100644 index 0000000..c7a3094 --- /dev/null +++ b/boot42-preview/src/main/java/com/ankurm/boot42/Boot42PreviewApplication.java @@ -0,0 +1,9 @@ +package com.ankurm.boot42; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** Minimal application. Every experiment in this module lives in src/test. */ +@SpringBootApplication +@org.springframework.boot.context.properties.EnableConfigurationProperties(OrderProperties.class) +public class Boot42PreviewApplication { +} diff --git a/boot42-preview/src/main/java/com/ankurm/boot42/OrderProperties.java b/boot42-preview/src/main/java/com/ankurm/boot42/OrderProperties.java new file mode 100644 index 0000000..7aadd24 --- /dev/null +++ b/boot42-preview/src/main/java/com/ankurm/boot42/OrderProperties.java @@ -0,0 +1,10 @@ +package com.ankurm.boot42; + +import java.util.Map; +import java.util.Set; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** A Set and a Map bound from a file where the keys are deliberately not alphabetical. */ +@ConfigurationProperties("app") +public record OrderProperties(Set regions, Map limits) { +} diff --git a/boot42-preview/src/main/java/com/ankurm/boot42/RestClientReplacement.java b/boot42-preview/src/main/java/com/ankurm/boot42/RestClientReplacement.java new file mode 100644 index 0000000..0ef0421 --- /dev/null +++ b/boot42-preview/src/main/java/com/ankurm/boot42/RestClientReplacement.java @@ -0,0 +1,15 @@ +package com.ankurm.boot42; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +/** What the deprecated RestTemplateBuilder code turns into: an injected RestClient.Builder. */ +@Configuration(proxyBeanMethods = false) +class RestClientReplacement { + + @Bean + RestClient inventoryClient(RestClient.Builder builder) { + return builder.baseUrl("http://inventory.internal").build(); + } +} diff --git a/boot42-preview/src/main/java/com/ankurm/boot42/WhoAmIController.java b/boot42-preview/src/main/java/com/ankurm/boot42/WhoAmIController.java new file mode 100644 index 0000000..0df63f4 --- /dev/null +++ b/boot42-preview/src/main/java/com/ankurm/boot42/WhoAmIController.java @@ -0,0 +1,23 @@ +package com.ankurm.boot42; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.LinkedHashMap; +import java.util.Map; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** Echoes what the servlet container believes about the request, after any forwarded-header handling. */ +@RestController +class WhoAmIController { + + @GetMapping("/whoami") + Map whoAmI(HttpServletRequest request) { + Map m = new LinkedHashMap<>(); + m.put("scheme", request.getScheme()); + m.put("secure", request.isSecure()); + m.put("serverName", request.getServerName()); + m.put("serverPort", request.getServerPort()); + m.put("remoteAddr", request.getRemoteAddr()); + return m; + } +} diff --git a/boot42-preview/src/main/resources/order.properties b/boot42-preview/src/main/resources/order.properties new file mode 100644 index 0000000..22fcc1f --- /dev/null +++ b/boot42-preview/src/main/resources/order.properties @@ -0,0 +1,7 @@ +app.regions=zulu,alpha,mike,bravo,yankee,charlie +app.limits.zulu=1 +app.limits.alpha=2 +app.limits.mike=3 +app.limits.bravo=4 +app.limits.yankee=5 +app.limits.charlie=6 diff --git a/boot42-preview/src/test/java/com/ankurm/boot42/BindingOrderTest.java b/boot42-preview/src/test/java/com/ankurm/boot42/BindingOrderTest.java new file mode 100644 index 0000000..d6ef6fb --- /dev/null +++ b/boot42-preview/src/test/java/com/ankurm/boot42/BindingOrderTest.java @@ -0,0 +1,26 @@ +package com.ankurm.boot42; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; + +/** 4.2.0-M1: Sets and Maps bind consistently and keep the order of the configuration file. */ +class BindingOrderTest { + + @Test + void recordsIterationOrder() { + Transcript t = new Transcript("02-set-and-map-binding-order"); + try (ConfigurableApplicationContext ctx = BootRun.start("spring.config.name=order")) { + OrderProperties p = ctx.getBean(OrderProperties.class); + t.line("file order : zulu, alpha, mike, bravo, yankee, charlie"); + t.line("Set class : " + p.regions().getClass().getName()); + t.line("Set order : " + new ArrayList<>(p.regions())); + t.line("Map class : " + p.limits().getClass().getName()); + t.line("Map key order : " + new ArrayList<>(p.limits().keySet())); + t.write(); + assertThat(p.regions()).hasSize(6); + } + } +} diff --git a/boot42-preview/src/test/java/com/ankurm/boot42/BootRun.java b/boot42-preview/src/test/java/com/ankurm/boot42/BootRun.java new file mode 100644 index 0000000..2293140 --- /dev/null +++ b/boot42-preview/src/test/java/com/ankurm/boot42/BootRun.java @@ -0,0 +1,32 @@ +package com.ankurm.boot42; + +import java.util.Map; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.web.client.RestClient; + +/** Starts the real application with the given properties on a random port. */ +final class BootRun { + + private BootRun() { + } + + static ConfigurableApplicationContext start(String... props) { + return new SpringApplicationBuilder(Boot42PreviewApplication.class) + .web(WebApplicationType.SERVLET) + .properties(props) + .properties("server.port=0", "spring.main.banner-mode=off", "logging.level.root=WARN") + .run(); + } + + static RestClient client(ConfigurableApplicationContext ctx) { + Environment env = ctx.getEnvironment(); + return RestClient.create("http://localhost:" + env.getProperty("local.server.port")); + } + + static Map whoAmI(ConfigurableApplicationContext ctx, Map headers) { + return client(ctx).get().uri("/whoami").headers(h -> headers.forEach(h::add)).retrieve().body(Map.class); + } +} diff --git a/boot42-preview/src/test/java/com/ankurm/boot42/ForwardedHeadersTest.java b/boot42-preview/src/test/java/com/ankurm/boot42/ForwardedHeadersTest.java new file mode 100644 index 0000000..b47431c --- /dev/null +++ b/boot42-preview/src/test/java/com/ankurm/boot42/ForwardedHeadersTest.java @@ -0,0 +1,67 @@ +package com.ankurm.boot42; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * 4.2.0-M1: "When server.forward-headers-strategy=FRAMEWORK is selected, X-Forwarded-* headers are used by + * default." Sends both header styles under each strategy and records what the container believes. + */ +class ForwardedHeadersTest { + + private static final Map X_FORWARDED = Map.of("X-Forwarded-Proto", "https", + "X-Forwarded-Host", "shop.example.com", "X-Forwarded-Port", "443"); + private static final Map RFC_7239 = Map.of("Forwarded", "proto=https;host=shop.example.com"); + + private static final Map> HEADERS = new LinkedHashMap<>(); + static { + HEADERS.put("x-forwarded", X_FORWARDED); + HEADERS.put("rfc7239", RFC_7239); + } + + @Test + void recordsBehaviourPerStrategy() { + Transcript t = new Transcript("01-forwarded-headers"); + Map> results = new LinkedHashMap<>(); + for (String strategy : new String[] { "none", "native", "framework" }) { + for (var e : HEADERS.entrySet()) { + try (ConfigurableApplicationContext ctx = BootRun.start("server.forward-headers-strategy=" + strategy)) { + Map seen = BootRun.whoAmI(ctx, e.getValue()); + String key = strategy + "/" + e.getKey(); + results.put(key, seen); + t.line(String.format("%-22s scheme=%s secure=%s host=%s port=%s", key, seen.get("scheme"), + seen.get("secure"), seen.get("serverName"), seen.get("serverPort").toString().equals("443") ? "443" : "")); + } + } + } + // the new 4.2 knob: which header family the FRAMEWORK strategy reads + for (var e : HEADERS.entrySet()) { + try (ConfigurableApplicationContext ctx = BootRun.start("server.forward-headers-strategy=framework", + "spring.mvc.forwarded-headers.header-format=standard")) { + Map seen = BootRun.whoAmI(ctx, e.getValue()); + t.line(String.format("%-22s scheme=%s secure=%s host=%s port=%s", "framework+standard/" + e.getKey(), + seen.get("scheme"), seen.get("secure"), seen.get("serverName"), seen.get("serverPort").toString().equals("443") ? "443" : "")); + } + } + // my first guess at the value was "forwarded"; record what the binder says about it + try (ConfigurableApplicationContext ctx = BootRun.start("server.forward-headers-strategy=framework", + "spring.mvc.forwarded-headers.header-format=forwarded")) { + t.line("header-format=forwarded -> started (property ignored)"); + } + catch (Exception ex) { + Throwable root = ex; + while (root.getCause() != null) { + root = root.getCause(); + } + t.line("header-format=forwarded -> " + root.getClass().getSimpleName() + ": " + root.getMessage()); + } + t.write(); + // a strategy of "none" must never trust either header + assertThat(results.get("none/x-forwarded").get("scheme")).isEqualTo("http"); + assertThat(results.get("none/rfc7239").get("scheme")).isEqualTo("http"); + } +} diff --git a/boot42-preview/src/test/java/com/ankurm/boot42/Transcript.java b/boot42-preview/src/test/java/com/ankurm/boot42/Transcript.java new file mode 100644 index 0000000..8e997f9 --- /dev/null +++ b/boot42-preview/src/test/java/com/ankurm/boot42/Transcript.java @@ -0,0 +1,36 @@ +package com.ankurm.boot42; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** Collects lines and writes output/NN-name.txt. The test asserts the same values it writes. */ +final class Transcript { + + private final List lines = new ArrayList<>(); + private final Path file; + + Transcript(String name) { + String v = System.getProperty("boot.label", "4.2.0-M2"); + String suffix = v.equals("4.2.0-M2") ? "" : "-on-" + v; + this.file = Path.of("output", name + suffix + ".txt"); + line("# " + name + " (Spring Boot " + v + ", JDK " + Runtime.version().feature() + ")"); + } + + Transcript line(String s) { + lines.add(s); + return this; + } + + void write() { + try { + Files.createDirectories(file.getParent()); + Files.write(file, lines); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } +}