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