Add boot42-rc1-checklist module: Spring Boot 4.2.0-RC1 upgrade checklist run against RC1 and 4.1.2
This commit is contained in:
@@ -45,6 +45,7 @@ files.
|
|||||||
| [`file-transfer/`](file-transfer) | [File Upload and Download in Spring Boot 4: Multipart, Streaming and S3 Presigned URLs](https://ankurm.com/file-upload-download-spring-boot-4-multipart-streaming-s3-presigned-urls/) | multipart limits enforced by Tomcat (and ignored by `MockMvc`), the client-controlled filename, `Range` downloads, a 200 MB download and 150 MB upload under `-Xmx64m`, and S3 presigned URLs checked against a local SigV4 verifier (not a real S3) |
|
| [`file-transfer/`](file-transfer) | [File Upload and Download in Spring Boot 4: Multipart, Streaming and S3 Presigned URLs](https://ankurm.com/file-upload-download-spring-boot-4-multipart-streaming-s3-presigned-urls/) | multipart limits enforced by Tomcat (and ignored by `MockMvc`), the client-controlled filename, `Range` downloads, a 200 MB download and 150 MB upload under `-Xmx64m`, and S3 presigned URLs checked against a local SigV4 verifier (not a real S3) |
|
||||||
| [`project-structure/`](project-structure) | [Spring Boot Project Structure: Package-by-Layer vs Package-by-Feature](https://ankurm.com/spring-boot-project-structure/) | the same application written package-by-layer and package-by-feature with one HTTP API, the real source trees, 9 of 9 types public by layer vs 3 of 15 by feature, and the `javac` error that makes a feature boundary a compiler rule instead of a convention |
|
| [`project-structure/`](project-structure) | [Spring Boot Project Structure: Package-by-Layer vs Package-by-Feature](https://ankurm.com/spring-boot-project-structure/) | the same application written package-by-layer and package-by-feature with one HTTP API, the real source trees, 9 of 9 types public by layer vs 3 of 15 by feature, and the `javac` error that makes a feature boundary a compiler rule instead of a convention |
|
||||||
| [`crud-mysql/`](crud-mysql) | [Spring Boot CRUD REST API with MySQL and Spring Data JPA](https://ankurm.com/spring-boot-crud-rest-api-mysql/) | a complete book API on a real MySQL 8.4.6 server with Flyway owning the schema and Hibernate set to `validate`, `ProblemDetail` errors with per-field validation messages, a duplicate ISBN caught by the database's unique key and mapped to `409` on create and on update, MySQL's own `Duplicate entry` message, and the SQL behind every request |
|
| [`crud-mysql/`](crud-mysql) | [Spring Boot CRUD REST API with MySQL and Spring Data JPA](https://ankurm.com/spring-boot-crud-rest-api-mysql/) | a complete book API on a real MySQL 8.4.6 server with Flyway owning the schema and Hibernate set to `validate`, `ProblemDetail` errors with per-field validation messages, a duplicate ISBN caught by the database's unique key and mapped to `409` on create and on update, MySQL's own `Duplicate entry` message, and the SQL behind every request |
|
||||||
|
| [`boot42-rc1-checklist/`](boot42-rc1-checklist) | [Spring Framework 7.1 and Spring Boot 4.2: What's Coming and How to Prepare](https://ankurm.com/spring-boot-4-2-spring-framework-7-1-upgrade-checklist/) | each 4.2 milestone change run on 4.2.0-RC1 and on 4.1.2: `@DefaultValue` placeholders resolved, five new Kafka admin keys that 4.1.2 silently ignores, embedded LDAP SSL failing without a bundle, the properties migrator's real report (and the removed key it misses), and the `RestTemplate` removal warnings from the compiler |
|
||||||
|
|
||||||
`core-di`, `core-beans`, `custom-starter`, `core-events`, `multi-datasource`, `i18n`, `aot-cache`, `kotlin`, `testing-no-containers`, `rest-test-client` and `hexagonal` are the exception: they have **no `docs/NN-topic.md` chapter folder**. Their deeper material lives in
|
`core-di`, `core-beans`, `custom-starter`, `core-events`, `multi-datasource`, `i18n`, `aot-cache`, `kotlin`, `testing-no-containers`, `rest-test-client` and `hexagonal` are the exception: they have **no `docs/NN-topic.md` chapter folder**. Their deeper material lives in
|
||||||
collapsible sections inside the articles themselves. Where their captured output sits is not
|
collapsible sections inside the articles themselves. Where their captured output sits is not
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# boot42-rc1-checklist
|
||||||
|
|
||||||
|
Companion module for the ankurm.com article *Spring Framework 7.1 and Spring Boot 4.2: What's Coming and How to Prepare*.
|
||||||
|
Everything runs against **Spring Boot 4.2.0-RC1** (Spring Framework 7.1.0-RC1; a release candidate, **not GA**) and, for the before/after comparison, against **4.1.2** (GA). JDK 25.
|
||||||
|
|
||||||
|
| Path | What it is |
|
||||||
|
|---|---|
|
||||||
|
| `pom.xml` / `pom-41.xml` | the same project on 4.2.0-RC1 and on 4.1.2 (a `${revision}` parent does not resolve from a repository, hence two files) |
|
||||||
|
| `src/test/.../DefaultValuePlaceholderTest` | `@DefaultValue` with a `${...}` placeholder: resolved on 4.2, literal text on 4.1.2 (and a startup failure for an `int`) |
|
||||||
|
| `src/test/.../KafkaAdminBindingTest` | the new `spring.kafka.template.admin.*` / `listener.admin.*` keys: 5 of 5 bind on 4.2, 0 of 5 on 4.1.2 with no error |
|
||||||
|
| `src/test/.../LdapEmbeddedSslTest` | embedded LDAP `ssl.enabled=true` without a bundle: fails on 4.2, starts on 4.1.2 |
|
||||||
|
| `src/test/.../MigratorTest` | what `spring-boot-properties-migrator` prints for the relocated Tomcat keys and the removed path-matching key |
|
||||||
|
| `src/broken/UsesRestTemplate.java` | not built; `scripts/compile-broken.sh` compiles it with `-Xlint:deprecation` and keeps the compiler's own message |
|
||||||
|
| `output/` | every transcript quoted in the article; `./scripts/run-all.sh` regenerates them |
|
||||||
|
|
||||||
|
Run: `./scripts/run-all.sh`. Not executed here (they need Docker or a registry): the new default buildpack builder and the image-based build cache.
|
||||||
|
|
||||||
|
There is deliberately no `docs/` folder: the explanations live in the article.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# 01-default-value-placeholders (Spring Boot 4.1.2, JDK 25)
|
||||||
|
no demo.* properties set: zone = ${demo.region:eu}-primary plain = static-value
|
||||||
|
demo.region=ap: zone = ${demo.region:eu}-primary
|
||||||
|
int property, placeholder default: context FAILED to start
|
||||||
|
root cause: java.lang.NumberFormatException
|
||||||
|
For input string: "${demoport.number:8080}"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# 01-default-value-placeholders (Spring Boot 4.2.0-RC1, JDK 25)
|
||||||
|
no demo.* properties set: zone = eu-primary plain = static-value
|
||||||
|
demo.region=ap: zone = ap-primary
|
||||||
|
int property, placeholder default: number = 8080
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 02-kafka-admin-binding (Spring Boot 4.1.2, JDK 25)
|
||||||
|
properties supplied: 5
|
||||||
|
IGNORED spring.kafka.listener.admin.client-id
|
||||||
|
IGNORED spring.kafka.listener.admin.fail-fast
|
||||||
|
IGNORED spring.kafka.listener.await-async-results-on-stop
|
||||||
|
IGNORED spring.kafka.template.admin.client-id
|
||||||
|
IGNORED spring.kafka.template.admin.fail-fast
|
||||||
|
bound = 0 of 5
|
||||||
|
listener.isAwaitAsyncResultsOnStop() = no such method
|
||||||
|
template.getAdmin() = no such method
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 02-kafka-admin-binding (Spring Boot 4.2.0-RC1, JDK 25)
|
||||||
|
properties supplied: 5
|
||||||
|
bound spring.kafka.listener.admin.client-id
|
||||||
|
bound spring.kafka.listener.admin.fail-fast
|
||||||
|
bound spring.kafka.listener.await-async-results-on-stop
|
||||||
|
bound spring.kafka.template.admin.client-id
|
||||||
|
bound spring.kafka.template.admin.fail-fast
|
||||||
|
bound = 5 of 5
|
||||||
|
listener.isAwaitAsyncResultsOnStop() = true
|
||||||
|
template.getAdmin() = org.springframework.boot.kafka.autoconfigure.KafkaProperties$SimpleAdmin@889d9e8
|
||||||
|
template admin client-id = template-admin
|
||||||
|
template admin fail-fast = true
|
||||||
|
listener admin client-id = listener-admin
|
||||||
|
listener admin fail-fast = true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# 03-ldap-embedded-ssl-without-bundle (Spring Boot 4.1.2, JDK 25)
|
||||||
|
spring.ldap.embedded.ssl.enabled=true, no bundle: context STARTED
|
||||||
|
embedded server beans: 1
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# 03-ldap-embedded-ssl-without-bundle (Spring Boot 4.2.0-RC1, JDK 25)
|
||||||
|
spring.ldap.embedded.ssl.enabled=true, no bundle: context FAILED to start
|
||||||
|
java.lang.IllegalStateException
|
||||||
|
SSL is enabled but no SSL bundle has been set. An SSL bundle providing the server's certificate and private key is required for LDAPS
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# 04-properties-migrator-report (Spring Boot 4.1.2, JDK 25)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 04-properties-migrator-report (Spring Boot 4.2.0-RC1, JDK 25)
|
||||||
|
The use of configuration keys that have been renamed was found in the environment:
|
||||||
|
Property source 'defaultProperties':
|
||||||
|
Key: server.tomcat.additional-tld-skip-patterns
|
||||||
|
Replacement: server.tomcat.servlet.additional-tld-skip-patterns
|
||||||
|
Key: server.tomcat.redirect-context-root
|
||||||
|
Replacement: server.tomcat.servlet.redirect-context-root
|
||||||
|
Key: server.tomcat.use-relative-redirects
|
||||||
|
Replacement: server.tomcat.servlet.use-relative-redirects
|
||||||
|
The use of configuration keys that are no longer supported was found in the environment:
|
||||||
|
Property source 'defaultProperties':
|
||||||
|
Key: spring.mvc.pathmatch.matching-strategy
|
||||||
|
Reason: Path matching is no longer configurable
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# 05-resttemplate-compiler-warnings (Spring Boot 4.1.2, JDK 25.0.5)
|
||||||
|
$ javac -Xlint:deprecation -cp <module classpath> -d target/broken src/broken/UsesRestTemplate.java
|
||||||
|
exit: 0
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 05-resttemplate-compiler-warnings (Spring Boot 4.2.0-RC1, JDK 25.0.5)
|
||||||
|
$ javac -Xlint:deprecation -cp <module classpath> -d target/broken src/broken/UsesRestTemplate.java
|
||||||
|
src/broken/UsesRestTemplate.java:11: warning: [removal] RestTemplateBuilder in org.springframework.boot.restclient has been deprecated and marked for removal
|
||||||
|
public String fetch(RestTemplateBuilder builder) {
|
||||||
|
^
|
||||||
|
src/broken/UsesRestTemplate.java:12: warning: [removal] RestTemplate in org.springframework.web.client has been deprecated and marked for removal
|
||||||
|
RestTemplate rest = builder.build();
|
||||||
|
^
|
||||||
|
2 warnings
|
||||||
|
exit: 0
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?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.2</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>boot42-rc1-checklist</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>boot42-rc1-checklist</name>
|
||||||
|
<description>Spring Boot 4.2.0-RC1 upgrade checklist: each claim run against 4.2.0-RC1 and 4.1.2</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
<boot.label>4.1.2</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-kafka</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-ldap</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.unboundid</groupId>
|
||||||
|
<artifactId>unboundid-ldapsdk</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-properties-migrator</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-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,74 @@
|
|||||||
|
<?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-RC1</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>boot42-rc1-checklist</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>boot42-rc1-checklist</name>
|
||||||
|
<description>Spring Boot 4.2.0-RC1 upgrade checklist: each claim run against 4.2.0-RC1 and 4.1.2</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>25</java.version>
|
||||||
|
<boot.label>4.2.0-RC1</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-kafka</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-ldap</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.unboundid</groupId>
|
||||||
|
<artifactId>unboundid-ldapsdk</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-properties-migrator</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-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>
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Compiles src/broken/UsesRestTemplate.java with -Xlint:deprecation against the module classpath
|
||||||
|
# of the given pom (default pom.xml = 4.2.0-RC1; pass pom-41.xml for the 4.1.2 baseline) and
|
||||||
|
# stores the compiler's own message in output/05-resttemplate-compiler-warnings[-on-4.1.2].txt.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
POM="${1:-pom.xml}"
|
||||||
|
LABEL=$(grep -m1 '<boot.label>' "$POM" | sed -E 's/.*<boot.label>(.*)<\/boot.label>.*/\1/')
|
||||||
|
SUFFIX=""; [[ "$LABEL" == 4.2* ]] || SUFFIX="-on-$LABEL"
|
||||||
|
OUT="output/05-resttemplate-compiler-warnings$SUFFIX.txt"
|
||||||
|
mvn -B -q -f "$POM" dependency:build-classpath -Dmdep.outputFile=target/cp.txt >/dev/null 2>&1
|
||||||
|
rm -rf target/broken && mkdir -p target/broken
|
||||||
|
{
|
||||||
|
echo "# 05-resttemplate-compiler-warnings (Spring Boot $LABEL, JDK $(javac -version 2>&1 | grep -v Picked | awk '{print $2}'))"
|
||||||
|
echo '$ javac -Xlint:deprecation -cp <module classpath> -d target/broken src/broken/UsesRestTemplate.java'
|
||||||
|
javac -Xlint:deprecation -cp "$(cat target/cp.txt)" -d target/broken src/broken/UsesRestTemplate.java 2>&1 \
|
||||||
|
| grep -v '^Picked up' | sed -e "s#^$(pwd)/##"
|
||||||
|
echo "exit: ${PIPESTATUS[0]}"
|
||||||
|
} > "$OUT"
|
||||||
|
cat "$OUT"
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerates every file in output/ on Spring Boot 4.2.0-RC1 and on the 4.1.2 baseline.
|
||||||
|
# Needs JDK 25 and Maven 3.9 on PATH.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
mvn -B -q test
|
||||||
|
mvn -B -q -f pom-41.xml test
|
||||||
|
scripts/compile-broken.sh pom.xml >/dev/null
|
||||||
|
scripts/compile-broken.sh pom-41.xml >/dev/null
|
||||||
|
echo "done: $(ls output | wc -l) files in output/"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.boot.restclient.RestTemplateBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not part of the build. scripts/compile-broken.sh compiles it with -Xlint:deprecation and
|
||||||
|
* commits the compiler's own message. RestTemplate and the Boot builder around it are
|
||||||
|
* deprecated in Spring Framework 7.1 / Spring Boot 4.2.
|
||||||
|
*/
|
||||||
|
public class UsesRestTemplate {
|
||||||
|
|
||||||
|
public String fetch(RestTemplateBuilder builder) {
|
||||||
|
RestTemplate rest = builder.build();
|
||||||
|
return rest.getForObject("http://localhost:8080/ping", String.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ankurm.rc1;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A non-String property with a placeholder default. On 4.1.x the literal text
|
||||||
|
* {@code ${demo.port:8080}} cannot be converted to int, so the context fails to start.
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties("demoport")
|
||||||
|
public record DemoPort(@DefaultValue("${demoport.number:8080}") int number) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ankurm.rc1;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor-bound properties whose defaults contain placeholders.
|
||||||
|
*
|
||||||
|
* <p>Spring Boot 4.2.0-M2 made {@code @DefaultValue} resolve property placeholders during
|
||||||
|
* binding. On 4.1.x the same annotation value is taken literally.
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties("demo")
|
||||||
|
public record DemoProperties(
|
||||||
|
@DefaultValue("${demo.region:eu}-primary") String zone,
|
||||||
|
@DefaultValue("static-value") String plain) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ankurm.rc1;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
@ConfigurationPropertiesScan
|
||||||
|
public class Rc1ChecklistApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(Rc1ChecklistApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.ankurm.rc1;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class DefaultValuePlaceholderTest {
|
||||||
|
|
||||||
|
@EnableConfigurationProperties(DemoProperties.class)
|
||||||
|
static class StringConfig {
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableConfigurationProperties(DemoPort.class)
|
||||||
|
static class PortConfig {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void placeholderInDefaultValue() {
|
||||||
|
Transcript t = new Transcript("01-default-value-placeholders");
|
||||||
|
new ApplicationContextRunner().withUserConfiguration(StringConfig.class).run(ctx -> {
|
||||||
|
DemoProperties p = ctx.getBean(DemoProperties.class);
|
||||||
|
t.line("no demo.* properties set: zone = " + p.zone() + " plain = " + p.plain());
|
||||||
|
if (Transcript.is42()) {
|
||||||
|
assertThat(p.zone()).isEqualTo("eu-primary");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assertThat(p.zone()).isEqualTo("${demo.region:eu}-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
new ApplicationContextRunner().withUserConfiguration(StringConfig.class)
|
||||||
|
.withPropertyValues("demo.region=ap")
|
||||||
|
.run(ctx -> {
|
||||||
|
DemoProperties p = ctx.getBean(DemoProperties.class);
|
||||||
|
t.line("demo.region=ap: zone = " + p.zone());
|
||||||
|
if (Transcript.is42()) {
|
||||||
|
assertThat(p.zone()).isEqualTo("ap-primary");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assertThat(p.zone()).isEqualTo("${demo.region:eu}-primary");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
new ApplicationContextRunner().withUserConfiguration(PortConfig.class).run(ctx -> {
|
||||||
|
if (Transcript.is42()) {
|
||||||
|
assertThat(ctx).hasNotFailed();
|
||||||
|
t.line("int property, placeholder default: number = " + ctx.getBean(DemoPort.class).number());
|
||||||
|
assertThat(ctx.getBean(DemoPort.class).number()).isEqualTo(8080);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assertThat(ctx).hasFailed();
|
||||||
|
Throwable root = rootCause(ctx.getStartupFailure());
|
||||||
|
t.line("int property, placeholder default: context FAILED to start");
|
||||||
|
t.line("root cause: " + root.getClass().getName());
|
||||||
|
t.line(" " + root.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.write();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Throwable rootCause(Throwable ex) {
|
||||||
|
Throwable t = ex;
|
||||||
|
while (t.getCause() != null) {
|
||||||
|
t = t.getCause();
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.ankurm.rc1;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.context.properties.bind.BindContext;
|
||||||
|
import org.springframework.boot.context.properties.bind.BindHandler;
|
||||||
|
import org.springframework.boot.context.properties.bind.Bindable;
|
||||||
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
|
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
|
||||||
|
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
|
||||||
|
import org.springframework.boot.kafka.autoconfigure.KafkaProperties;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the new Kafka admin properties and records which of the five keys the binder actually
|
||||||
|
* consumed. On 4.2 all five bind. On 4.1.2 Boot does not complain about the unknown keys; they
|
||||||
|
* are simply never read, which is the failure mode worth knowing about.
|
||||||
|
*/
|
||||||
|
class KafkaAdminBindingTest {
|
||||||
|
|
||||||
|
private static final Map<String, String> PROPS = Map.of(
|
||||||
|
"spring.kafka.template.admin.client-id", "template-admin",
|
||||||
|
"spring.kafka.template.admin.fail-fast", "true",
|
||||||
|
"spring.kafka.listener.admin.client-id", "listener-admin",
|
||||||
|
"spring.kafka.listener.admin.fail-fast", "true",
|
||||||
|
"spring.kafka.listener.await-async-results-on-stop", "true");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void adminPropertiesBind() throws Exception {
|
||||||
|
Transcript t = new Transcript("02-kafka-admin-binding");
|
||||||
|
Set<String> consumed = new TreeSet<>();
|
||||||
|
BindHandler recorder = new BindHandler() {
|
||||||
|
@Override
|
||||||
|
public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context,
|
||||||
|
Object result) {
|
||||||
|
if (context.getConfigurationProperty() != null) {
|
||||||
|
consumed.add(name.toString());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Binder binder = new Binder(new MapConfigurationPropertySource(PROPS));
|
||||||
|
KafkaProperties kp = binder.bind("spring.kafka", Bindable.of(KafkaProperties.class), recorder)
|
||||||
|
.orElseGet(KafkaProperties::new);
|
||||||
|
|
||||||
|
t.line("properties supplied: " + PROPS.size());
|
||||||
|
for (String key : new TreeSet<>(PROPS.keySet())) {
|
||||||
|
t.line((consumed.contains(key.substring("spring.kafka.".length())) || consumed.contains(key) ? "bound " : "IGNORED ")
|
||||||
|
+ key);
|
||||||
|
}
|
||||||
|
long bound = PROPS.keySet().stream()
|
||||||
|
.filter(k -> consumed.contains(k.substring("spring.kafka.".length())) || consumed.contains(k))
|
||||||
|
.count();
|
||||||
|
t.line("bound = " + bound + " of " + PROPS.size());
|
||||||
|
|
||||||
|
Object listener = call(kp, "getListener");
|
||||||
|
Object template = call(kp, "getTemplate");
|
||||||
|
t.line("listener.isAwaitAsyncResultsOnStop() = " + callOrAbsent(listener, "isAwaitAsyncResultsOnStop"));
|
||||||
|
t.line("template.getAdmin() = " + callOrAbsent(template, "getAdmin"));
|
||||||
|
if (Transcript.is42()) {
|
||||||
|
assertThat(bound).isEqualTo(5);
|
||||||
|
Object tplAdmin = call(template, "getAdmin");
|
||||||
|
Object lsnAdmin = call(listener, "getAdmin");
|
||||||
|
t.line("template admin client-id = " + call(tplAdmin, "getClientId"));
|
||||||
|
t.line("template admin fail-fast = " + call(tplAdmin, "isFailFast"));
|
||||||
|
t.line("listener admin client-id = " + call(lsnAdmin, "getClientId"));
|
||||||
|
t.line("listener admin fail-fast = " + call(lsnAdmin, "isFailFast"));
|
||||||
|
assertThat(call(tplAdmin, "getClientId")).isEqualTo("template-admin");
|
||||||
|
assertThat(call(lsnAdmin, "isFailFast")).isEqualTo(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assertThat(bound).isZero();
|
||||||
|
}
|
||||||
|
t.write();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object call(Object target, String method) throws Exception {
|
||||||
|
Method m = target.getClass().getMethod(method);
|
||||||
|
return m.invoke(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String callOrAbsent(Object target, String method) {
|
||||||
|
try {
|
||||||
|
return String.valueOf(call(target, method));
|
||||||
|
}
|
||||||
|
catch (NoSuchMethodException ex) {
|
||||||
|
return "no such method";
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
return ex.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ankurm.rc1;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.ldap.autoconfigure.embedded.EmbeddedLdapAutoConfiguration;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4.2.0-M2: spring.ldap.embedded.ssl.enabled=true without spring.ldap.embedded.ssl.bundle now
|
||||||
|
* fails at startup instead of quietly starting a plain-text listener.
|
||||||
|
*/
|
||||||
|
class LdapEmbeddedSslTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sslWithoutBundle() {
|
||||||
|
Transcript t = new Transcript("03-ldap-embedded-ssl-without-bundle");
|
||||||
|
new ApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(EmbeddedLdapAutoConfiguration.class))
|
||||||
|
.withPropertyValues("spring.ldap.embedded.base-dn=dc=example,dc=org",
|
||||||
|
"spring.ldap.embedded.port=0",
|
||||||
|
"spring.ldap.embedded.ssl.enabled=true")
|
||||||
|
.run(ctx -> {
|
||||||
|
if (Transcript.is42()) {
|
||||||
|
assertThat(ctx).hasFailed();
|
||||||
|
Throwable root = ctx.getStartupFailure();
|
||||||
|
while (root.getCause() != null) {
|
||||||
|
root = root.getCause();
|
||||||
|
}
|
||||||
|
t.line("spring.ldap.embedded.ssl.enabled=true, no bundle: context FAILED to start");
|
||||||
|
t.line(root.getClass().getName());
|
||||||
|
t.line(root.getMessage());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
assertThat(ctx).hasNotFailed();
|
||||||
|
t.line("spring.ldap.embedded.ssl.enabled=true, no bundle: context STARTED");
|
||||||
|
t.line("embedded server beans: "
|
||||||
|
+ ctx.getBeanNamesForType(com.unboundid.ldap.listener.InMemoryDirectoryServer.class).length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.write();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.ankurm.rc1;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.WebApplicationType;
|
||||||
|
import org.springframework.boot.test.system.CapturedOutput;
|
||||||
|
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an empty application with the property keys 4.2 relocated or deprecated and captures
|
||||||
|
* what spring-boot-properties-migrator prints. The keys come from the 4.2.0-M2 configuration
|
||||||
|
* changelog.
|
||||||
|
*/
|
||||||
|
@ExtendWith(OutputCaptureExtension.class)
|
||||||
|
class MigratorTest {
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
static class Empty {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void migratorReport(CapturedOutput output) {
|
||||||
|
Transcript t = new Transcript("04-properties-migrator-report");
|
||||||
|
SpringApplication app = new SpringApplication(Empty.class);
|
||||||
|
app.setWebApplicationType(WebApplicationType.NONE);
|
||||||
|
app.setDefaultProperties(java.util.Map.of(
|
||||||
|
"server.tomcat.use-relative-redirects", "false",
|
||||||
|
"server.tomcat.redirect-context-root", "false",
|
||||||
|
"server.tomcat.additional-tld-skip-patterns", "*.jar",
|
||||||
|
"spring.mvc.pathmatch.matching-strategy", "path_pattern_parser",
|
||||||
|
"management.metrics.observations.ignored-meters", "http.server.requests"));
|
||||||
|
app.run().close();
|
||||||
|
boolean any = false;
|
||||||
|
for (String line : output.getAll().split("\n")) {
|
||||||
|
String s = line.strip();
|
||||||
|
if (s.startsWith("Property source") || s.startsWith("Key:") || s.startsWith("Reason:")
|
||||||
|
|| s.startsWith("Replacement:") || s.contains("configuration keys")
|
||||||
|
|| s.contains("have been renamed") || s.contains("deprecated") && s.contains("configuration")
|
||||||
|
|| s.contains("no longer supported") || s.contains("been removed")) {
|
||||||
|
t.line(s);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Transcript.is42()) {
|
||||||
|
assertThat(output.getAll()).contains("server.tomcat.servlet.use-relative-redirects");
|
||||||
|
assertThat(any).isTrue();
|
||||||
|
}
|
||||||
|
t.write();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package com.ankurm.rc1;
|
||||||
|
|
||||||
|
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 (or NN-name-on-4.1.2.txt when run against the
|
||||||
|
* 4.1 baseline). The test asserts the same values it writes, so every figure quoted in the
|
||||||
|
* article is an assertion that fails the build when it stops being true.
|
||||||
|
*/
|
||||||
|
final class Transcript {
|
||||||
|
|
||||||
|
static final String LABEL = System.getProperty("boot.label", "4.2.0-RC1");
|
||||||
|
|
||||||
|
static boolean is42() {
|
||||||
|
return LABEL.startsWith("4.2");
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<String> lines = new ArrayList<>();
|
||||||
|
private final Path file;
|
||||||
|
|
||||||
|
Transcript(String name) {
|
||||||
|
String suffix = is42() ? "" : "-on-" + LABEL;
|
||||||
|
this.file = Path.of("output", name + suffix + ".txt");
|
||||||
|
line("# " + name + " (Spring Boot " + LABEL + ", JDK " + Runtime.version().feature() + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
Transcript line(String s) {
|
||||||
|
lines.add(s);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write() {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(file.getParent());
|
||||||
|
Files.write(file, lines);
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new IllegalStateException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user