Add boot42-preview module: Spring Boot 4.2.0-M2 vs 4.1.1 forwarded headers, AMQP starters, buildpack cache, RestTemplate removal warnings
Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Gth99spB1CzQmBFofXmU4Z
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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<String> regions, Map<String, Integer> limits) {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> whoAmI(HttpServletRequest request) {
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> headers) {
|
||||
return client(ctx).get().uri("/whoami").headers(h -> headers.forEach(h::add)).retrieve().body(Map.class);
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> X_FORWARDED = Map.of("X-Forwarded-Proto", "https",
|
||||
"X-Forwarded-Host", "shop.example.com", "X-Forwarded-Port", "443");
|
||||
private static final Map<String, String> RFC_7239 = Map.of("Forwarded", "proto=https;host=shop.example.com");
|
||||
|
||||
private static final Map<String, Map<String, String>> 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<String, 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" : "<random>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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" : "<random>"));
|
||||
}
|
||||
}
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user