Add boot4-migration module: one Boot 3.5.16 service migrated to 4.1.2, with the real compile, start-up, silent and test failures captured at each step

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b4VQVEJWetTT1dYzyJ
This commit is contained in:
2026-10-29 05:57:26 +00:00
co-authored by Claude Sonnet 5.5
parent fd0cf9b527
commit 85074ed51a
45 changed files with 1021 additions and 0 deletions
+1
View File
@@ -50,6 +50,7 @@ files.
| [`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 | | [`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 |
| [`boot42-otel/`](boot42-otel) | [Spring Boot 4.2 OpenTelemetry: Semantic Conventions, Shared OTLP Settings and MDC Keys](https://ankurm.com/spring-boot-4-2-opentelemetry-semantic-conventions-shared-otlp-mdc/) | one request measured under each `management.observations.conventions` value (the documented `open-telemetry` fails to start on RC1, `opentelemetry` works), meter, span and Prometheus names before and after, `management.opentelemetry.otlp.*` verified against a local OTLP receiver (paths, merged headers, gzip, per-signal overrides), custom MDC keys, and the LongTaskTimer that is no longer on by default | | [`boot42-otel/`](boot42-otel) | [Spring Boot 4.2 OpenTelemetry: Semantic Conventions, Shared OTLP Settings and MDC Keys](https://ankurm.com/spring-boot-4-2-opentelemetry-semantic-conventions-shared-otlp-mdc/) | one request measured under each `management.observations.conventions` value (the documented `open-telemetry` fails to start on RC1, `opentelemetry` works), meter, span and Prometheus names before and after, `management.opentelemetry.otlp.*` verified against a local OTLP receiver (paths, merged headers, gzip, per-signal overrides), custom MDC keys, and the LongTaskTimer that is no longer on by default |
| [`boot4-migration/`](boot4-migration) | [Spring Boot 3 to 4 Migration Guide (Updated for 4.1 and 4.2)](https://ankurm.com/spring-boot-3-to-4-migration-guide/) | one Boot 3.5.16 service migrated to 4.1.2 with the real failures captured at each step: 12 javac errors from a version bump, a start-up failure from a missing `spring-boot-starter-restclient`, Flyway silently not running without `spring-boot-starter-flyway`, removed `logging.file.*` keys silently ignored, Jackson 3 sorting bean properties, liveness/readiness groups appearing, and a `@Mock` field that is `null` without `MockitoExtension` |
`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
+37
View File
@@ -0,0 +1,37 @@
# boot4-migration
Companion module for the ankurm.com post
[Spring Boot 3 to 4 Migration Guide (Updated for 4.1 and 4.2)](https://ankurm.com/spring-boot-3-to-4-migration-guide/).
One small service (Web MVC, Security, Actuator, JDBC + Flyway, AOP aspect, `RestClient.Builder`,
a Jackson serializer, three tests) in two forms:
| Directory | What it is | Built by |
|---|---|---|
| `before/` | the service on Spring Boot 3.5.16 | never by a normal build; `scripts/run-all.sh` copies it to a scratch dir |
| `after/` | the same service migrated to Spring Boot 4.1.2 | `cd after && mvn verify` |
`run-all.sh` builds `before/` as written, then bumps only the parent version and records what
breaks at each step: the build error, the javac errors, the start-up failure report, the
things that start fine but behave differently, and the test failures. Every file in `output/` is
a real transcript from that script (JDK 25.0.5, Maven 3.9).
```bash
export JAVA_HOME=/path/to/jdk-25
./scripts/run-all.sh # 15-25 minutes on a shared 2-vCPU box; needs Maven Central
```
| Output | Shows |
|---|---|
| `01-before-boot35.txt` | `before/` builds and runs on 3.5.16 (the baseline every other file is compared with) |
| `02-bump-only-compile-errors.txt` | only the parent version changed: Maven POM error, then 12 javac errors |
| `03-compiles-but-will-not-start.txt` | imports ported, starters not: compiles, fails at start-up (no `RestClient.Builder`) |
| `04-starts-but-silently-wrong.txt` | starts, answers 200, but Flyway never ran, logs never rotate |
| `05-properties-migrator.txt` | what `spring-boot-properties-migrator` does and does not report |
| `06-after-boot41.txt` | `after/`: 8 tests pass, same endpoints, new health groups |
| `07-jackson-property-order.txt` | bean property order on Jackson 2, Jackson 3, and `use-jackson2-defaults` |
| `08-test-compile-errors.txt` | `@MockBean` and the old `@AutoConfigureMockMvc` package |
| `09-test-runtime-failure.txt` | a plain `@Mock` field is `null`: compiles, fails at run time |
| `10-security-dsl-compile-errors.txt` | the real javac message for the removed `authorizeHttpRequests()` chain |
Not covered: Gradle, Kotlin, native image, and any library that has not shipped a Boot 4 release.
+39
View File
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- THE "AFTER" APP: the same service migrated to Spring Boot 4.1.2. Builds on its own:
mvn -B verify
The "before" sources live in ../before and are never part of this build. -->
<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>migration-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<!-- main code: one starter per technology -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</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-jdbc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-flyway</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aspectj</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-restclient</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
<!-- test code: each starter has a -test companion -->
<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>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,11 @@
package com.ankurm.migration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@@ -0,0 +1,18 @@
package com.ankurm.migration;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
/** Takes the auto-configured RestClient.Builder. RestClient itself lives in spring-web, so this always compiles. */
@Component
public class Gateway {
private final RestClient client;
public Gateway(RestClient.Builder builder) {
this.client = builder.baseUrl("http://localhost:9").build();
}
public String describe() {
return client.getClass().getSimpleName();
}
}
@@ -0,0 +1,6 @@
package com.ankurm.migration;
import java.time.Instant;
public record Greeting(long id, String title, Instant createdAt, String author) {
}
@@ -0,0 +1,62 @@
package com.ankurm.migration;
import java.time.Instant;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final Logger log = LoggerFactory.getLogger(GreetingController.class);
private final GreetingService service;
private final JdbcTemplate jdbc;
private final Gateway gateway;
public GreetingController(GreetingService service, JdbcTemplate jdbc, Gateway gateway) {
this.service = service;
this.jdbc = jdbc;
this.gateway = gateway;
}
@GetMapping("/greetings/{id}")
public Greeting greeting(@PathVariable long id) {
log.info("served greeting {} ........................................", id);
return new Greeting(id, service.title(id), Instant.parse("2026-01-02T03:04:05Z"), "ankur");
}
/** Serialised by Jackson's default record handling (no custom serializer). */
@GetMapping("/summary")
public Summary summary() {
Summary s = new Summary();
s.setId(1);
s.setTitle("Hello");
s.setCreatedAt(Instant.parse("2026-01-02T03:04:05Z"));
s.setAuthor("ankur");
return s;
}
/** Reads the table that the Flyway migration V1 creates. */
@GetMapping("/greetings/count")
public Map<String, Object> count() {
Integer n = jdbc.queryForObject("select count(*) from greeting", Integer.class);
return Map.of("rows", n);
}
/** How many times the @Aspect has intercepted GreetingService. */
@GetMapping("/aspect")
public Map<String, Object> aspect() {
return Map.of("interceptions", TimingAspect.COUNT.get());
}
@GetMapping("/gateway")
public Map<String, Object> gateway() {
return Map.of("client", gateway.describe());
}
}
@@ -0,0 +1,10 @@
package com.ankurm.migration;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String title(long id) {
return "Hello #" + id;
}
}
@@ -0,0 +1,26 @@
package com.ankurm.migration;
import org.springframework.boot.jackson.JacksonComponent;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ser.std.StdSerializer;
/** Boot 4: Jackson 3 (tools.jackson), @JacksonComponent, and no checked IOException. */
@JacksonComponent
public class JsonConfig extends StdSerializer<Greeting> {
public JsonConfig() {
super(Greeting.class);
}
@Override
public void serialize(Greeting g, JsonGenerator gen, SerializationContext ctxt) throws JacksonException {
gen.writeStartObject();
gen.writeNumberProperty("id", g.id());
gen.writeStringProperty("title", g.title());
gen.writeStringProperty("createdAt", g.createdAt().toString());
gen.writeStringProperty("author", g.author());
gen.writeEndObject();
}
}
@@ -0,0 +1,23 @@
package com.ankurm.migration;
import org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
@Order(SecurityFilterProperties.BASIC_AUTH_ORDER)
SecurityFilterChain chain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a
.requestMatchers("/actuator/health/**", "/greetings/**", "/aspect", "/gateway").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
@@ -0,0 +1,24 @@
package com.ankurm.migration;
import java.time.Instant;
/**
* A plain bean (not a record) whose properties are deliberately NOT in alphabetical order.
* Jackson 2 wrote them in declaration order; Jackson 3 sorts bean properties alphabetically by default.
* (Records are unaffected: creator parameters are always written first, in constructor order.)
*/
public class Summary {
private long id;
private String title;
private Instant createdAt;
private String author;
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
}
@@ -0,0 +1,21 @@
package com.ankurm.migration;
import java.util.concurrent.atomic.AtomicInteger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/** Needs AspectJ weaving support on the classpath: spring-boot-starter-aop in Boot 3. */
@Aspect
@Component
public class TimingAspect {
static final AtomicInteger COUNT = new AtomicInteger();
@Around("execution(* com.ankurm.migration.GreetingService.*(..))")
public Object count(ProceedingJoinPoint pjp) throws Throwable {
COUNT.incrementAndGet();
return pjp.proceed();
}
}
@@ -0,0 +1,7 @@
spring.datasource.url=jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1
management.endpoints.web.exposure.include=health,metrics
management.endpoint.health.show-details=always
logging.file.name=${LOG_DIR:target/logs}/app.log
# Boot 4.1 removed the old logging.file.* rolling keys; these are the replacements
logging.logback.rollingpolicy.max-file-size=2KB
logging.logback.rollingpolicy.max-history=50
@@ -0,0 +1,2 @@
create table greeting (id bigint primary key, title varchar(100) not null);
insert into greeting values (1, 'first'), (2, 'second'), (3, 'third');
@@ -0,0 +1,24 @@
package com.ankurm.migration;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
/** Prints the status of a Basic-authenticated POST without a CSRF token (to compare 3.5 with 4.1). */
@SpringBootTest(properties = "spring.security.user.password=pw")
@AutoConfigureMockMvc
class CsrfProbeTest {
@Autowired MockMvc mvc;
@Test
void postWithoutCsrfToken() throws Exception {
int status = mvc.perform(post("/anything").with(httpBasic("user", "pw"))).andReturn().getResponse().getStatus();
System.out.println("CSRFPROBE status=" + status);
}
}
@@ -0,0 +1,27 @@
package com.ankurm.migration;
import static org.mockito.BDDMockito.given;
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.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
class GreetingControllerTest {
@Autowired MockMvc mvc;
@MockitoBean GreetingService service;
@Test
void greetingUsesService() throws Exception {
given(service.title(7)).willReturn("mocked");
mvc.perform(get("/greetings/7")).andExpect(status().isOk()).andExpect(jsonPath("$.title").value("mocked"));
}
}
@@ -0,0 +1,22 @@
package com.ankurm.migration;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
/** Boot 4 fix: Mockito's own extension initialises @Mock fields. */
@SpringBootTest
@ExtendWith(MockitoExtension.class)
class MockFieldTest {
@Mock Runnable task;
@Test
void mockFieldIsInitialised() {
assertNotNull(task, "@Mock field was never initialised");
}
}
@@ -0,0 +1,50 @@
package com.ankurm.migration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.web.servlet.MockMvc;
/** Pins the runtime behaviour the "breaks at runtime" checklist is about, so a regression fails the build. */
@SpringBootTest
@AutoConfigureMockMvc
class RuntimeFactsTest {
@Autowired MockMvc mvc;
@Autowired JdbcTemplate jdbc;
@Test
void flywayMigrationRan() {
assertThat(jdbc.queryForObject("select count(*) from greeting", Integer.class)).isEqualTo(3);
}
@Test
void aspectIntercepts() throws Exception {
mvc.perform(get("/greetings/1")).andExpect(status().isOk());
assertThat(TimingAspect.COUNT.get()).isPositive();
}
@Test
void restClientBuilderIsAutoConfigured() throws Exception {
mvc.perform(get("/gateway")).andExpect(status().isOk());
}
@Test
void metricsEndpointExists() throws Exception {
mvc.perform(get("/actuator/metrics").with(user("u")))
.andExpect(status().isOk());
}
@Test
void serializerIsTheJacksonComponent() throws Exception {
mvc.perform(get("/greetings/2")).andExpect(content().json("{\"id\":2,\"title\":\"Hello #2\"}"));
}
}
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- THE "BEFORE" APP: a Spring Boot 3.5.16 service written the way most 3.x services are.
It is NOT part of any build in this repository; scripts/run-all.sh copies it to a scratch
directory and builds it there, first as-is and then with the parent bumped to 4.1.2. -->
<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>3.5.16</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
<artifactId>migration-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>25</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-actuator</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-jdbc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
<!-- Boot 3: Flyway needs only the library, Boot's auto-configuration was in the big autoconfigure jar -->
<dependency><groupId>org.flywaydb</groupId><artifactId>flyway-core</artifactId></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>
</dependencies>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,11 @@
package com.ankurm.migration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@@ -0,0 +1,18 @@
package com.ankurm.migration;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
/** Takes the auto-configured RestClient.Builder. RestClient itself lives in spring-web, so this always compiles. */
@Component
public class Gateway {
private final RestClient client;
public Gateway(RestClient.Builder builder) {
this.client = builder.baseUrl("http://localhost:9").build();
}
public String describe() {
return client.getClass().getSimpleName();
}
}
@@ -0,0 +1,6 @@
package com.ankurm.migration;
import java.time.Instant;
public record Greeting(long id, String title, Instant createdAt, String author) {
}
@@ -0,0 +1,62 @@
package com.ankurm.migration;
import java.time.Instant;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final Logger log = LoggerFactory.getLogger(GreetingController.class);
private final GreetingService service;
private final JdbcTemplate jdbc;
private final Gateway gateway;
public GreetingController(GreetingService service, JdbcTemplate jdbc, Gateway gateway) {
this.service = service;
this.jdbc = jdbc;
this.gateway = gateway;
}
@GetMapping("/greetings/{id}")
public Greeting greeting(@PathVariable long id) {
log.info("served greeting {} ........................................", id);
return new Greeting(id, service.title(id), Instant.parse("2026-01-02T03:04:05Z"), "ankur");
}
/** Serialised by Jackson's default record handling (no custom serializer). */
@GetMapping("/summary")
public Summary summary() {
Summary s = new Summary();
s.setId(1);
s.setTitle("Hello");
s.setCreatedAt(Instant.parse("2026-01-02T03:04:05Z"));
s.setAuthor("ankur");
return s;
}
/** Reads the table that the Flyway migration V1 creates. */
@GetMapping("/greetings/count")
public Map<String, Object> count() {
Integer n = jdbc.queryForObject("select count(*) from greeting", Integer.class);
return Map.of("rows", n);
}
/** How many times the @Aspect has intercepted GreetingService. */
@GetMapping("/aspect")
public Map<String, Object> aspect() {
return Map.of("interceptions", TimingAspect.COUNT.get());
}
@GetMapping("/gateway")
public Map<String, Object> gateway() {
return Map.of("client", gateway.describe());
}
}
@@ -0,0 +1,10 @@
package com.ankurm.migration;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String title(long id) {
return "Hello #" + id;
}
}
@@ -0,0 +1,22 @@
package com.ankurm.migration;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.jackson.JsonComponent;
/** Boot 3 idiom: a Jackson 2 serializer registered through Boot's @JsonComponent. */
@JsonComponent
public class JsonConfig extends JsonSerializer<Greeting> {
@Override
public void serialize(Greeting g, JsonGenerator gen, SerializerProvider sp) throws IOException {
gen.writeStartObject();
gen.writeNumberField("id", g.id());
gen.writeStringField("title", g.title());
gen.writeStringField("createdAt", g.createdAt().toString());
gen.writeStringField("author", g.author());
gen.writeEndObject();
}
}
@@ -0,0 +1,23 @@
package com.ankurm.migration;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain chain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.requestMatchers("/actuator/health/**", "/greetings/**", "/aspect", "/gateway").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
return http.build();
}
}
@@ -0,0 +1,24 @@
package com.ankurm.migration;
import java.time.Instant;
/**
* A plain bean (not a record) whose properties are deliberately NOT in alphabetical order.
* Jackson 2 wrote them in declaration order; Jackson 3 sorts bean properties alphabetically by default.
* (Records are unaffected: creator parameters are always written first, in constructor order.)
*/
public class Summary {
private long id;
private String title;
private Instant createdAt;
private String author;
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }
}
@@ -0,0 +1,21 @@
package com.ankurm.migration;
import java.util.concurrent.atomic.AtomicInteger;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/** Needs AspectJ weaving support on the classpath: spring-boot-starter-aop in Boot 3. */
@Aspect
@Component
public class TimingAspect {
static final AtomicInteger COUNT = new AtomicInteger();
@Around("execution(* com.ankurm.migration.GreetingService.*(..))")
public Object count(ProceedingJoinPoint pjp) throws Throwable {
COUNT.incrementAndGet();
return pjp.proceed();
}
}
@@ -0,0 +1,7 @@
spring.datasource.url=jdbc:h2:mem:demo;DB_CLOSE_DELAY=-1
management.endpoints.web.exposure.include=health,metrics
management.endpoint.health.show-details=always
# Boot 3 logging properties that were deprecated for years (replaced by logging.logback.rollingpolicy.*)
logging.file.name=${LOG_DIR:target/logs}/app.log
logging.file.max-size=2KB
logging.file.max-history=50
@@ -0,0 +1,2 @@
create table greeting (id bigint primary key, title varchar(100) not null);
insert into greeting values (1, 'first'), (2, 'second'), (3, 'third');
@@ -0,0 +1,24 @@
package com.ankurm.migration;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
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.web.servlet.MockMvc;
/** Prints the status of a Basic-authenticated POST without a CSRF token (to compare 3.5 with 4.1). */
@SpringBootTest(properties = "spring.security.user.password=pw")
@AutoConfigureMockMvc
class CsrfProbeTest {
@Autowired MockMvc mvc;
@Test
void postWithoutCsrfToken() throws Exception {
int status = mvc.perform(post("/anything").with(httpBasic("user", "pw"))).andReturn().getResponse().getStatus();
System.out.println("CSRFPROBE status=" + status);
}
}
@@ -0,0 +1,27 @@
package com.ankurm.migration;
import static org.mockito.BDDMockito.given;
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.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.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
class GreetingControllerTest {
@Autowired MockMvc mvc;
@MockBean GreetingService service;
@Test
void greetingUsesService() throws Exception {
given(service.title(7)).willReturn("mocked");
mvc.perform(get("/greetings/7")).andExpect(status().isOk()).andExpect(jsonPath("$.title").value("mocked"));
}
}
@@ -0,0 +1,22 @@
package com.ankurm.migration;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.boot.test.context.SpringBootTest;
/**
* A plain Mockito @Mock field inside a @SpringBootTest, with no MockitoExtension.
* Boot 3 initialised it through MockitoTestExecutionListener; Boot 4 removed that listener.
*/
@SpringBootTest
class MockFieldTest {
@Mock Runnable task;
@Test
void mockFieldIsInitialised() {
assertNotNull(task, "@Mock field was never initialised");
}
}
@@ -0,0 +1,23 @@
$ mvn package # before/ as written: Boot 3.5.16, JDK 25.0.5
CSRFPROBE status=403
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
$ java -jar migration-demo-1.0.0.jar # Boot 3.5.16
GET /summary
{"id":1,"title":"Hello","createdAt":"2026-01-02T03:04:05Z","author":"ankur"} -> HTTP 200
GET /greetings/1
{"id":1,"title":"Hello #1","createdAt":"2026-01-02T03:04:05Z","author":"ankur"} -> HTTP 200
GET /greetings/count
{"rows":3} -> HTTP 200
GET /aspect
{"interceptions":1} -> HTTP 200
GET /gateway
{"client":"DefaultRestClient"} -> HTTP 200
GET /actuator/metrics (first 60 chars)
{"names":["application.ready.time","application.started.time
GET /actuator/health (groups and status only)
groups=None status=UP components=['db', 'diskSpace', 'ping', 'ssl']
GET /actuator/health/liveness
-> HTTP 404
after 40 more requests, log directory holds: 1 app.log; 5 app.log.<date>.<n>.gz; (app.log is 342 bytes; limit set is logging.file.max-size=2KB or its 4.1 replacement)
@@ -0,0 +1,35 @@
$ mvn -DskipTests package # same sources, same pom, only <parent><version> 3.5.16 -> 4.1.2
[ERROR] 'dependencies.dependency.version' for org.springframework.boot:spring-boot-starter-aop:jar is missing. @ line 25, column 17
[ERROR] 'dependencies.dependency.version' for org.springframework.boot:spring-boot-starter-aop:jar is missing. @ line 25, column 17
# spring-boot-starter-aop has no managed version any more. Rename it to spring-boot-starter-aspectj and try again:
$ mvn compile
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.15.0:compile (default-compile) on project migration-demo: Fatal error compiling: Cannot load from object array because "this.hashes" is null
[INFO] BUILD FAILURE
# Maven's compiler plugin 3.15.0 on JDK 25 crashed inside javac instead of listing the errors, so ask javac directly:
$ javac -cp $(mvn dependency:build-classpath) src/main/java/**/*.java
SecurityConfig.java:3: error: package org.springframework.boot.autoconfigure.security does not exist
import org.springframework.boot.autoconfigure.security.SecurityProperties;
JsonConfig.java:5: error: package com.fasterxml.jackson.core does not exist
import com.fasterxml.jackson.core.JsonGenerator;
JsonConfig.java:6: error: package com.fasterxml.jackson.databind does not exist
import com.fasterxml.jackson.databind.JsonSerializer;
JsonConfig.java:7: error: package com.fasterxml.jackson.databind does not exist
import com.fasterxml.jackson.databind.SerializerProvider;
JsonConfig.java:8: error: cannot find symbol
import org.springframework.boot.jackson.JsonComponent;
JsonConfig.java:12: error: cannot find symbol
public class JsonConfig extends JsonSerializer<Greeting> {
JsonConfig.java:11: error: cannot find symbol
@JsonComponent
JsonConfig.java:14: error: cannot find symbol
public void serialize(Greeting g, JsonGenerator gen, SerializerProvider sp) throws IOException {
JsonConfig.java:14: error: cannot find symbol
public void serialize(Greeting g, JsonGenerator gen, SerializerProvider sp) throws IOException {
SecurityConfig.java:14: error: cannot find symbol
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityConfig.java:16: error: method authorizeHttpRequests in class HttpSecurity cannot be applied to given types;
http.authorizeHttpRequests()
JsonConfig.java:13: error: method does not override or implement a method from a supertype
@Override
javac error count: 12
@@ -0,0 +1,11 @@
$ # s2 = before/ + Boot 4.1.2 + aop->aspectj + JsonConfig and SecurityConfig ported (imports only); every other dependency untouched
$ mvn -DskipTests package
[INFO] BUILD SUCCESS
$ java -jar migration-demo-1.0.0.jar
APPLICATION DID NOT START. The failure report:
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.ankurm.migration.Gateway required a bean of type 'org.springframework.web.client.RestClient$Builder' that could not be found.
Action:
@@ -0,0 +1,25 @@
$ # s3 = s2 + spring-boot-starter-restclient (the only thing the start-up failure asked for). Flyway, properties and the rest still Boot 3 style.
$ mvn -DskipTests package
[INFO] BUILD SUCCESS
$ java -jar migration-demo-1.0.0.jar
GET /summary
{"author":"ankur","createdAt":"2026-01-02T03:04:05Z","id":1,"title":"Hello"} -> HTTP 200
GET /greetings/1
{"id":1,"title":"Hello #1","createdAt":"2026-01-02T03:04:05Z","author":"ankur"} -> HTTP 200
GET /greetings/count
{"timestamp":"2026-10-29T05:55:01.660Z","status":500,"error":"Internal Server Error","path":"/greetings/count"} -> HTTP 500
GET /aspect
{"interceptions":1} -> HTTP 200
GET /gateway
{"client":"DefaultRestClient"} -> HTTP 200
GET /actuator/metrics (first 60 chars)
{"names":["application.ready.time","application.started.time
GET /actuator/health (groups and status only)
groups=['liveness', 'readiness'] status=UP components=['db', 'diskSpace', 'livenessState', 'ping', 'readinessState', 'ssl']
GET /actuator/health/liveness
-> HTTP 200
root cause of that 500 in the application log:
Table "GREETING" not found
Flyway log lines at start-up: 0
after 40 more requests, log directory holds: 1 app.log; (app.log is 54188 bytes; limit set is logging.file.max-size=2KB or its 4.1 replacement)
@@ -0,0 +1,13 @@
$ # s6 = s3 + spring-boot-properties-migrator (runtime scope)
[INFO] BUILD SUCCESS
app started
$ grep -iE 'propert|logging.file|migrat' run.log # what the migrator printed at start-up
WARN 2635 --- [ main] o.s.b.c.p.m.PropertiesMigrationListener :
he use of configuration keys that have been renamed was found in the environment:
Property source 'Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'':
Key: logging.file.max-history
Replacement: logging.logback.rollingpolicy.max-history
Key: logging.file.max-size
Replacement: logging.logback.rollingpolicy.max-file-size
Each configuration key has been temporarily mapped to its replacement for your convenience. To silence this warning, please update your configuration to use the new keys.
(lines matching: 3)
@@ -0,0 +1,23 @@
$ mvn verify # after/ : Boot 4.1.2, one starter per technology, -test companions, Jackson 3, new packages
CSRFPROBE status=403
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
$ java -jar migration-demo-1.0.0.jar
GET /summary
{"author":"ankur","createdAt":"2026-01-02T03:04:05Z","id":1,"title":"Hello"} -> HTTP 200
GET /greetings/1
{"id":1,"title":"Hello #1","createdAt":"2026-01-02T03:04:05Z","author":"ankur"} -> HTTP 200
GET /greetings/count
{"rows":3} -> HTTP 200
GET /aspect
{"interceptions":1} -> HTTP 200
GET /gateway
{"client":"DefaultRestClient"} -> HTTP 200
GET /actuator/metrics (first 60 chars)
{"names":["application.ready.time","application.started.time
GET /actuator/health (groups and status only)
groups=['liveness', 'readiness'] status=UP components=['db', 'diskSpace', 'livenessState', 'ping', 'readinessState', 'ssl']
GET /actuator/health/liveness
-> HTTP 200
after 40 more requests, log directory holds: 1 app.log; 5 app.log.<date>.<n>.gz; (app.log is 513 bytes; limit set is logging.file.max-size=2KB or its 4.1 replacement)
@@ -0,0 +1,4 @@
# GET /summary on the same bean (id, title, createdAt, author declared in that order)
Boot 3.5.16 (Jackson 2): {"id":1,"title":"Hello","createdAt":"2026-01-02T03:04:05Z","author":"ankur"} -> HTTP 200
Boot 4.1.2 (Jackson 3, defaults): {"author":"ankur","createdAt":"2026-01-02T03:04:05Z","id":1,"title":"Hello"} -> HTTP 200
Boot 4.1.2 + spring.jackson.use-jackson2-defaults: {"id":1,"title":"Hello","createdAt":"2026-01-02T03:04:05Z","author":"ankur"} -> HTTP 200
@@ -0,0 +1,6 @@
$ mvn test-compile # after/ pom and main code, but the Boot 3 GreetingControllerTest (@MockBean, old @AutoConfigureMockMvc package)
t1/src/test/java/com/ankurm/migration/GreetingControllerTest.java:[10,63] package org.springframework.boot.test.autoconfigure.web.servlet does not exist
t1/src/test/java/com/ankurm/migration/GreetingControllerTest.java:[12,50] package org.springframework.boot.test.mock.mockito does not exist
t1/src/test/java/com/ankurm/migration/GreetingControllerTest.java:[16,2] cannot find symbol
t1/src/test/java/com/ankurm/migration/GreetingControllerTest.java:[20,6] cannot find symbol
[INFO] BUILD FAILURE
@@ -0,0 +1,5 @@
$ mvn test # after/ pom and main code + the Boot 3 MockFieldTest unchanged (compiles) and CsrfProbeTest (one import fixed)
CSRFPROBE status=403
[ERROR] MockFieldTest.mockFieldIsInitialised:20 @Mock field was never initialised ==> expected: not <null>
[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILURE
@@ -0,0 +1,8 @@
$ javac SecurityConfig.java # Boot 3 file, import repointed to the 4.1.2 package; the old method-chaining DSL left as it was
SecurityConfig.java:16: error: method authorizeHttpRequests in class HttpSecurity cannot be applied to given types;
http.authorizeHttpRequests()
^
required: Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry>
found: no arguments
reason: actual and formal argument lists differ in length
1 error
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
# Regenerates every file in output/. Needs JDK 25 (JAVA_HOME), Maven 3.9 and network access to Maven Central.
# The "before" app (Boot 3.5.16) is never built in place: each stage is copied to a scratch directory.
# Takes 15-25 minutes on a shared 2-vCPU box (each stage is a real Maven build and most start the app).
set -u
HERE=$(cd "$(dirname "$0")/.." && pwd); OUT=$HERE/output; W=${WORK:-/tmp/b4m-work}
MVN=${MVN:-mvn}; PORT=${PORT:-18090}; URL=http://localhost:$PORT
BOOT_OLD=3.5.16; BOOT_NEW=4.1.2
mkdir -p "$OUT"; rm -rf "$W"; mkdir -p "$W"
mv_() { local d=$1; shift; (cd "$d" && $MVN -B -T1 "$@" 2>&1 | grep -vE 'JAVA_TOOL_OPTIONS|^WARNING: |Download(ing|ed)' | sed "s#$W/##g"); }
stage() { rm -rf "$W/$1"; mkdir -p "$W/$1"; cp -r "$HERE/$2/pom.xml" "$HERE/$2/src" "$W/$1/"; }
bump() { sed -i "s#<version>$BOOT_OLD</version>#<version>$BOOT_NEW</version>#" "$W/$1/pom.xml"; }
AFTER_MAIN=$HERE/after/src/main/java/com/ankurm/migration
BEFORE_TEST=$HERE/before/src/test/java/com/ankurm/migration
# start <label> <jar> [extra args]; leaves the app running on $PORT, pid in $W/pid
start() {
local label=$1 jar=$2; shift 2
rm -rf "$W/logs-$label"
LOG_DIR="$W/logs-$label" java -jar "$jar" --server.port=$PORT --spring.security.user.password=pw "$@" > "$W/run-$label.log" 2>&1 &
echo $! > "$W/pid"
for _ in $(seq 1 240); do curl -s -o /dev/null $URL/actuator/health && return 0; kill -0 "$(cat "$W/pid")" 2>/dev/null || return 1; sleep 1; done
return 1
}
stop() { kill "$(cat "$W/pid")" 2>/dev/null; wait "$(cat "$W/pid")" 2>/dev/null; sleep 1; }
g() { echo "GET $1"; curl -s -u user:pw -w ' -> HTTP %{http_code}\n' "$URL$1" | cut -c1-200; }
# probe <label> <jar> [extra args] -> prints what the running app does
probe() {
local label=$1; shift
if ! start "$label" "$@"; then
echo "APPLICATION DID NOT START. The failure report:"; sed -n '/APPLICATION FAILED TO START/,/^Action:/p' "$W/run-$label.log" | grep -v '^$' | head -12
stop; return
fi
g /summary; g /greetings/1; g /greetings/count; g /aspect; g /gateway
echo "GET /actuator/metrics (first 60 chars)"; curl -s -u user:pw -w ' -> HTTP %{http_code}\n' $URL/actuator/metrics | cut -c1-60
echo "GET /actuator/health (groups and status only)"
curl -s $URL/actuator/health | python3 -c 'import sys,json; d=json.load(sys.stdin); print(" groups=%s status=%s components=%s" % (d.get("groups"), d["status"], sorted(d.get("components",{}))))'
echo "GET /actuator/health/liveness"; curl -s -o /dev/null -w ' -> HTTP %{http_code}\n' $URL/actuator/health/liveness
if curl -s -u user:pw $URL/greetings/count | grep -q '"status":500'; then echo "root cause of that 500 in the application log:"; grep -oE 'Table "[A-Z]+" not found' "$W/run-$label.log" | sort -u | sed 's/^/ /'; echo "Flyway log lines at start-up: $(grep -c 'org.flywaydb' "$W/run-$label.log")"; fi
for _ in $(seq 1 40); do curl -s $URL/greetings/1 > /dev/null; done
echo "after 40 more requests, log directory holds: $(ls "$W/logs-$label" | sed 's/\.[0-9-]*\.[0-9]*\.gz/.<date>.<n>.gz/' | sort | uniq -c | tr -s ' ' | tr '\n' ';') (app.log is $(stat -c %s "$W/logs-$label/app.log") bytes; limit set is logging.file.max-size=2KB or its 4.1 replacement)"
stop
}
hdr() { echo "\$ $*"; }
# ---------------------------------------------------------------- 01 baseline on Boot 3.5.16
stage s0 before
{ hdr "mvn package # before/ as written: Boot $BOOT_OLD, JDK $(java -version 2>&1 | grep -v JAVA_TOOL | head -1 | cut -d'"' -f2)"
mv_ "$W/s0" package > "$W/s0.log"; grep -E 'Tests run:|CSRFPROBE|BUILD' "$W/s0.log" | grep -v 'Time elapsed'
echo; hdr "java -jar migration-demo-1.0.0.jar # Boot $BOOT_OLD"
probe boot35 "$W/s0/target/migration-demo-1.0.0.jar"; } > "$OUT/01-before-boot35.txt"
cp "$OUT/01-before-boot35.txt" "$W/probe-boot35.txt"
# ---------------------------------------------------------------- 02 change nothing but the parent version
stage s1 before; bump s1
{ hdr "mvn -DskipTests package # same sources, same pom, only <parent><version> $BOOT_OLD -> $BOOT_NEW"
mv_ "$W/s1" -DskipTests package | grep -E 'ERROR.*(dependency|missing)|BUILD' | sort -u
echo; echo "# spring-boot-starter-aop has no managed version any more. Rename it to spring-boot-starter-aspectj and try again:"
sed -i 's/spring-boot-starter-aop/spring-boot-starter-aspectj/' "$W/s1/pom.xml"
hdr "mvn compile"
mv_ "$W/s1" compile > "$W/s1.log"
grep -E 'BUILD|Fatal error compiling' "$W/s1.log" | sed 's/ -> \[Help 1\]//' | sort -u | cut -c1-230
echo "# Maven's compiler plugin 3.15.0 on JDK 25 crashed inside javac instead of listing the errors, so ask javac directly:"
hdr "javac -cp \$(mvn dependency:build-classpath) src/main/java/**/*.java"
(cd "$W/s1" && $MVN -B -q dependency:build-classpath -Dmdep.outputFile="$W/cp.txt" > /dev/null 2>&1; mkdir -p "$W/jc"
javac -d "$W/jc" -cp "$(cat "$W/cp.txt")" $(find src/main -name '*.java') 2>&1 | grep -v JAVA_TOOL > "$W/s1.javac")
grep -A1 -E 'error:' "$W/s1.javac" | grep -v '^--' | sed 's#^src/main/java/com/ankurm/migration/##'
echo "javac error count: $(grep -c 'error:' "$W/s1.javac")"; } > "$OUT/02-bump-only-compile-errors.txt"
# ---------------------------------------------------------------- 03 imports fixed, starters untouched -> startup failure
stage s2 before; bump s2; sed -i 's/spring-boot-starter-aop/spring-boot-starter-aspectj/' "$W/s2/pom.xml"
rm -rf "$W/s2/src/test"; cp "$AFTER_MAIN"/JsonConfig.java "$AFTER_MAIN"/SecurityConfig.java "$W/s2/src/main/java/com/ankurm/migration/"
{ hdr "# s2 = before/ + Boot $BOOT_NEW + aop->aspectj + JsonConfig and SecurityConfig ported (imports only); every other dependency untouched"
hdr "mvn -DskipTests package"
mv_ "$W/s2" -DskipTests package | grep -E 'ERROR|BUILD'
echo; hdr "java -jar migration-demo-1.0.0.jar"
probe starters-missing "$W/s2/target/migration-demo-1.0.0.jar"; } > "$OUT/03-compiles-but-will-not-start.txt"
# ---------------------------------------------------------------- 04 start-up fixed -> the silent ones
stage s3 before; bump s3; sed -i 's/spring-boot-starter-aop/spring-boot-starter-aspectj/' "$W/s3/pom.xml"
rm -rf "$W/s3/src/test"; cp "$AFTER_MAIN"/JsonConfig.java "$AFTER_MAIN"/SecurityConfig.java "$W/s3/src/main/java/com/ankurm/migration/"
sed -i 's#<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc#<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-restclient</artifactId></dependency>\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc#' "$W/s3/pom.xml"
{ hdr "# s3 = s2 + spring-boot-starter-restclient (the only thing the start-up failure asked for). Flyway, properties and the rest still Boot 3 style."
hdr "mvn -DskipTests package"
mv_ "$W/s3" -DskipTests package | grep -E 'ERROR|BUILD'
echo; hdr "java -jar migration-demo-1.0.0.jar"
probe silent "$W/s3/target/migration-demo-1.0.0.jar"; } > "$OUT/04-starts-but-silently-wrong.txt"
cp "$OUT/04-starts-but-silently-wrong.txt" "$W/probe-silent.txt"
# ---------------------------------------------------------------- 05 properties migrator on the silent build
stage s6 before; bump s6; sed -i 's/spring-boot-starter-aop/spring-boot-starter-aspectj/' "$W/s6/pom.xml"
rm -rf "$W/s6/src/test"; cp "$AFTER_MAIN"/JsonConfig.java "$AFTER_MAIN"/SecurityConfig.java "$W/s6/src/main/java/com/ankurm/migration/"
sed -i 's#<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc#<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-restclient</artifactId></dependency>\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-properties-migrator</artifactId><scope>runtime</scope></dependency>\n <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc#' "$W/s6/pom.xml"
{ hdr "# s6 = s3 + spring-boot-properties-migrator (runtime scope)"
mv_ "$W/s6" -DskipTests package | grep -E 'ERROR|BUILD'
start migrator "$W/s6/target/migration-demo-1.0.0.jar" && echo "app started" || echo "app did not start"
hdr "grep -iE 'propert|logging.file|migrat' run.log # what the migrator printed at start-up"
grep -iE 'logging\.file|PropertiesMigration|The use of configuration keys|Property source|Reason|Replacement' "$W/run-migrator.log" | sed 's/^[0-9T:.+-]* *//' | cut -c1-220 | head -20
echo "(lines matching: $(grep -ciE 'logging\.file|PropertiesMigration' "$W/run-migrator.log"))"
stop; } > "$OUT/05-properties-migrator.txt"
# ---------------------------------------------------------------- 06 the migrated app
{ hdr "mvn verify # after/ : Boot $BOOT_NEW, one starter per technology, -test companions, Jackson 3, new packages"
stage a after
mv_ "$W/a" verify > "$W/a.log"; grep -E 'Tests run:|CSRFPROBE|BUILD' "$W/a.log" | grep -v 'Time elapsed'
echo; hdr "java -jar migration-demo-1.0.0.jar"
probe after "$W/a/target/migration-demo-1.0.0.jar"; } > "$OUT/06-after-boot41.txt"
cp "$OUT/06-after-boot41.txt" "$W/probe-after.txt"
{ hdr "java -jar migration-demo-1.0.0.jar --spring.jackson.use-jackson2-defaults=true"
start j2 "$W/a/target/migration-demo-1.0.0.jar" --spring.jackson.use-jackson2-defaults=true && g /summary; stop; } > "$W/probe-j2.txt"
# ---------------------------------------------------------------- 07 Jackson order, side by side
{ echo "# GET /summary on the same bean (id, title, createdAt, author declared in that order)"
echo "Boot $BOOT_OLD (Jackson 2): $(grep -A1 '^GET /summary' "$W/probe-boot35.txt" | tail -1)"
echo "Boot $BOOT_NEW (Jackson 3, defaults): $(grep -A1 '^GET /summary' "$W/probe-after.txt" | tail -1)"
echo "Boot $BOOT_NEW + spring.jackson.use-jackson2-defaults: $(grep -A1 '^GET /summary' "$W/probe-j2.txt" | tail -1)"; } > "$OUT/07-jackson-property-order.txt"
# ---------------------------------------------------------------- 08 test code
stage t1 after; rm -rf "$W/t1/src/test"; mkdir -p "$W/t1/src/test/java/com/ankurm/migration"
cp "$BEFORE_TEST"/GreetingControllerTest.java "$W/t1/src/test/java/com/ankurm/migration/"
{ hdr "mvn test-compile # after/ pom and main code, but the Boot 3 GreetingControllerTest (@MockBean, old @AutoConfigureMockMvc package)"
mv_ "$W/t1" test-compile > "$W/t1.log"; grep -E '^\[ERROR\] .*\.java' "$W/t1.log" | sed 's#^\[ERROR\] ##' | sort -u; grep -E 'BUILD' "$W/t1.log"; } > "$OUT/08-test-compile-errors.txt"
stage t2 after; rm -rf "$W/t2/src/test"; mkdir -p "$W/t2/src/test/java/com/ankurm/migration"
cp "$BEFORE_TEST"/MockFieldTest.java "$W/t2/src/test/java/com/ankurm/migration/"
sed 's#org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc#org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc#' "$BEFORE_TEST"/CsrfProbeTest.java > "$W/t2/src/test/java/com/ankurm/migration/CsrfProbeTest.java"
{ hdr "mvn test # after/ pom and main code + the Boot 3 MockFieldTest unchanged (compiles) and CsrfProbeTest (one import fixed)"
mv_ "$W/t2" test > "$W/t2.log"; grep -E '^\[(ERROR|INFO)\] Tests run:|CSRFPROBE|^\[ERROR\] MockFieldTest|BUILD' "$W/t2.log" | grep -v 'Time elapsed'; } > "$OUT/09-test-runtime-failure.txt"
bash "$HERE/scripts/security-dsl.sh" > /dev/null
echo "done"; ls -l "$OUT"
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Output 10: the Boot 3 SecurityConfig with ONLY its import repointed (SecurityProperties -> SecurityFilterProperties),
# compiled with javac against the Boot 4.1.2 classpath that run-all.sh already resolved ($W/cp.txt).
set -u
HERE=$(cd "$(dirname "$0")/.." && pwd); W=${WORK:-/tmp/b4m-work}; OUT=$HERE/output/10-security-dsl-compile-errors.txt
D=$W/sec; rm -rf "$D"; mkdir -p "$D/classes"
sed 's#org.springframework.boot.autoconfigure.security.SecurityProperties#org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterProperties#; s#SecurityProperties.BASIC_AUTH_ORDER#SecurityFilterProperties.BASIC_AUTH_ORDER#' \
"$HERE/before/src/main/java/com/ankurm/migration/SecurityConfig.java" > "$D/SecurityConfig.java"
{ echo '$ javac SecurityConfig.java # Boot 3 file, import repointed to the 4.1.2 package; the old method-chaining DSL left as it was'
javac -d "$D/classes" -cp "$(cat "$W/cp.txt")" "$D/SecurityConfig.java" 2>&1 | grep -v JAVA_TOOL | sed "s#$D/##"; } > "$OUT"
cat "$OUT"