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:
2026-10-22 18:34:34 +00:00
parent 2996628ff1
commit 917d39fb65
25 changed files with 631 additions and 0 deletions
+1
View File
@@ -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
+18
View File
@@ -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
+74
View File
@@ -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>
+74
View File
@@ -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
View File
@@ -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"
+10
View File
@@ -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);
}
}
}