Compare commits
3
Commits
3e2029ab3a
...
b4b43b94bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4b43b94bf | ||
|
|
e4f927d7af | ||
|
|
acd63e658b |
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Executable
+78
@@ -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
|
||||
Executable
+11
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
Executable
+81
@@ -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
|
||||
Executable
+7
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+9
@@ -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
|
||||
Executable
+52
@@ -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"
|
||||
Reference in New Issue
Block a user