Compare commits

...
3 Commits
62 changed files with 1509 additions and 0 deletions
+16
View File
@@ -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.
@@ -0,0 +1,15 @@
<!-- NOT EXECUTED: needs a Docker daemon and a registry. Element names come from the setters javap shows in
output/04-buildpack-cache-classes.txt (CacheInfo.setImage, ImageCacheInfo.setName). -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<buildCache>
<image>
<name>registry.example.com/team/demo-build-cache:latest</name>
</image>
</buildCache>
</image>
</configuration>
</plugin>
@@ -0,0 +1,10 @@
# 01-forwarded-headers (Spring Boot 4.1.1, JDK 25)
none/x-forwarded scheme=http secure=false host=localhost port=<random>
none/rfc7239 scheme=http secure=false host=localhost port=<random>
native/x-forwarded scheme=https secure=true host=shop.example.com port=443
native/rfc7239 scheme=http secure=false host=localhost port=<random>
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)
@@ -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=<random>
none/rfc7239 scheme=http secure=false host=localhost port=<random>
native/x-forwarded scheme=https secure=true host=shop.example.com port=443
native/rfc7239 scheme=http secure=false host=localhost port=<random>
framework/x-forwarded scheme=https secure=true host=shop.example.com port=443
framework/rfc7239 scheme=http secure=false host=localhost port=<random>
framework+standard/x-forwarded scheme=http secure=false host=localhost port=<random>
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
@@ -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]
@@ -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]
@@ -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
@@ -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;
@@ -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
@@ -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)
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
<artifactId>boot42-preview-on-41</artifactId>
<version>1.0.0</version>
<name>boot42-preview</name>
<description>Spring Boot 4.2 milestone preview: what to test now, run against 4.2.0-M2 and 4.1.1</description>
<properties>
<java.version>25</java.version>
<boot.label>4.1.1</boot.label>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<boot.label>${boot.label}</boot.label>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.2.0-M2</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
<artifactId>boot42-preview</artifactId>
<version>1.0.0</version>
<name>boot42-preview</name>
<description>Spring Boot 4.2 milestone preview: what to test now, run against 4.2.0-M2 and 4.1.1</description>
<properties>
<java.version>25</java.version>
<boot.label>4.2.0-M2</boot.label>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<boot.label>${boot.label}</boot.label>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
+78
View File
@@ -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 '<description>' "$W/p.xml" | sed 's/ *<[^>]*>//g')
dep=$(grep -E '<artifactId>spring-boot-(amqp|rabbitmq|amqp-rabbitmq)</artifactId>' "$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
+11
View File
@@ -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 "<random>".
set -euo pipefail
cd "$(dirname "$0")/.."
mvn -B test
mvn -B -f pom-41.xml test
./scripts/capture-facts.sh
ls -1 output
@@ -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);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
# jakarta-ee-11
Companion module for the ankurm.com post *Jakarta EE 11 for Spring Developers: What Actually Matters*.
Reads the Jakarta EE 10 and 11 API jars from Maven Central with `javap`, reads Spring Boot 4.1.1's own BOM and configuration metadata, and runs one tiny Boot application to ask the embedded Tomcat which Servlet level it implements. No claim in the post comes from prose.
| Component | Version |
|---|---|
| Spring Boot | 4.1.1 |
| JDK | 25 |
| Maven | 3.9 |
## Run
```bash
./scripts/run-all.sh # regenerates everything under output/
```
## Output
| File | Shows |
|---|---|
| `output/01-boot-managed-jakarta-apis.txt` | Jakarta versions Spring Boot 4.1.1 manages, and four artifacts it does not |
| `output/02-api-diff.txt` | Class and member counts, EE 10 vs EE 11, for Servlet, Persistence, Validation, Annotations, Concurrency |
| `output/03-entitymanager-3.2.txt` | New `EntityManager` methods in Persistence 3.2 |
| `output/04-data-and-concurrency.txt` | Jakarta Data 1.0.2 repository types; `@ManagedExecutorDefinition` in 3.0.0 vs 3.1.1 |
| `output/05-servlet-level.txt` | The running application's Servlet level (written by `ServletLevelTest`) |
| `output/06-spring-data-and-jakarta-data.txt` | Spring Data 4.1.1 jars contain no `jakarta.data` types |
| `output/07-virtual-threads-property.txt` | Boot's `spring.threads.virtual.enabled` property, from shipped metadata |
@@ -0,0 +1,32 @@
# 01-boot-managed-jakarta-apis (spring-boot-dependencies 4.1.1)
hibernate-validator.version = 9.1.3.Final
hibernate.version = 7.4.5.Final
jakarta-activation.version = 2.1.4
jakarta-annotation.version = 3.0.0
jakarta-inject.version = 2.0.1
jakarta-jms.version = 3.1.0
jakarta-json-bind.version = 3.0.2
jakarta-json.version = 2.1.3
jakarta-mail.version = 2.1.5
jakarta-management.version = 1.1.4
jakarta-persistence.version = 3.2.0
jakarta-servlet-jsp-jstl.version = 3.0.2
jakarta-servlet.version = 6.1.0
jakarta-transaction.version = 2.0.1
jakarta-validation.version = 3.1.1
jakarta-websocket.version = 2.2.0
jakarta-ws-rs.version = 4.0.0
jakarta-xml-bind.version = 4.0.5
jakarta-xml-soap.version = 3.0.2
jakarta-xml-ws.version = 4.0.3
jetty.version = 12.1.12
spring-data-bom.version = 2026.0.1
spring-framework.version = 7.0.9
tomcat.version = 11.0.24
--- artifacts NOT in the BOM (0 = Spring Boot does not manage them)
jakarta.data-api 0
jakarta.enterprise.concurrent-api 0
jakarta.enterprise.cdi-api 0
jakarta.security.enterprise-api 0
jakarta.faces-api 0
jakarta.ws.rs-api 1
+45
View File
@@ -0,0 +1,45 @@
# 02-api-diff (javap -public over every class of the two jars; members are signature strings, so a changed signature counts as one removed and one added)
--- Servlet: jakarta.servlet-api 6.0.0 -> 6.1.0
classes added : 1 removed: 1
members added : 26 removed: 3
+ class jakarta.servlet.http.HttpSession$Accessor
- class jakarta.servlet.http.Cookie$1
--- Persistence: jakarta.persistence-api 3.1.0 -> 3.2.0
classes added : 24 removed: 0
members added : 251 removed: 39
+ class jakarta.persistence.CheckConstraint
+ class jakarta.persistence.ConnectionConsumer
+ class jakarta.persistence.ConnectionFunction
+ class jakarta.persistence.EnumeratedValue
+ class jakarta.persistence.FindOption
+ class jakarta.persistence.Graph
+ class jakarta.persistence.LockOption
+ class jakarta.persistence.PersistenceConfiguration
+ class jakarta.persistence.PersistenceUnitTransactionType
+ class jakarta.persistence.RefreshOption
+ class jakarta.persistence.SchemaManager
+ class jakarta.persistence.SchemaValidationException
--- Validation: jakarta.validation-api 3.0.0 -> 3.1.0
classes added : 9 removed: 0
members added : 1 removed: 1
+ class jakarta.validation.bootstrap.package-info
+ class jakarta.validation.constraints.package-info
+ class jakarta.validation.constraintvalidation.package-info
+ class jakarta.validation.executable.package-info
+ class jakarta.validation.groups.package-info
+ class jakarta.validation.metadata.package-info
+ class jakarta.validation.package-info
+ class jakarta.validation.spi.package-info
+ class jakarta.validation.valueextraction.package-info
--- Annotations: jakarta.annotation-api 2.1.1 -> 3.0.0
classes added : 2 removed: 1
members added : 0 removed: 1
+ class jakarta.annotation.package-info
+ class jakarta.annotation.security.package-info
- class jakarta.annotation.ManagedBean
--- Concurrency: jakarta.enterprise.concurrent-api 3.0.0 -> 3.1.1
classes added : 3 removed: 0
members added : 15 removed: 0
+ class jakarta.enterprise.concurrent.Schedule
+ class jakarta.enterprise.concurrent.package-info
+ class jakarta.enterprise.concurrent.spi.package-info
@@ -0,0 +1,18 @@
# 03-entitymanager-3.2 (methods on jakarta.persistence.EntityManager that 3.1.0 did not have)
public abstract <C, T> T callWithConnection(ConnectionFunction<C, T>);
public abstract <C> void runWithConnection(ConnectionConsumer<C>);
public abstract <T> Query createNativeQuery(String, Class<T>);
public abstract <T> T find(Class<T>, Object, FindOption...);
public abstract <T> T find(EntityGraph<T>, Object, FindOption...);
public abstract <T> T getReference(T);
public abstract <T> TypedQuery<T> createQuery(TypedQueryReference<T>);
public abstract <T> TypedQuery<T> createQuery(criteria.CriteriaSelect<T>);
public abstract CacheRetrieveMode getCacheRetrieveMode();
public abstract CacheStoreMode getCacheStoreMode();
public abstract Query createQuery(criteria.CriteriaDelete<?>);
public abstract Query createQuery(criteria.CriteriaUpdate<?>);
public abstract StoredProcedureQuery createStoredProcedureQuery(String, Class<?>...);
public abstract void lock(Object, LockModeType, LockOption...);
public abstract void refresh(Object, RefreshOption...);
public abstract void setCacheRetrieveMode(CacheRetrieveMode);
public abstract void setCacheStoreMode(CacheStoreMode);
@@ -0,0 +1,27 @@
# 04-data-and-concurrency
--- jakarta.data-api 1.0.2: types in jakarta.data.repository
BasicRepository
By
CrudRepository
DataRepository
Delete
Find
Insert
OrderBy
Param
Query
Repository
Save
Update
--- jakarta.enterprise.concurrent-api 3.0.0: attributes of @ManagedExecutorDefinition
String name();
String context();
long hungTaskThreshold();
int maxAsync();
--- jakarta.enterprise.concurrent-api 3.1.1: attributes of @ManagedExecutorDefinition
String name();
Class<?>[] qualifiers();
String context();
long hungTaskThreshold();
int maxAsync();
boolean virtual();
@@ -0,0 +1,2 @@
# 05-servlet-level (Spring Boot 4.1.1, embedded Tomcat, JDK 25)
GET /servlet-level -> 6.1 (container: Apache Tomcat/11.0.24)
@@ -0,0 +1,3 @@
# 06-spring-data-and-jakarta-data (classes mentioning jakarta/data inside Spring Data jars from Spring Data 2026.0.1)
spring-data-commons 4.1.1: entries under jakarta/data/: 0; manifest lines mentioning jakarta.data: 0
spring-data-jpa 4.1.1: entries under jakarta/data/: 0; manifest lines mentioning jakarta.data: 0
@@ -0,0 +1,2 @@
# 07-virtual-threads-property (spring-boot-autoconfigure 4.1.1 configuration metadata)
spring.threads.virtual.enabled (java.lang.Boolean, default False): Whether to use virtual threads.
+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
<artifactId>jakarta-ee-11</artifactId>
<version>1.0.0</version>
<name>jakarta-ee-11</name>
<description>Which Jakarta EE 11 specifications a Spring Boot 4.1 application actually uses</description>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Facts about the Jakarta EE 11 API jars, read from Maven Central artifacts and Spring Boot's own BOM. No running application.
set -uo pipefail
cd "$(dirname "$0")/.."
R="$HOME/.m2/repository"; W=$(mktemp -d); CEN=https://repo1.maven.org/maven2
get() { mvn -B -q dependency:get -Dartifact="$1" >/dev/null 2>&1; }
jar() { echo "$R/$(echo "$1" | cut -d: -f1 | tr . /)/$2/$3/$2-$3.jar"; }
# --- 01: what Spring Boot 4.1.1 manages -------------------------------------------------------------------
for i in 1 2 3 4 5; do code=$(curl -s -o "$W/bom.pom" -w '%{http_code}' $CEN/org/springframework/boot/spring-boot-dependencies/4.1.1/spring-boot-dependencies-4.1.1.pom); [ "$code" = 200 ] && break; sleep $((i*3)); done
{
echo "# 01-boot-managed-jakarta-apis (spring-boot-dependencies 4.1.1)"
grep -E '<(jakarta-[a-z-]+|tomcat|jetty|hibernate|hibernate-validator|spring-data-bom|spring-framework)\.version>' "$W/bom.pom" | sed 's/^ *//; s/<\([^>]*\)>\(.*\)<\/.*/\1 = \2/' | sort
echo "--- artifacts NOT in the BOM (0 = Spring Boot does not manage them)"
for a in jakarta.data-api jakarta.enterprise.concurrent-api jakarta.enterprise.cdi-api jakarta.security.enterprise-api jakarta.faces-api jakarta.ws.rs-api; do
printf ' %-36s %s\n' "$a" "$(grep -c "<artifactId>$a</artifactId>" "$W/bom.pom")"
done
} > output/01-boot-managed-jakarta-apis.txt
# --- 02: API differences between the EE 10 and EE 11 versions of four specs ---------------------------------
diffspec() { # name group artifact old new
get "$2:$3:$4"; get "$2:$3:$5"
for v in $4 $5; do
j=$(jar "$2" "$3" "$v"); rm -rf "$W/x-$v"; mkdir -p "$W/x-$v"; unzip -q -o "$j" '*.class' -d "$W/x-$v" 2>/dev/null
( cd "$W/x-$v" && find . -name '*.class' ! -name 'module-info.class' | sed 's#^\./##; s#\.class$##; s#/#.#g' | sort > "$W/cls-$v.txt"
javap -public -cp . $(cat "$W/cls-$v.txt" | grep -v 'package-info') 2>/dev/null | grep -E '^\s+(public|protected)|^(public|protected)' | sed 's/^ *//' | sort -u > "$W/mem-$v.txt" )
done
echo "--- $1: $3 $4 -> $5"
echo " classes added : $(comm -13 "$W/cls-$4.txt" "$W/cls-$5.txt" | wc -l) removed: $(comm -23 "$W/cls-$4.txt" "$W/cls-$5.txt" | wc -l)"
echo " members added : $(comm -13 "$W/mem-$4.txt" "$W/mem-$5.txt" | wc -l) removed: $(comm -23 "$W/mem-$4.txt" "$W/mem-$5.txt" | wc -l)"
comm -13 "$W/cls-$4.txt" "$W/cls-$5.txt" | sed 's/^/ + class /' | head -12
comm -23 "$W/cls-$4.txt" "$W/cls-$5.txt" | sed 's/^/ - class /' | head -12
}
{
echo "# 02-api-diff (javap -public over every class of the two jars; members are signature strings, so a changed signature counts as one removed and one added)"
diffspec Servlet jakarta.servlet jakarta.servlet-api 6.0.0 6.1.0
diffspec Persistence jakarta.persistence jakarta.persistence-api 3.1.0 3.2.0
diffspec Validation jakarta.validation jakarta.validation-api 3.0.0 3.1.0
diffspec Annotations jakarta.annotation jakarta.annotation-api 2.1.1 3.0.0
diffspec Concurrency jakarta.enterprise.concurrent jakarta.enterprise.concurrent-api 3.0.0 3.1.1
} > output/02-api-diff.txt
# --- 03: the new Persistence 3.2 EntityManager methods, by name --------------------------------------------
{
echo "# 03-entitymanager-3.2 (methods on jakarta.persistence.EntityManager that 3.1.0 did not have)"
for v in 3.1.0 3.2.0; do javap -public -cp "$(jar jakarta.persistence jakarta.persistence-api $v)" jakarta.persistence.EntityManager 2>/dev/null | grep '(' | sed 's/^ *//; s/jakarta.persistence.//g; s/java.lang.//g; s/java.util.//g' | sort > "$W/em-$v.txt"; done
comm -13 "$W/em-3.1.0.txt" "$W/em-3.2.0.txt"
} > output/03-entitymanager-3.2.txt
# --- 04: Jakarta Data 1.0 API surface and the virtual-thread switch in Concurrency 3.1 ---------------------
get jakarta.data:jakarta.data-api:1.0.2
{
echo "# 04-data-and-concurrency"
echo "--- jakarta.data-api 1.0.2: types in jakarta.data.repository"
unzip -l "$(jar jakarta.data jakarta.data-api 1.0.2)" | awk '{print $4}' | grep -E '^jakarta/data/repository/[A-Z][A-Za-z]*\.class$' | sed 's#jakarta/data/repository/##; s#\.class##' | sort | sed 's/^/ /'
for v in 3.0.0 3.1.1; do
echo "--- jakarta.enterprise.concurrent-api $v: attributes of @ManagedExecutorDefinition"
javap -public -cp "$(jar jakarta.enterprise.concurrent jakarta.enterprise.concurrent-api $v)" jakarta.enterprise.concurrent.ManagedExecutorDefinition 2>/dev/null | grep '()' | sed 's/^ *//; s/public abstract //; s/java.lang.//g' | sed 's/^/ /'
done
} > output/04-data-and-concurrency.txt
# --- 06: does the Spring Data version Boot manages contain any Jakarta Data code? ---------------------------
{
echo "# 06-spring-data-and-jakarta-data (classes mentioning jakarta/data inside Spring Data jars from Spring Data 2026.0.1)"
for a in spring-data-commons spring-data-jpa; do
v=$(curl -s $CEN/org/springframework/data/$a/maven-metadata.xml | grep -o '<version>4\.[0-9.]*</version>' | tail -1 | sed 's/<[^>]*>//g')
get org.springframework.data:$a:$v
j="$R/org/springframework/data/$a/$v/$a-$v.jar"
n=$(unzip -l "$j" 2>/dev/null | grep -c 'jakarta/data/')
m=$(unzip -p "$j" 2>/dev/null META-INF/MANIFEST.MF | grep -c 'jakarta.data')
echo " $a $v: entries under jakarta/data/: $n; manifest lines mentioning jakarta.data: $m"
done
} > output/06-spring-data-and-jakarta-data.txt
# --- 07: Spring Boot's own switch for virtual threads (read from the shipped configuration metadata) ----------
get org.springframework.boot:spring-boot-autoconfigure:4.1.1
{
echo "# 07-virtual-threads-property (spring-boot-autoconfigure 4.1.1 configuration metadata)"
unzip -p "$(jar org.springframework.boot spring-boot-autoconfigure 4.1.1)" META-INF/spring-configuration-metadata.json \
| python3 -c "import json,sys; [print(p['name'], '(' + p.get('type','?') + ', default ' + str(p.get('defaultValue','none')) + '):', p.get('description','')) for p in json.load(sys.stdin)['properties'] if p['name']=='spring.threads.virtual.enabled']"
} > output/07-virtual-threads-property.txt
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Regenerates every file under output/. Needs JDK 25, Maven 3.9 and network access to Maven Central.
set -euo pipefail
cd "$(dirname "$0")/.."
mvn -B test # 05-servlet-level.txt
./scripts/capture-facts.sh # 01-04, 06, 07
ls -1 output
@@ -0,0 +1,7 @@
package com.ankurm.jee11;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Jee11Application {
}
@@ -0,0 +1,16 @@
package com.ankurm.jee11;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/** Asks the servlet container which Servlet specification level it implements. */
@RestController
class ServletLevelController {
@GetMapping("/servlet-level")
String level(HttpServletRequest request) {
var ctx = request.getServletContext();
return ctx.getMajorVersion() + "." + ctx.getMinorVersion() + " (container: " + ctx.getServerInfo() + ")";
}
}
@@ -0,0 +1,29 @@
package com.ankurm.jee11;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.web.client.RestClient;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ServletLevelTest {
@LocalServerPort
int port;
@Test
void containerReportsServlet61() throws IOException {
String body = RestClient.create("http://localhost:" + port).get().uri("/servlet-level").retrieve().body(String.class);
Files.createDirectories(Path.of("output"));
Files.write(Path.of("output/05-servlet-level.txt"), List.of(
"# 05-servlet-level (Spring Boot 4.1.1, embedded Tomcat, JDK " + Runtime.version().feature() + ")",
"GET /servlet-level -> " + body));
assertThat(body).startsWith("6.1");
}
}
+27
View File
@@ -0,0 +1,27 @@
# openrewrite
Companion module for the ankurm.com post *Automating javax → jakarta and Boot 3 → 4 Upgrades with OpenRewrite*.
`legacy-app/` is a small Spring Boot 2.7.18 application (JPA, validation, security, a servlet filter, `RestTemplate`, a JUnit 4 test, a `spring.factories` auto-configuration). `scripts/run-all.sh` copies it to a scratch directory outside this checkout, runs two OpenRewrite recipes in sequence, builds and tests after each, and records everything under `output/`.
| Tool | Version |
|---|---|
| rewrite-maven-plugin | 6.46.1 |
| rewrite-spring | 6.37.1 |
| rewrite-migrate-java | 3.42.1 |
| JDK / Maven | 17 / 3.9 |
```bash
export JAVA_HOME=/path/to/jdk17
./scripts/run-all.sh
```
| Output | Shows |
|---|---|
| `00-gotcha-gitignore.txt` | OpenRewrite silently skips sources that git ignores |
| `01-baseline.txt` | Legacy app builds and passes on Boot 2.7.18 |
| `02-step1-diff.txt` / `03-step1-build.txt` | `UpgradeSpringBoot_3_5`: diff and build |
| `04-step2-diff.txt` / `05-step2-build.txt` | `UpgradeSpringBoot_4_0`: diff and build |
| `06-what-it-left-behind.txt` | What the recipes did not change |
| `07-step3-boot-4.1.1.txt` | Hand bump from 4.0.x to 4.1.1, build and test |
| `08-ci-gate.txt` | `rewrite:dryRun` exit codes, including a property-name trap |
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
<artifactId>legacy-app</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
</dependencies>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,7 @@
package com.ankurm.legacy;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AuditAutoConfiguration {
}
@@ -0,0 +1,26 @@
package com.ankurm.legacy;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Entity
public class Customer {
@Id
@GeneratedValue
private Long id;
@NotBlank
private String name;
@Email
private String email;
protected Customer() {}
public Customer(String name, String email) { this.name = name; this.email = email; }
public Long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
public void setName(String name) { this.name = name; }
public void setEmail(String email) { this.email = email; }
}
@@ -0,0 +1,28 @@
package com.ankurm.legacy;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/customers")
public class CustomerController {
private final CustomerRepository repository;
public CustomerController(CustomerRepository repository) { this.repository = repository; }
@PostConstruct
void seed() {
repository.save(new Customer("Ada", "[email protected]"));
}
@GetMapping
List<Customer> all() { return repository.findAll(); }
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
Customer create(@Valid @RequestBody Customer c) { return repository.save(c); }
}
@@ -0,0 +1,14 @@
package com.ankurm.legacy;
import javax.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.QueryHints;
import javax.persistence.QueryHint;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints(@QueryHint(name = "javax.persistence.lock.timeout", value = "3000"))
Customer findFirstByName(String name);
}
@@ -0,0 +1,11 @@
package com.ankurm.legacy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LegacyApplication {
public static void main(String[] args) {
SpringApplication.run(LegacyApplication.class, args);
}
}
@@ -0,0 +1,13 @@
package com.ankurm.legacy;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class RemoteClient {
private final RestTemplate rest = new RestTemplate();
public String fetch(String url) {
return rest.getForObject(url, String.class);
}
}
@@ -0,0 +1,19 @@
package com.ankurm.legacy;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class RequestLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
response.setHeader("X-Seen-By", "RequestLogFilter");
chain.doFilter(request, response);
}
}
@@ -0,0 +1,17 @@
package com.ankurm.legacy;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().antMatchers("/actuator/health").permitAll()
.anyRequest().permitAll();
}
}
@@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ankurm.legacy.AuditAutoConfiguration
@@ -0,0 +1,6 @@
spring.redis.host=localhost
spring.redis.port=6379
management.metrics.export.prometheus.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop
server.max-http-header-size=16KB
spring.jpa.properties.javax.persistence.validation.mode=none
@@ -0,0 +1,26 @@
package com.ankurm.legacy;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CustomerControllerTest {
@Autowired MockMvc mvc;
@Test
public void listsSeededCustomer() throws Exception {
mvc.perform(get("/customers")).andExpect(status().isOk()).andExpect(jsonPath("$[0].name").value("Ada"));
}
}
@@ -0,0 +1,3 @@
# 00-gotcha-gitignore: same recipe, same project, sources under a .gitignore'd directory
[INFO] Project [legacy-app] Parsing source files
[INFO] Applying recipes would make no changes. No patch file generated.
+3
View File
@@ -0,0 +1,3 @@
# 01-baseline: legacy app, Spring Boot 2.7.18, JDK 17
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
+173
View File
@@ -0,0 +1,173 @@
# 02-step1-diff: recipe org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5 on the legacy app (52 s)
diff -ru -x target legacy-app/pom.xml step1/pom.xml
--- legacy-app/pom.xml
+++ step1/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
- <version>2.7.18</version>
+ <version>3.5.16</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
@@ -14,6 +14,10 @@
<java.version>17</java.version>
</properties>
<dependencies>
+ <dependency>
+ <groupId>jakarta.servlet</groupId>
+ <artifactId>jakarta.servlet-api</artifactId>
+ </dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
@@ -21,7 +25,6 @@
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
- <dependency><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId><scope>test</scope></dependency>
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
</dependencies>
<build>
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java step1/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java
--- legacy-app/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java
+++ step1/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java
@@ -1,7 +1,7 @@
package com.ankurm.legacy;
-import org.springframework.context.annotation.Configuration;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
-@Configuration
+@AutoConfiguration
public class AuditAutoConfiguration {
}
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/Customer.java step1/src/main/java/com/ankurm/legacy/Customer.java
--- legacy-app/src/main/java/com/ankurm/legacy/Customer.java
+++ step1/src/main/java/com/ankurm/legacy/Customer.java
@@ -1,10 +1,10 @@
package com.ankurm.legacy;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.Id;
-import javax.validation.constraints.Email;
-import javax.validation.constraints.NotBlank;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
@Entity
public class Customer {
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/CustomerController.java step1/src/main/java/com/ankurm/legacy/CustomerController.java
--- legacy-app/src/main/java/com/ankurm/legacy/CustomerController.java
+++ step1/src/main/java/com/ankurm/legacy/CustomerController.java
@@ -1,8 +1,8 @@
package com.ankurm.legacy;
import java.util.List;
-import javax.annotation.PostConstruct;
-import javax.validation.Valid;
+import jakarta.annotation.PostConstruct;
+import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/CustomerRepository.java step1/src/main/java/com/ankurm/legacy/CustomerRepository.java
--- legacy-app/src/main/java/com/ankurm/legacy/CustomerRepository.java
+++ step1/src/main/java/com/ankurm/legacy/CustomerRepository.java
@@ -1,10 +1,10 @@
package com.ankurm.legacy;
-import javax.persistence.LockModeType;
+import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.QueryHints;
-import javax.persistence.QueryHint;
+import jakarta.persistence.QueryHint;
public interface CustomerRepository extends JpaRepository<Customer, Long> {
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/RequestLogFilter.java step1/src/main/java/com/ankurm/legacy/RequestLogFilter.java
--- legacy-app/src/main/java/com/ankurm/legacy/RequestLogFilter.java
+++ step1/src/main/java/com/ankurm/legacy/RequestLogFilter.java
@@ -1,10 +1,10 @@
package com.ankurm.legacy;
import java.io.IOException;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/SecurityConfig.java step1/src/main/java/com/ankurm/legacy/SecurityConfig.java
--- legacy-app/src/main/java/com/ankurm/legacy/SecurityConfig.java
+++ step1/src/main/java/com/ankurm/legacy/SecurityConfig.java
@@ -1,17 +1,19 @@
package com.ankurm.legacy;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
-public class SecurityConfig extends WebSecurityConfigurerAdapter {
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.csrf().disable()
- .authorizeRequests().antMatchers("/actuator/health").permitAll()
- .anyRequest().permitAll();
+public class SecurityConfig {
+ @Bean
+ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+ http.csrf(csrf -> csrf.disable())
+ .authorizeHttpRequests(requests -> requests.requestMatchers("/actuator/health").permitAll()
+ .anyRequest().permitAll());
+ return http.build();
}
}
Only in step1/src/main/resources/META-INF: spring
diff -ru -x target legacy-app/src/main/resources/application.properties step1/src/main/resources/application.properties
--- legacy-app/src/main/resources/application.properties
+++ step1/src/main/resources/application.properties
@@ -3,4 +3,4 @@
management.metrics.export.prometheus.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop
server.max-http-header-size=16KB
-spring.jpa.properties.javax.persistence.validation.mode=none
+spring.jpa.properties.jakarta.persistence.validation.mode=none
diff -ru -x target legacy-app/src/test/java/com/ankurm/legacy/CustomerControllerTest.java step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
--- legacy-app/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
+++ step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
@@ -4,15 +4,12 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-import org.junit.Test;
-import org.junit.runner.RunWith;
+import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
-@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class CustomerControllerTest {
Only in step1/src/main/resources/META-INF: spring
+3
View File
@@ -0,0 +1,3 @@
# 03-step1-build: after step 1, JDK 17
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
+65
View File
@@ -0,0 +1,65 @@
# 04-step2-diff: recipe org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0 applied on top of step 1 (32 s)
diff -ru -x target step1/pom.xml step2/pom.xml
--- step1/pom.xml
+++ step2/pom.xml
@@ -4,7 +4,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
- <version>3.5.16</version>
+ <version>4.0.8</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
@@ -18,14 +18,23 @@
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
</dependency>
- <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
+ <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-restclient</artifactId>
+ </dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
- <dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-webmvc-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security-test</artifactId><scope>test</scope></dependency>
</dependencies>
<build>
<plugins>
diff -ru -x target step1/src/main/resources/application.properties step2/src/main/resources/application.properties
--- step1/src/main/resources/application.properties
+++ step2/src/main/resources/application.properties
@@ -1,6 +1,6 @@
-spring.redis.host=localhost
-spring.redis.port=6379
-management.metrics.export.prometheus.enabled=true
+spring.data.redis.host=localhost
+spring.data.redis.port=6379
+management.prometheus.metrics.export.enabled=true
spring.jpa.hibernate.ddl-auto=create-drop
-server.max-http-header-size=16KB
+server.max-http-request-header-size=16KB
spring.jpa.properties.jakarta.persistence.validation.mode=none
diff -ru -x target step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java step2/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
--- step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
+++ step2/src/test/java/com/ankurm/legacy/CustomerControllerTest.java
@@ -6,7 +6,7 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
+3
View File
@@ -0,0 +1,3 @@
# 05-step2-build: after step 2, JDK 17
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
@@ -0,0 +1,13 @@
# 06-what-it-left-behind (in /tmp/or-work/step2, after both recipes)
--- leftover 'javax.' string literals or keys in source and resources:
src/main/java/com/ankurm/legacy/CustomerRepository.java:12: @QueryHints(@QueryHint(name = "javax.persistence.lock.timeout", value = "3000"))
--- META-INF files:
./spring.factories
./spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
--- explicit jakarta.servlet-api dependency added to the pom:
1
--- RestTemplate usages (recipe does not touch them):
src/main/java/com/ankurm/legacy/RemoteClient.java:4:import org.springframework.web.client.RestTemplate;
src/main/java/com/ankurm/legacy/RemoteClient.java:8: private final RestTemplate rest = new RestTemplate();
--- Spring Boot version the recipe stopped at:
<version>4.0.8</version>
@@ -0,0 +1,4 @@
# 07-step3-boot-4.1.1: parent bumped by hand from the recipe's 4.0.x to 4.1.1, JDK 17
<version>4.1.1</version>
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
+4
View File
@@ -0,0 +1,4 @@
# 08-ci-gate (mvn rewrite:dryRun, exit code decides the CI job)
legacy app, -Drewrite.failOnDryRunResults=true (the name you would guess): exit code 0
legacy app, -DfailOnDryRunResults=true (the real user property): exit code 1
migrated app, -DfailOnDryRunResults=true: exit code 0
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# usage: rewrite.sh <from-dir> <to-dir> <recipe-fqn> (copies from-dir to to-dir and runs the recipe there)
set -uo pipefail
RW=6.46.1; SPRING=6.37.1; MIGRATE=3.42.1
rm -rf "$2"; mkdir -p "$(dirname "$2")"; cp -r "$1" "$2"; rm -rf "$2/target"
cd "$2"
mvn -B -q org.openrewrite.maven:rewrite-maven-plugin:$RW:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:$SPRING,org.openrewrite.recipe:rewrite-migrate-java:$MIGRATE \
-Drewrite.activeRecipes="$3" -Drewrite.exportDatatables=false 2>&1
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Regenerates output/. Needs JDK 17 on JAVA_HOME (the legacy app targets 17), Maven 3.9, network access to Maven Central.
# Work happens in $WORK (default /tmp/or-work) OUTSIDE this git checkout, because OpenRewrite honours .gitignore
# and ignores files git ignores (see output/00-gotcha-gitignore.txt).
set -uo pipefail
cd "$(dirname "$0")/.."
WORK=${WORK:-/tmp/or-work}; OUT=$PWD/output; mkdir -p "$OUT"
R1=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5
R2=org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0
tests() { (cd "$1" && mvn -B test 2>&1 | grep -E "Tests run:.*Fail|BUILD|ERROR" | grep -v "Time elapsed" | head -${2:-8}); }
difff() { diff -ru -x target "$1" "$2" | sed -E 's/\t20[0-9-]+ [0-9:.]+ \+0000$//; s#'"$WORK"'/##g'; }
{ echo "# 01-baseline: legacy app, Spring Boot 2.7.18, JDK 17"; tests legacy-app; } > "$OUT/01-baseline.txt"
s=$(date +%s); ./scripts/rewrite.sh legacy-app "$WORK/step1" $R1 >/dev/null 2>&1; e=$(date +%s)
{ echo "# 02-step1-diff: recipe $R1 on the legacy app ($((e-s)) s)"; difff legacy-app "$WORK/step1"; diff -rq legacy-app "$WORK/step1" -x target | grep '^Only in' | sed "s#$WORK/##"; } > "$OUT/02-step1-diff.txt"
{ echo "# 03-step1-build: after step 1, JDK 17"; tests "$WORK/step1"; } > "$OUT/03-step1-build.txt"
s=$(date +%s); ./scripts/rewrite.sh "$WORK/step1" "$WORK/step2" $R2 >/dev/null 2>&1; e=$(date +%s)
{ echo "# 04-step2-diff: recipe $R2 applied on top of step 1 ($((e-s)) s)"; difff "$WORK/step1" "$WORK/step2"; } > "$OUT/04-step2-diff.txt"
{ echo "# 05-step2-build: after step 2, JDK 17"; tests "$WORK/step2"; } > "$OUT/05-step2-build.txt"
{
echo "# 06-what-it-left-behind (in $WORK/step2, after both recipes)"
echo "--- leftover 'javax.' string literals or keys in source and resources:"
grep -rn 'javax\.' "$WORK/step2/src" | sed "s#$WORK/step2/##" || true
echo "--- META-INF files:"; (cd "$WORK/step2/src/main/resources/META-INF" && find . -type f | sort)
echo "--- explicit jakarta.servlet-api dependency added to the pom:"; grep -c '<artifactId>jakarta.servlet-api</artifactId>' "$WORK/step2/pom.xml"
echo "--- RestTemplate usages (recipe does not touch them):"; grep -rn 'RestTemplate' "$WORK/step2/src" | sed "s#$WORK/step2/##"
echo "--- Spring Boot version the recipe stopped at:"; grep -A1 '<artifactId>spring-boot-starter-parent' "$WORK/step2/pom.xml" | grep version
} > "$OUT/06-what-it-left-behind.txt"
rm -rf "$WORK/step3"; cp -r "$WORK/step2" "$WORK/step3"; rm -rf "$WORK/step3/target"
sed -i 's#<version>4.0.[0-9]*</version>#<version>4.1.1</version>#' "$WORK/step3/pom.xml"
{ echo "# 07-step3-boot-4.1.1: parent bumped by hand from the recipe's 4.0.x to 4.1.1, JDK 17"; grep -A1 '<artifactId>spring-boot-starter-parent' "$WORK/step3/pom.xml" | grep version; tests "$WORK/step3"; } > "$OUT/07-step3-boot-4.1.1.txt"
# CI gate: dryRun with failOnDryRunResults. Fails on the legacy app, passes once migrated.
gate() { (cd "$1" && rm -rf target && mvn -B -q org.openrewrite.maven:rewrite-maven-plugin:6.46.1:dryRun \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:6.37.1,org.openrewrite.recipe:rewrite-migrate-java:3.42.1 \
-Drewrite.activeRecipes="$2" -D"$3"=true >/dev/null 2>&1; echo "exit code $?"); }
rm -rf "$WORK/g1" "$WORK/g2"; cp -r legacy-app "$WORK/g1"; cp -r "$WORK/step2" "$WORK/g2"
{ echo "# 08-ci-gate (mvn rewrite:dryRun, exit code decides the CI job)"
echo "legacy app, -Drewrite.failOnDryRunResults=true (the name you would guess): $(gate "$WORK/g1" $R1 rewrite.failOnDryRunResults)"
echo "legacy app, -DfailOnDryRunResults=true (the real user property): $(gate "$WORK/g1" $R1 failOnDryRunResults)"
echo "migrated app, -DfailOnDryRunResults=true: $(gate "$WORK/g2" $R1 failOnDryRunResults)"; } > "$OUT/08-ci-gate.txt"
{ echo "# 00-gotcha-gitignore: same recipe, same project, sources under a .gitignore'd directory"
rm -rf "$WORK/ign"; mkdir -p "$WORK/ign"; (cd "$WORK/ign" && git init -q && echo 'legacy/' > .gitignore && cp -r "$OLDPWD/legacy-app" legacy && cd legacy \
&& mvn -B org.openrewrite.maven:rewrite-maven-plugin:6.46.1:dryRun -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:6.37.1,org.openrewrite.recipe:rewrite-migrate-java:3.42.1 -Drewrite.activeRecipes=$R1 2>&1 | grep -E "Applying recipes|Parsing|patch file" )
} > "$OUT/00-gotcha-gitignore.txt"
ls -1 "$OUT"