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:
@@ -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\"}"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user