From 85074ed51abad3adae6da1c643a50f4f040eaa89 Mon Sep 17 00:00:00 2001 From: asmhatre Date: Thu, 29 Oct 2026 05:57:26 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_017ic4b4VQVEJWetTT1dYzyJ --- README.md | 1 + boot4-migration/README.md | 37 +++++ boot4-migration/after/pom.xml | 39 ++++++ .../com/ankurm/migration/DemoApplication.java | 11 ++ .../java/com/ankurm/migration/Gateway.java | 18 +++ .../java/com/ankurm/migration/Greeting.java | 6 + .../ankurm/migration/GreetingController.java | 62 +++++++++ .../com/ankurm/migration/GreetingService.java | 10 ++ .../java/com/ankurm/migration/JsonConfig.java | 26 ++++ .../com/ankurm/migration/SecurityConfig.java | 23 +++ .../java/com/ankurm/migration/Summary.java | 24 ++++ .../com/ankurm/migration/TimingAspect.java | 21 +++ .../src/main/resources/application.properties | 7 + .../resources/db/migration/V1__greeting.sql | 2 + .../com/ankurm/migration/CsrfProbeTest.java | 24 ++++ .../migration/GreetingControllerTest.java | 27 ++++ .../com/ankurm/migration/MockFieldTest.java | 22 +++ .../ankurm/migration/RuntimeFactsTest.java | 50 +++++++ boot4-migration/before/pom.xml | 37 +++++ .../com/ankurm/migration/DemoApplication.java | 11 ++ .../java/com/ankurm/migration/Gateway.java | 18 +++ .../java/com/ankurm/migration/Greeting.java | 6 + .../ankurm/migration/GreetingController.java | 62 +++++++++ .../com/ankurm/migration/GreetingService.java | 10 ++ .../java/com/ankurm/migration/JsonConfig.java | 22 +++ .../com/ankurm/migration/SecurityConfig.java | 23 +++ .../java/com/ankurm/migration/Summary.java | 24 ++++ .../com/ankurm/migration/TimingAspect.java | 21 +++ .../src/main/resources/application.properties | 7 + .../resources/db/migration/V1__greeting.sql | 2 + .../com/ankurm/migration/CsrfProbeTest.java | 24 ++++ .../migration/GreetingControllerTest.java | 27 ++++ .../com/ankurm/migration/MockFieldTest.java | 22 +++ boot4-migration/output/01-before-boot35.txt | 23 +++ .../output/02-bump-only-compile-errors.txt | 35 +++++ .../output/03-compiles-but-will-not-start.txt | 11 ++ .../output/04-starts-but-silently-wrong.txt | 25 ++++ .../output/05-properties-migrator.txt | 13 ++ boot4-migration/output/06-after-boot41.txt | 23 +++ .../output/07-jackson-property-order.txt | 4 + .../output/08-test-compile-errors.txt | 6 + .../output/09-test-runtime-failure.txt | 5 + .../output/10-security-dsl-compile-errors.txt | 8 ++ boot4-migration/scripts/run-all.sh | 131 ++++++++++++++++++ boot4-migration/scripts/security-dsl.sh | 11 ++ 45 files changed, 1021 insertions(+) create mode 100644 boot4-migration/README.md create mode 100644 boot4-migration/after/pom.xml create mode 100644 boot4-migration/after/src/main/java/com/ankurm/migration/DemoApplication.java create mode 100644 boot4-migration/after/src/main/java/com/ankurm/migration/Gateway.java create mode 100644 boot4-migration/after/src/main/java/com/ankurm/migration/Greeting.java create mode 100644 boot4-migration/after/src/main/java/com/ankurm/migration/GreetingController.java create mode 100644 boot4-migration/after/src/main/java/com/ankurm/migration/GreetingService.java create mode 100644 boot4-migration/after/src/main/java/com/ankurm/migration/JsonConfig.java create mode 100644 boot4-migration/after/src/main/java/com/ankurm/migration/SecurityConfig.java create mode 100644 boot4-migration/after/src/main/java/com/ankurm/migration/Summary.java create mode 100644 boot4-migration/after/src/main/java/com/ankurm/migration/TimingAspect.java create mode 100644 boot4-migration/after/src/main/resources/application.properties create mode 100644 boot4-migration/after/src/main/resources/db/migration/V1__greeting.sql create mode 100644 boot4-migration/after/src/test/java/com/ankurm/migration/CsrfProbeTest.java create mode 100644 boot4-migration/after/src/test/java/com/ankurm/migration/GreetingControllerTest.java create mode 100644 boot4-migration/after/src/test/java/com/ankurm/migration/MockFieldTest.java create mode 100644 boot4-migration/after/src/test/java/com/ankurm/migration/RuntimeFactsTest.java create mode 100644 boot4-migration/before/pom.xml create mode 100644 boot4-migration/before/src/main/java/com/ankurm/migration/DemoApplication.java create mode 100644 boot4-migration/before/src/main/java/com/ankurm/migration/Gateway.java create mode 100644 boot4-migration/before/src/main/java/com/ankurm/migration/Greeting.java create mode 100644 boot4-migration/before/src/main/java/com/ankurm/migration/GreetingController.java create mode 100644 boot4-migration/before/src/main/java/com/ankurm/migration/GreetingService.java create mode 100644 boot4-migration/before/src/main/java/com/ankurm/migration/JsonConfig.java create mode 100644 boot4-migration/before/src/main/java/com/ankurm/migration/SecurityConfig.java create mode 100644 boot4-migration/before/src/main/java/com/ankurm/migration/Summary.java create mode 100644 boot4-migration/before/src/main/java/com/ankurm/migration/TimingAspect.java create mode 100644 boot4-migration/before/src/main/resources/application.properties create mode 100644 boot4-migration/before/src/main/resources/db/migration/V1__greeting.sql create mode 100644 boot4-migration/before/src/test/java/com/ankurm/migration/CsrfProbeTest.java create mode 100644 boot4-migration/before/src/test/java/com/ankurm/migration/GreetingControllerTest.java create mode 100644 boot4-migration/before/src/test/java/com/ankurm/migration/MockFieldTest.java create mode 100644 boot4-migration/output/01-before-boot35.txt create mode 100644 boot4-migration/output/02-bump-only-compile-errors.txt create mode 100644 boot4-migration/output/03-compiles-but-will-not-start.txt create mode 100644 boot4-migration/output/04-starts-but-silently-wrong.txt create mode 100644 boot4-migration/output/05-properties-migrator.txt create mode 100644 boot4-migration/output/06-after-boot41.txt create mode 100644 boot4-migration/output/07-jackson-property-order.txt create mode 100644 boot4-migration/output/08-test-compile-errors.txt create mode 100644 boot4-migration/output/09-test-runtime-failure.txt create mode 100644 boot4-migration/output/10-security-dsl-compile-errors.txt create mode 100755 boot4-migration/scripts/run-all.sh create mode 100755 boot4-migration/scripts/security-dsl.sh diff --git a/README.md b/README.md index b61c64b..b89d3bc 100644 --- a/README.md +++ b/README.md @@ -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 | | [`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 | +| [`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 collapsible sections inside the articles themselves. Where their captured output sits is not diff --git a/boot4-migration/README.md b/boot4-migration/README.md new file mode 100644 index 0000000..2fdbefb --- /dev/null +++ b/boot4-migration/README.md @@ -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. diff --git a/boot4-migration/after/pom.xml b/boot4-migration/after/pom.xml new file mode 100644 index 0000000..fc14cb2 --- /dev/null +++ b/boot4-migration/after/pom.xml @@ -0,0 +1,39 @@ + + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.1.2 + + + com.ankurm + migration-demo + 1.0.0 + + 25 + + + + org.springframework.bootspring-boot-starter-webmvc + org.springframework.bootspring-boot-starter-actuator + org.springframework.bootspring-boot-starter-security + org.springframework.bootspring-boot-starter-jdbc + org.springframework.bootspring-boot-starter-flyway + org.springframework.bootspring-boot-starter-aspectj + org.springframework.bootspring-boot-starter-restclient + com.h2databaseh2runtime + + org.springframework.bootspring-boot-starter-webmvc-testtest + org.springframework.bootspring-boot-starter-security-testtest + + + + org.springframework.bootspring-boot-maven-plugin + + + diff --git a/boot4-migration/after/src/main/java/com/ankurm/migration/DemoApplication.java b/boot4-migration/after/src/main/java/com/ankurm/migration/DemoApplication.java new file mode 100644 index 0000000..ba69bca --- /dev/null +++ b/boot4-migration/after/src/main/java/com/ankurm/migration/DemoApplication.java @@ -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); + } +} diff --git a/boot4-migration/after/src/main/java/com/ankurm/migration/Gateway.java b/boot4-migration/after/src/main/java/com/ankurm/migration/Gateway.java new file mode 100644 index 0000000..d04d5e5 --- /dev/null +++ b/boot4-migration/after/src/main/java/com/ankurm/migration/Gateway.java @@ -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(); + } +} diff --git a/boot4-migration/after/src/main/java/com/ankurm/migration/Greeting.java b/boot4-migration/after/src/main/java/com/ankurm/migration/Greeting.java new file mode 100644 index 0000000..fecf4a1 --- /dev/null +++ b/boot4-migration/after/src/main/java/com/ankurm/migration/Greeting.java @@ -0,0 +1,6 @@ +package com.ankurm.migration; + +import java.time.Instant; + +public record Greeting(long id, String title, Instant createdAt, String author) { +} diff --git a/boot4-migration/after/src/main/java/com/ankurm/migration/GreetingController.java b/boot4-migration/after/src/main/java/com/ankurm/migration/GreetingController.java new file mode 100644 index 0000000..3ba6fc9 --- /dev/null +++ b/boot4-migration/after/src/main/java/com/ankurm/migration/GreetingController.java @@ -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 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 aspect() { + return Map.of("interceptions", TimingAspect.COUNT.get()); + } + + @GetMapping("/gateway") + public Map gateway() { + return Map.of("client", gateway.describe()); + } +} diff --git a/boot4-migration/after/src/main/java/com/ankurm/migration/GreetingService.java b/boot4-migration/after/src/main/java/com/ankurm/migration/GreetingService.java new file mode 100644 index 0000000..d2fa800 --- /dev/null +++ b/boot4-migration/after/src/main/java/com/ankurm/migration/GreetingService.java @@ -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; + } +} diff --git a/boot4-migration/after/src/main/java/com/ankurm/migration/JsonConfig.java b/boot4-migration/after/src/main/java/com/ankurm/migration/JsonConfig.java new file mode 100644 index 0000000..9f95018 --- /dev/null +++ b/boot4-migration/after/src/main/java/com/ankurm/migration/JsonConfig.java @@ -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 { + + 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(); + } +} diff --git a/boot4-migration/after/src/main/java/com/ankurm/migration/SecurityConfig.java b/boot4-migration/after/src/main/java/com/ankurm/migration/SecurityConfig.java new file mode 100644 index 0000000..0082be6 --- /dev/null +++ b/boot4-migration/after/src/main/java/com/ankurm/migration/SecurityConfig.java @@ -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(); + } +} diff --git a/boot4-migration/after/src/main/java/com/ankurm/migration/Summary.java b/boot4-migration/after/src/main/java/com/ankurm/migration/Summary.java new file mode 100644 index 0000000..79c88bf --- /dev/null +++ b/boot4-migration/after/src/main/java/com/ankurm/migration/Summary.java @@ -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; } +} diff --git a/boot4-migration/after/src/main/java/com/ankurm/migration/TimingAspect.java b/boot4-migration/after/src/main/java/com/ankurm/migration/TimingAspect.java new file mode 100644 index 0000000..22a8c48 --- /dev/null +++ b/boot4-migration/after/src/main/java/com/ankurm/migration/TimingAspect.java @@ -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(); + } +} diff --git a/boot4-migration/after/src/main/resources/application.properties b/boot4-migration/after/src/main/resources/application.properties new file mode 100644 index 0000000..28f5e00 --- /dev/null +++ b/boot4-migration/after/src/main/resources/application.properties @@ -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 diff --git a/boot4-migration/after/src/main/resources/db/migration/V1__greeting.sql b/boot4-migration/after/src/main/resources/db/migration/V1__greeting.sql new file mode 100644 index 0000000..86d6781 --- /dev/null +++ b/boot4-migration/after/src/main/resources/db/migration/V1__greeting.sql @@ -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'); diff --git a/boot4-migration/after/src/test/java/com/ankurm/migration/CsrfProbeTest.java b/boot4-migration/after/src/test/java/com/ankurm/migration/CsrfProbeTest.java new file mode 100644 index 0000000..fd5ab1e --- /dev/null +++ b/boot4-migration/after/src/test/java/com/ankurm/migration/CsrfProbeTest.java @@ -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); + } +} diff --git a/boot4-migration/after/src/test/java/com/ankurm/migration/GreetingControllerTest.java b/boot4-migration/after/src/test/java/com/ankurm/migration/GreetingControllerTest.java new file mode 100644 index 0000000..2b6cbb3 --- /dev/null +++ b/boot4-migration/after/src/test/java/com/ankurm/migration/GreetingControllerTest.java @@ -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")); + } +} diff --git a/boot4-migration/after/src/test/java/com/ankurm/migration/MockFieldTest.java b/boot4-migration/after/src/test/java/com/ankurm/migration/MockFieldTest.java new file mode 100644 index 0000000..5d82415 --- /dev/null +++ b/boot4-migration/after/src/test/java/com/ankurm/migration/MockFieldTest.java @@ -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"); + } +} diff --git a/boot4-migration/after/src/test/java/com/ankurm/migration/RuntimeFactsTest.java b/boot4-migration/after/src/test/java/com/ankurm/migration/RuntimeFactsTest.java new file mode 100644 index 0000000..35c222f --- /dev/null +++ b/boot4-migration/after/src/test/java/com/ankurm/migration/RuntimeFactsTest.java @@ -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\"}")); + } +} diff --git a/boot4-migration/before/pom.xml b/boot4-migration/before/pom.xml new file mode 100644 index 0000000..2b00929 --- /dev/null +++ b/boot4-migration/before/pom.xml @@ -0,0 +1,37 @@ + + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.16 + + + com.ankurm + migration-demo + 1.0.0 + + 25 + + + org.springframework.bootspring-boot-starter-web + org.springframework.bootspring-boot-starter-actuator + org.springframework.bootspring-boot-starter-security + org.springframework.bootspring-boot-starter-jdbc + org.springframework.bootspring-boot-starter-aop + com.h2databaseh2runtime + + org.flywaydbflyway-core + org.springframework.bootspring-boot-starter-testtest + org.springframework.securityspring-security-testtest + + + + org.springframework.bootspring-boot-maven-plugin + + + diff --git a/boot4-migration/before/src/main/java/com/ankurm/migration/DemoApplication.java b/boot4-migration/before/src/main/java/com/ankurm/migration/DemoApplication.java new file mode 100644 index 0000000..ba69bca --- /dev/null +++ b/boot4-migration/before/src/main/java/com/ankurm/migration/DemoApplication.java @@ -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); + } +} diff --git a/boot4-migration/before/src/main/java/com/ankurm/migration/Gateway.java b/boot4-migration/before/src/main/java/com/ankurm/migration/Gateway.java new file mode 100644 index 0000000..d04d5e5 --- /dev/null +++ b/boot4-migration/before/src/main/java/com/ankurm/migration/Gateway.java @@ -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(); + } +} diff --git a/boot4-migration/before/src/main/java/com/ankurm/migration/Greeting.java b/boot4-migration/before/src/main/java/com/ankurm/migration/Greeting.java new file mode 100644 index 0000000..fecf4a1 --- /dev/null +++ b/boot4-migration/before/src/main/java/com/ankurm/migration/Greeting.java @@ -0,0 +1,6 @@ +package com.ankurm.migration; + +import java.time.Instant; + +public record Greeting(long id, String title, Instant createdAt, String author) { +} diff --git a/boot4-migration/before/src/main/java/com/ankurm/migration/GreetingController.java b/boot4-migration/before/src/main/java/com/ankurm/migration/GreetingController.java new file mode 100644 index 0000000..3ba6fc9 --- /dev/null +++ b/boot4-migration/before/src/main/java/com/ankurm/migration/GreetingController.java @@ -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 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 aspect() { + return Map.of("interceptions", TimingAspect.COUNT.get()); + } + + @GetMapping("/gateway") + public Map gateway() { + return Map.of("client", gateway.describe()); + } +} diff --git a/boot4-migration/before/src/main/java/com/ankurm/migration/GreetingService.java b/boot4-migration/before/src/main/java/com/ankurm/migration/GreetingService.java new file mode 100644 index 0000000..d2fa800 --- /dev/null +++ b/boot4-migration/before/src/main/java/com/ankurm/migration/GreetingService.java @@ -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; + } +} diff --git a/boot4-migration/before/src/main/java/com/ankurm/migration/JsonConfig.java b/boot4-migration/before/src/main/java/com/ankurm/migration/JsonConfig.java new file mode 100644 index 0000000..8012e25 --- /dev/null +++ b/boot4-migration/before/src/main/java/com/ankurm/migration/JsonConfig.java @@ -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 { + @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(); + } +} diff --git a/boot4-migration/before/src/main/java/com/ankurm/migration/SecurityConfig.java b/boot4-migration/before/src/main/java/com/ankurm/migration/SecurityConfig.java new file mode 100644 index 0000000..7b14ab5 --- /dev/null +++ b/boot4-migration/before/src/main/java/com/ankurm/migration/SecurityConfig.java @@ -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(); + } +} diff --git a/boot4-migration/before/src/main/java/com/ankurm/migration/Summary.java b/boot4-migration/before/src/main/java/com/ankurm/migration/Summary.java new file mode 100644 index 0000000..79c88bf --- /dev/null +++ b/boot4-migration/before/src/main/java/com/ankurm/migration/Summary.java @@ -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; } +} diff --git a/boot4-migration/before/src/main/java/com/ankurm/migration/TimingAspect.java b/boot4-migration/before/src/main/java/com/ankurm/migration/TimingAspect.java new file mode 100644 index 0000000..22a8c48 --- /dev/null +++ b/boot4-migration/before/src/main/java/com/ankurm/migration/TimingAspect.java @@ -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(); + } +} diff --git a/boot4-migration/before/src/main/resources/application.properties b/boot4-migration/before/src/main/resources/application.properties new file mode 100644 index 0000000..9caae0e --- /dev/null +++ b/boot4-migration/before/src/main/resources/application.properties @@ -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 diff --git a/boot4-migration/before/src/main/resources/db/migration/V1__greeting.sql b/boot4-migration/before/src/main/resources/db/migration/V1__greeting.sql new file mode 100644 index 0000000..86d6781 --- /dev/null +++ b/boot4-migration/before/src/main/resources/db/migration/V1__greeting.sql @@ -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'); diff --git a/boot4-migration/before/src/test/java/com/ankurm/migration/CsrfProbeTest.java b/boot4-migration/before/src/test/java/com/ankurm/migration/CsrfProbeTest.java new file mode 100644 index 0000000..76709ca --- /dev/null +++ b/boot4-migration/before/src/test/java/com/ankurm/migration/CsrfProbeTest.java @@ -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); + } +} diff --git a/boot4-migration/before/src/test/java/com/ankurm/migration/GreetingControllerTest.java b/boot4-migration/before/src/test/java/com/ankurm/migration/GreetingControllerTest.java new file mode 100644 index 0000000..e92b6ff --- /dev/null +++ b/boot4-migration/before/src/test/java/com/ankurm/migration/GreetingControllerTest.java @@ -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")); + } +} diff --git a/boot4-migration/before/src/test/java/com/ankurm/migration/MockFieldTest.java b/boot4-migration/before/src/test/java/com/ankurm/migration/MockFieldTest.java new file mode 100644 index 0000000..ef4af41 --- /dev/null +++ b/boot4-migration/before/src/test/java/com/ankurm/migration/MockFieldTest.java @@ -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"); + } +} diff --git a/boot4-migration/output/01-before-boot35.txt b/boot4-migration/output/01-before-boot35.txt new file mode 100644 index 0000000..33529d1 --- /dev/null +++ b/boot4-migration/output/01-before-boot35.txt @@ -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...gz; (app.log is 342 bytes; limit set is logging.file.max-size=2KB or its 4.1 replacement) diff --git a/boot4-migration/output/02-bump-only-compile-errors.txt b/boot4-migration/output/02-bump-only-compile-errors.txt new file mode 100644 index 0000000..e5f0e90 --- /dev/null +++ b/boot4-migration/output/02-bump-only-compile-errors.txt @@ -0,0 +1,35 @@ +$ mvn -DskipTests package # same sources, same pom, only 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 { +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 diff --git a/boot4-migration/output/03-compiles-but-will-not-start.txt b/boot4-migration/output/03-compiles-but-will-not-start.txt new file mode 100644 index 0000000..197117c --- /dev/null +++ b/boot4-migration/output/03-compiles-but-will-not-start.txt @@ -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: diff --git a/boot4-migration/output/04-starts-but-silently-wrong.txt b/boot4-migration/output/04-starts-but-silently-wrong.txt new file mode 100644 index 0000000..8fd01c5 --- /dev/null +++ b/boot4-migration/output/04-starts-but-silently-wrong.txt @@ -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) diff --git a/boot4-migration/output/05-properties-migrator.txt b/boot4-migration/output/05-properties-migrator.txt new file mode 100644 index 0000000..66868b2 --- /dev/null +++ b/boot4-migration/output/05-properties-migrator.txt @@ -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) diff --git a/boot4-migration/output/06-after-boot41.txt b/boot4-migration/output/06-after-boot41.txt new file mode 100644 index 0000000..f97f720 --- /dev/null +++ b/boot4-migration/output/06-after-boot41.txt @@ -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...gz; (app.log is 513 bytes; limit set is logging.file.max-size=2KB or its 4.1 replacement) diff --git a/boot4-migration/output/07-jackson-property-order.txt b/boot4-migration/output/07-jackson-property-order.txt new file mode 100644 index 0000000..b22d7fb --- /dev/null +++ b/boot4-migration/output/07-jackson-property-order.txt @@ -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 diff --git a/boot4-migration/output/08-test-compile-errors.txt b/boot4-migration/output/08-test-compile-errors.txt new file mode 100644 index 0000000..587af37 --- /dev/null +++ b/boot4-migration/output/08-test-compile-errors.txt @@ -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 diff --git a/boot4-migration/output/09-test-runtime-failure.txt b/boot4-migration/output/09-test-runtime-failure.txt new file mode 100644 index 0000000..fe1311a --- /dev/null +++ b/boot4-migration/output/09-test-runtime-failure.txt @@ -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 +[ERROR] Tests run: 2, Failures: 1, Errors: 0, Skipped: 0 +[INFO] BUILD FAILURE diff --git a/boot4-migration/output/10-security-dsl-compile-errors.txt b/boot4-migration/output/10-security-dsl-compile-errors.txt new file mode 100644 index 0000000..8de0a87 --- /dev/null +++ b/boot4-migration/output/10-security-dsl-compile-errors.txt @@ -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.AuthorizationManagerRequestMatcherRegistry> + found: no arguments + reason: actual and formal argument lists differ in length +1 error diff --git a/boot4-migration/scripts/run-all.sh b/boot4-migration/scripts/run-all.sh new file mode 100755 index 0000000..cccf1c3 --- /dev/null +++ b/boot4-migration/scripts/run-all.sh @@ -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#$BOOT_OLD#$BOOT_NEW#" "$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