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:
@@ -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