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
+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"