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:
@@ -50,6 +50,7 @@ files.
|
|||||||
| [`crud-mysql/`](crud-mysql) | [Spring Boot CRUD REST API with MySQL and Spring Data JPA](https://ankurm.com/spring-boot-crud-rest-api-mysql/) | a complete book API on a real MySQL 8.4.6 server with Flyway owning the schema and Hibernate set to `validate`, `ProblemDetail` errors with per-field validation messages, a duplicate ISBN caught by the database's unique key and mapped to `409` on create and on update, MySQL's own `Duplicate entry` message, and the SQL behind every request |
|
| [`crud-mysql/`](crud-mysql) | [Spring Boot CRUD REST API with MySQL and Spring Data JPA](https://ankurm.com/spring-boot-crud-rest-api-mysql/) | a complete book API on a real MySQL 8.4.6 server with Flyway owning the schema and Hibernate set to `validate`, `ProblemDetail` errors with per-field validation messages, a duplicate ISBN caught by the database's unique key and mapped to `409` on create and on update, MySQL's own `Duplicate entry` message, and the SQL behind every request |
|
||||||
| [`boot42-rc1-checklist/`](boot42-rc1-checklist) | [Spring Framework 7.1 and Spring Boot 4.2: What's Coming and How to Prepare](https://ankurm.com/spring-boot-4-2-spring-framework-7-1-upgrade-checklist/) | each 4.2 milestone change run on 4.2.0-RC1 and on 4.1.2: `@DefaultValue` placeholders resolved, five new Kafka admin keys that 4.1.2 silently ignores, embedded LDAP SSL failing without a bundle, the properties migrator's real report (and the removed key it misses), and the `RestTemplate` removal warnings from the compiler |
|
| [`boot42-rc1-checklist/`](boot42-rc1-checklist) | [Spring Framework 7.1 and Spring Boot 4.2: What's Coming and How to Prepare](https://ankurm.com/spring-boot-4-2-spring-framework-7-1-upgrade-checklist/) | each 4.2 milestone change run on 4.2.0-RC1 and on 4.1.2: `@DefaultValue` placeholders resolved, five new Kafka admin keys that 4.1.2 silently ignores, embedded LDAP SSL failing without a bundle, the properties migrator's real report (and the removed key it misses), and the `RestTemplate` removal warnings from the compiler |
|
||||||
| [`boot42-otel/`](boot42-otel) | [Spring Boot 4.2 OpenTelemetry: Semantic Conventions, Shared OTLP Settings and MDC Keys](https://ankurm.com/spring-boot-4-2-opentelemetry-semantic-conventions-shared-otlp-mdc/) | one request measured under each `management.observations.conventions` value (the documented `open-telemetry` fails to start on RC1, `opentelemetry` works), meter, span and Prometheus names before and after, `management.opentelemetry.otlp.*` verified against a local OTLP receiver (paths, merged headers, gzip, per-signal overrides), custom MDC keys, and the LongTaskTimer that is no longer on by default |
|
| [`boot42-otel/`](boot42-otel) | [Spring Boot 4.2 OpenTelemetry: Semantic Conventions, Shared OTLP Settings and MDC Keys](https://ankurm.com/spring-boot-4-2-opentelemetry-semantic-conventions-shared-otlp-mdc/) | one request measured under each `management.observations.conventions` value (the documented `open-telemetry` fails to start on RC1, `opentelemetry` works), meter, span and Prometheus names before and after, `management.opentelemetry.otlp.*` verified against a local OTLP receiver (paths, merged headers, gzip, per-signal overrides), custom MDC keys, and the LongTaskTimer that is no longer on by default |
|
||||||
|
| [`boot4-migration/`](boot4-migration) | [Spring Boot 3 to 4 Migration Guide (Updated for 4.1 and 4.2)](https://ankurm.com/spring-boot-3-to-4-migration-guide/) | one Boot 3.5.16 service migrated to 4.1.2 with the real failures captured at each step: 12 javac errors from a version bump, a start-up failure from a missing `spring-boot-starter-restclient`, Flyway silently not running without `spring-boot-starter-flyway`, removed `logging.file.*` keys silently ignored, Jackson 3 sorting bean properties, liveness/readiness groups appearing, and a `@Mock` field that is `null` without `MockitoExtension` |
|
||||||
|
|
||||||
`core-di`, `core-beans`, `custom-starter`, `core-events`, `multi-datasource`, `i18n`, `aot-cache`, `kotlin`, `testing-no-containers`, `rest-test-client` and `hexagonal` are the exception: they have **no `docs/NN-topic.md` chapter folder**. Their deeper material lives in
|
`core-di`, `core-beans`, `custom-starter`, `core-events`, `multi-datasource`, `i18n`, `aot-cache`, `kotlin`, `testing-no-containers`, `rest-test-client` and `hexagonal` are the exception: they have **no `docs/NN-topic.md` chapter folder**. Their deeper material lives in
|
||||||
collapsible sections inside the articles themselves. Where their captured output sits is not
|
collapsible sections inside the articles themselves. Where their captured output sits is not
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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\"}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
Executable
+131
@@ -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"
|
||||||
Executable
+11
@@ -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"
|
||||||
Reference in New Issue
Block a user