diff --git a/openrewrite/README.md b/openrewrite/README.md new file mode 100644 index 0000000..251fa3d --- /dev/null +++ b/openrewrite/README.md @@ -0,0 +1,27 @@ +# openrewrite + +Companion module for the ankurm.com post *Automating javax → jakarta and Boot 3 → 4 Upgrades with OpenRewrite*. + +`legacy-app/` is a small Spring Boot 2.7.18 application (JPA, validation, security, a servlet filter, `RestTemplate`, a JUnit 4 test, a `spring.factories` auto-configuration). `scripts/run-all.sh` copies it to a scratch directory outside this checkout, runs two OpenRewrite recipes in sequence, builds and tests after each, and records everything under `output/`. + +| Tool | Version | +|---|---| +| rewrite-maven-plugin | 6.46.1 | +| rewrite-spring | 6.37.1 | +| rewrite-migrate-java | 3.42.1 | +| JDK / Maven | 17 / 3.9 | + +```bash +export JAVA_HOME=/path/to/jdk17 +./scripts/run-all.sh +``` + +| Output | Shows | +|---|---| +| `00-gotcha-gitignore.txt` | OpenRewrite silently skips sources that git ignores | +| `01-baseline.txt` | Legacy app builds and passes on Boot 2.7.18 | +| `02-step1-diff.txt` / `03-step1-build.txt` | `UpgradeSpringBoot_3_5`: diff and build | +| `04-step2-diff.txt` / `05-step2-build.txt` | `UpgradeSpringBoot_4_0`: diff and build | +| `06-what-it-left-behind.txt` | What the recipes did not change | +| `07-step3-boot-4.1.1.txt` | Hand bump from 4.0.x to 4.1.1, build and test | +| `08-ci-gate.txt` | `rewrite:dryRun` exit codes, including a property-name trap | diff --git a/openrewrite/legacy-app/pom.xml b/openrewrite/legacy-app/pom.xml new file mode 100644 index 0000000..492b954 --- /dev/null +++ b/openrewrite/legacy-app/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + com.ankurm + legacy-app + 1.0.0 + + 17 + + + org.springframework.bootspring-boot-starter-web + org.springframework.bootspring-boot-starter-data-jpa + org.springframework.bootspring-boot-starter-validation + org.springframework.bootspring-boot-starter-security + org.springframework.bootspring-boot-starter-actuator + com.h2databaseh2runtime + org.springframework.bootspring-boot-starter-testtest + org.junit.vintagejunit-vintage-enginetest + org.springframework.securityspring-security-testtest + + + + org.springframework.bootspring-boot-maven-plugin + + + diff --git a/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java new file mode 100644 index 0000000..e578260 --- /dev/null +++ b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java @@ -0,0 +1,7 @@ +package com.ankurm.legacy; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AuditAutoConfiguration { +} diff --git a/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/Customer.java b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/Customer.java new file mode 100644 index 0000000..2942388 --- /dev/null +++ b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/Customer.java @@ -0,0 +1,26 @@ +package com.ankurm.legacy; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + +@Entity +public class Customer { + @Id + @GeneratedValue + private Long id; + @NotBlank + private String name; + @Email + private String email; + + protected Customer() {} + public Customer(String name, String email) { this.name = name; this.email = email; } + public Long getId() { return id; } + public String getName() { return name; } + public String getEmail() { return email; } + public void setName(String name) { this.name = name; } + public void setEmail(String email) { this.email = email; } +} diff --git a/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/CustomerController.java b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/CustomerController.java new file mode 100644 index 0000000..88e9b91 --- /dev/null +++ b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/CustomerController.java @@ -0,0 +1,28 @@ +package com.ankurm.legacy; + +import java.util.List; +import javax.annotation.PostConstruct; +import javax.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/customers") +public class CustomerController { + + private final CustomerRepository repository; + + public CustomerController(CustomerRepository repository) { this.repository = repository; } + + @PostConstruct + void seed() { + repository.save(new Customer("Ada", "ada@example.com")); + } + + @GetMapping + List all() { return repository.findAll(); } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + Customer create(@Valid @RequestBody Customer c) { return repository.save(c); } +} diff --git a/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/CustomerRepository.java b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/CustomerRepository.java new file mode 100644 index 0000000..09695e1 --- /dev/null +++ b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/CustomerRepository.java @@ -0,0 +1,14 @@ +package com.ankurm.legacy; + +import javax.persistence.LockModeType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.QueryHints; +import javax.persistence.QueryHint; + +public interface CustomerRepository extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints(@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")) + Customer findFirstByName(String name); +} diff --git a/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/LegacyApplication.java b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/LegacyApplication.java new file mode 100644 index 0000000..9dbd00c --- /dev/null +++ b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/LegacyApplication.java @@ -0,0 +1,11 @@ +package com.ankurm.legacy; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LegacyApplication { + public static void main(String[] args) { + SpringApplication.run(LegacyApplication.class, args); + } +} diff --git a/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/RemoteClient.java b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/RemoteClient.java new file mode 100644 index 0000000..7c79947 --- /dev/null +++ b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/RemoteClient.java @@ -0,0 +1,13 @@ +package com.ankurm.legacy; + +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class RemoteClient { + private final RestTemplate rest = new RestTemplate(); + + public String fetch(String url) { + return rest.getForObject(url, String.class); + } +} diff --git a/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/RequestLogFilter.java b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/RequestLogFilter.java new file mode 100644 index 0000000..8fc169e --- /dev/null +++ b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/RequestLogFilter.java @@ -0,0 +1,19 @@ +package com.ankurm.legacy; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +public class RequestLogFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + response.setHeader("X-Seen-By", "RequestLogFilter"); + chain.doFilter(request, response); + } +} diff --git a/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/SecurityConfig.java b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/SecurityConfig.java new file mode 100644 index 0000000..0127fe8 --- /dev/null +++ b/openrewrite/legacy-app/src/main/java/com/ankurm/legacy/SecurityConfig.java @@ -0,0 +1,17 @@ +package com.ankurm.legacy; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .authorizeRequests().antMatchers("/actuator/health").permitAll() + .anyRequest().permitAll(); + } +} diff --git a/openrewrite/legacy-app/src/main/resources/META-INF/spring.factories b/openrewrite/legacy-app/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..03563db --- /dev/null +++ b/openrewrite/legacy-app/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ankurm.legacy.AuditAutoConfiguration diff --git a/openrewrite/legacy-app/src/main/resources/application.properties b/openrewrite/legacy-app/src/main/resources/application.properties new file mode 100644 index 0000000..7745c02 --- /dev/null +++ b/openrewrite/legacy-app/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.redis.host=localhost +spring.redis.port=6379 +management.metrics.export.prometheus.enabled=true +spring.jpa.hibernate.ddl-auto=create-drop +server.max-http-header-size=16KB +spring.jpa.properties.javax.persistence.validation.mode=none diff --git a/openrewrite/legacy-app/src/test/java/com/ankurm/legacy/CustomerControllerTest.java b/openrewrite/legacy-app/src/test/java/com/ankurm/legacy/CustomerControllerTest.java new file mode 100644 index 0000000..8771ce5 --- /dev/null +++ b/openrewrite/legacy-app/src/test/java/com/ankurm/legacy/CustomerControllerTest.java @@ -0,0 +1,26 @@ +package com.ankurm.legacy; + +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.Test; +import org.junit.runner.RunWith; +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.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +public class CustomerControllerTest { + + @Autowired MockMvc mvc; + + @Test + public void listsSeededCustomer() throws Exception { + mvc.perform(get("/customers")).andExpect(status().isOk()).andExpect(jsonPath("$[0].name").value("Ada")); + } +} diff --git a/openrewrite/output/00-gotcha-gitignore.txt b/openrewrite/output/00-gotcha-gitignore.txt new file mode 100644 index 0000000..dd5742f --- /dev/null +++ b/openrewrite/output/00-gotcha-gitignore.txt @@ -0,0 +1,3 @@ +# 00-gotcha-gitignore: same recipe, same project, sources under a .gitignore'd directory +[INFO] Project [legacy-app] Parsing source files +[INFO] Applying recipes would make no changes. No patch file generated. diff --git a/openrewrite/output/01-baseline.txt b/openrewrite/output/01-baseline.txt new file mode 100644 index 0000000..652232c --- /dev/null +++ b/openrewrite/output/01-baseline.txt @@ -0,0 +1,3 @@ +# 01-baseline: legacy app, Spring Boot 2.7.18, JDK 17 +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +[INFO] BUILD SUCCESS diff --git a/openrewrite/output/02-step1-diff.txt b/openrewrite/output/02-step1-diff.txt new file mode 100644 index 0000000..c4e73fd --- /dev/null +++ b/openrewrite/output/02-step1-diff.txt @@ -0,0 +1,173 @@ +# 02-step1-diff: recipe org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5 on the legacy app (52 s) +diff -ru -x target legacy-app/pom.xml step1/pom.xml +--- legacy-app/pom.xml ++++ step1/pom.xml +@@ -4,7 +4,7 @@ + + org.springframework.boot + spring-boot-starter-parent +- 2.7.18 ++ 3.5.16 + + + com.ankurm +@@ -14,6 +14,10 @@ + 17 + + ++ ++ jakarta.servlet ++ jakarta.servlet-api ++ + org.springframework.bootspring-boot-starter-web + org.springframework.bootspring-boot-starter-data-jpa + org.springframework.bootspring-boot-starter-validation +@@ -21,7 +25,6 @@ + org.springframework.bootspring-boot-starter-actuator + com.h2databaseh2runtime + org.springframework.bootspring-boot-starter-testtest +- org.junit.vintagejunit-vintage-enginetest + org.springframework.securityspring-security-testtest + + +diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java step1/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java +--- legacy-app/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java ++++ step1/src/main/java/com/ankurm/legacy/AuditAutoConfiguration.java +@@ -1,7 +1,7 @@ + package com.ankurm.legacy; + +-import org.springframework.context.annotation.Configuration; ++import org.springframework.boot.autoconfigure.AutoConfiguration; + +-@Configuration ++@AutoConfiguration + public class AuditAutoConfiguration { + } +diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/Customer.java step1/src/main/java/com/ankurm/legacy/Customer.java +--- legacy-app/src/main/java/com/ankurm/legacy/Customer.java ++++ step1/src/main/java/com/ankurm/legacy/Customer.java +@@ -1,10 +1,10 @@ + package com.ankurm.legacy; + +-import javax.persistence.Entity; +-import javax.persistence.GeneratedValue; +-import javax.persistence.Id; +-import javax.validation.constraints.Email; +-import javax.validation.constraints.NotBlank; ++import jakarta.persistence.Entity; ++import jakarta.persistence.GeneratedValue; ++import jakarta.persistence.Id; ++import jakarta.validation.constraints.Email; ++import jakarta.validation.constraints.NotBlank; + + @Entity + public class Customer { +diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/CustomerController.java step1/src/main/java/com/ankurm/legacy/CustomerController.java +--- legacy-app/src/main/java/com/ankurm/legacy/CustomerController.java ++++ step1/src/main/java/com/ankurm/legacy/CustomerController.java +@@ -1,8 +1,8 @@ + package com.ankurm.legacy; + + import java.util.List; +-import javax.annotation.PostConstruct; +-import javax.validation.Valid; ++import jakarta.annotation.PostConstruct; ++import jakarta.validation.Valid; + import org.springframework.http.HttpStatus; + import org.springframework.web.bind.annotation.*; + +diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/CustomerRepository.java step1/src/main/java/com/ankurm/legacy/CustomerRepository.java +--- legacy-app/src/main/java/com/ankurm/legacy/CustomerRepository.java ++++ step1/src/main/java/com/ankurm/legacy/CustomerRepository.java +@@ -1,10 +1,10 @@ + package com.ankurm.legacy; + +-import javax.persistence.LockModeType; ++import jakarta.persistence.LockModeType; + import org.springframework.data.jpa.repository.JpaRepository; + import org.springframework.data.jpa.repository.Lock; + import org.springframework.data.jpa.repository.QueryHints; +-import javax.persistence.QueryHint; ++import jakarta.persistence.QueryHint; + + public interface CustomerRepository extends JpaRepository { + +diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/RequestLogFilter.java step1/src/main/java/com/ankurm/legacy/RequestLogFilter.java +--- legacy-app/src/main/java/com/ankurm/legacy/RequestLogFilter.java ++++ step1/src/main/java/com/ankurm/legacy/RequestLogFilter.java +@@ -1,10 +1,10 @@ + package com.ankurm.legacy; + + import java.io.IOException; +-import javax.servlet.FilterChain; +-import javax.servlet.ServletException; +-import javax.servlet.http.HttpServletRequest; +-import javax.servlet.http.HttpServletResponse; ++import jakarta.servlet.FilterChain; ++import jakarta.servlet.ServletException; ++import jakarta.servlet.http.HttpServletRequest; ++import jakarta.servlet.http.HttpServletResponse; + import org.springframework.stereotype.Component; + import org.springframework.web.filter.OncePerRequestFilter; + +diff -ru -x target legacy-app/src/main/java/com/ankurm/legacy/SecurityConfig.java step1/src/main/java/com/ankurm/legacy/SecurityConfig.java +--- legacy-app/src/main/java/com/ankurm/legacy/SecurityConfig.java ++++ step1/src/main/java/com/ankurm/legacy/SecurityConfig.java +@@ -1,17 +1,19 @@ + package com.ankurm.legacy; + ++import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.Configuration; + import org.springframework.security.config.annotation.web.builders.HttpSecurity; + import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; ++import org.springframework.security.web.SecurityFilterChain; + + @Configuration + @EnableWebSecurity +-public class SecurityConfig extends WebSecurityConfigurerAdapter { +- @Override +- protected void configure(HttpSecurity http) throws Exception { +- http.csrf().disable() +- .authorizeRequests().antMatchers("/actuator/health").permitAll() +- .anyRequest().permitAll(); ++public class SecurityConfig { ++ @Bean ++ SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ++ http.csrf(csrf -> csrf.disable()) ++ .authorizeHttpRequests(requests -> requests.requestMatchers("/actuator/health").permitAll() ++ .anyRequest().permitAll()); ++ return http.build(); + } + } +Only in step1/src/main/resources/META-INF: spring +diff -ru -x target legacy-app/src/main/resources/application.properties step1/src/main/resources/application.properties +--- legacy-app/src/main/resources/application.properties ++++ step1/src/main/resources/application.properties +@@ -3,4 +3,4 @@ + management.metrics.export.prometheus.enabled=true + spring.jpa.hibernate.ddl-auto=create-drop + server.max-http-header-size=16KB +-spring.jpa.properties.javax.persistence.validation.mode=none ++spring.jpa.properties.jakarta.persistence.validation.mode=none +diff -ru -x target legacy-app/src/test/java/com/ankurm/legacy/CustomerControllerTest.java step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java +--- legacy-app/src/test/java/com/ankurm/legacy/CustomerControllerTest.java ++++ step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java +@@ -4,15 +4,12 @@ + import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +-import org.junit.Test; +-import org.junit.runner.RunWith; ++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.context.junit4.SpringRunner; + import org.springframework.test.web.servlet.MockMvc; + +-@RunWith(SpringRunner.class) + @SpringBootTest + @AutoConfigureMockMvc + public class CustomerControllerTest { +Only in step1/src/main/resources/META-INF: spring diff --git a/openrewrite/output/03-step1-build.txt b/openrewrite/output/03-step1-build.txt new file mode 100644 index 0000000..8cf7e6b --- /dev/null +++ b/openrewrite/output/03-step1-build.txt @@ -0,0 +1,3 @@ +# 03-step1-build: after step 1, JDK 17 +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +[INFO] BUILD SUCCESS diff --git a/openrewrite/output/04-step2-diff.txt b/openrewrite/output/04-step2-diff.txt new file mode 100644 index 0000000..3b245c7 --- /dev/null +++ b/openrewrite/output/04-step2-diff.txt @@ -0,0 +1,65 @@ +# 04-step2-diff: recipe org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0 applied on top of step 1 (32 s) +diff -ru -x target step1/pom.xml step2/pom.xml +--- step1/pom.xml ++++ step2/pom.xml +@@ -4,7 +4,7 @@ + + org.springframework.boot + spring-boot-starter-parent +- 3.5.16 ++ 4.0.8 + + + com.ankurm +@@ -18,14 +18,23 @@ + jakarta.servlet + jakarta.servlet-api + +- org.springframework.bootspring-boot-starter-web ++ org.springframework.bootspring-boot-starter-webmvc + org.springframework.bootspring-boot-starter-data-jpa ++ ++ org.springframework.boot ++ spring-boot-starter-restclient ++ + org.springframework.bootspring-boot-starter-validation + org.springframework.bootspring-boot-starter-security + org.springframework.bootspring-boot-starter-actuator + com.h2databaseh2runtime + org.springframework.bootspring-boot-starter-testtest +- org.springframework.securityspring-security-testtest ++ ++ org.springframework.boot ++ spring-boot-starter-webmvc-test ++ test ++ ++ org.springframework.bootspring-boot-starter-security-testtest + + + +diff -ru -x target step1/src/main/resources/application.properties step2/src/main/resources/application.properties +--- step1/src/main/resources/application.properties ++++ step2/src/main/resources/application.properties +@@ -1,6 +1,6 @@ +-spring.redis.host=localhost +-spring.redis.port=6379 +-management.metrics.export.prometheus.enabled=true ++spring.data.redis.host=localhost ++spring.data.redis.port=6379 ++management.prometheus.metrics.export.enabled=true + spring.jpa.hibernate.ddl-auto=create-drop +-server.max-http-header-size=16KB ++server.max-http-request-header-size=16KB + spring.jpa.properties.jakarta.persistence.validation.mode=none +diff -ru -x target step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java step2/src/test/java/com/ankurm/legacy/CustomerControllerTest.java +--- step1/src/test/java/com/ankurm/legacy/CustomerControllerTest.java ++++ step2/src/test/java/com/ankurm/legacy/CustomerControllerTest.java +@@ -6,7 +6,7 @@ + + 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.webmvc.test.autoconfigure.AutoConfigureMockMvc; + import org.springframework.boot.test.context.SpringBootTest; + import org.springframework.test.web.servlet.MockMvc; + diff --git a/openrewrite/output/05-step2-build.txt b/openrewrite/output/05-step2-build.txt new file mode 100644 index 0000000..926d958 --- /dev/null +++ b/openrewrite/output/05-step2-build.txt @@ -0,0 +1,3 @@ +# 05-step2-build: after step 2, JDK 17 +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +[INFO] BUILD SUCCESS diff --git a/openrewrite/output/06-what-it-left-behind.txt b/openrewrite/output/06-what-it-left-behind.txt new file mode 100644 index 0000000..39e1368 --- /dev/null +++ b/openrewrite/output/06-what-it-left-behind.txt @@ -0,0 +1,13 @@ +# 06-what-it-left-behind (in /tmp/or-work/step2, after both recipes) +--- leftover 'javax.' string literals or keys in source and resources: +src/main/java/com/ankurm/legacy/CustomerRepository.java:12: @QueryHints(@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")) +--- META-INF files: +./spring.factories +./spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +--- explicit jakarta.servlet-api dependency added to the pom: +1 +--- RestTemplate usages (recipe does not touch them): +src/main/java/com/ankurm/legacy/RemoteClient.java:4:import org.springframework.web.client.RestTemplate; +src/main/java/com/ankurm/legacy/RemoteClient.java:8: private final RestTemplate rest = new RestTemplate(); +--- Spring Boot version the recipe stopped at: + 4.0.8 diff --git a/openrewrite/output/07-step3-boot-4.1.1.txt b/openrewrite/output/07-step3-boot-4.1.1.txt new file mode 100644 index 0000000..f964c31 --- /dev/null +++ b/openrewrite/output/07-step3-boot-4.1.1.txt @@ -0,0 +1,4 @@ +# 07-step3-boot-4.1.1: parent bumped by hand from the recipe's 4.0.x to 4.1.1, JDK 17 + 4.1.1 +[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +[INFO] BUILD SUCCESS diff --git a/openrewrite/output/08-ci-gate.txt b/openrewrite/output/08-ci-gate.txt new file mode 100644 index 0000000..b154ade --- /dev/null +++ b/openrewrite/output/08-ci-gate.txt @@ -0,0 +1,4 @@ +# 08-ci-gate (mvn rewrite:dryRun, exit code decides the CI job) +legacy app, -Drewrite.failOnDryRunResults=true (the name you would guess): exit code 0 +legacy app, -DfailOnDryRunResults=true (the real user property): exit code 1 +migrated app, -DfailOnDryRunResults=true: exit code 0 diff --git a/openrewrite/scripts/rewrite.sh b/openrewrite/scripts/rewrite.sh new file mode 100755 index 0000000..5df63b6 --- /dev/null +++ b/openrewrite/scripts/rewrite.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# usage: rewrite.sh (copies from-dir to to-dir and runs the recipe there) +set -uo pipefail +RW=6.46.1; SPRING=6.37.1; MIGRATE=3.42.1 +rm -rf "$2"; mkdir -p "$(dirname "$2")"; cp -r "$1" "$2"; rm -rf "$2/target" +cd "$2" +mvn -B -q org.openrewrite.maven:rewrite-maven-plugin:$RW:run \ + -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:$SPRING,org.openrewrite.recipe:rewrite-migrate-java:$MIGRATE \ + -Drewrite.activeRecipes="$3" -Drewrite.exportDatatables=false 2>&1 diff --git a/openrewrite/scripts/run-all.sh b/openrewrite/scripts/run-all.sh new file mode 100755 index 0000000..814cd11 --- /dev/null +++ b/openrewrite/scripts/run-all.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Regenerates output/. Needs JDK 17 on JAVA_HOME (the legacy app targets 17), Maven 3.9, network access to Maven Central. +# Work happens in $WORK (default /tmp/or-work) OUTSIDE this git checkout, because OpenRewrite honours .gitignore +# and ignores files git ignores (see output/00-gotcha-gitignore.txt). +set -uo pipefail +cd "$(dirname "$0")/.." +WORK=${WORK:-/tmp/or-work}; OUT=$PWD/output; mkdir -p "$OUT" +R1=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5 +R2=org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0 +tests() { (cd "$1" && mvn -B test 2>&1 | grep -E "Tests run:.*Fail|BUILD|ERROR" | grep -v "Time elapsed" | head -${2:-8}); } +difff() { diff -ru -x target "$1" "$2" | sed -E 's/\t20[0-9-]+ [0-9:.]+ \+0000$//; s#'"$WORK"'/##g'; } + +{ echo "# 01-baseline: legacy app, Spring Boot 2.7.18, JDK 17"; tests legacy-app; } > "$OUT/01-baseline.txt" + +s=$(date +%s); ./scripts/rewrite.sh legacy-app "$WORK/step1" $R1 >/dev/null 2>&1; e=$(date +%s) +{ echo "# 02-step1-diff: recipe $R1 on the legacy app ($((e-s)) s)"; difff legacy-app "$WORK/step1"; diff -rq legacy-app "$WORK/step1" -x target | grep '^Only in' | sed "s#$WORK/##"; } > "$OUT/02-step1-diff.txt" +{ echo "# 03-step1-build: after step 1, JDK 17"; tests "$WORK/step1"; } > "$OUT/03-step1-build.txt" + +s=$(date +%s); ./scripts/rewrite.sh "$WORK/step1" "$WORK/step2" $R2 >/dev/null 2>&1; e=$(date +%s) +{ echo "# 04-step2-diff: recipe $R2 applied on top of step 1 ($((e-s)) s)"; difff "$WORK/step1" "$WORK/step2"; } > "$OUT/04-step2-diff.txt" +{ echo "# 05-step2-build: after step 2, JDK 17"; tests "$WORK/step2"; } > "$OUT/05-step2-build.txt" + +{ + echo "# 06-what-it-left-behind (in $WORK/step2, after both recipes)" + echo "--- leftover 'javax.' string literals or keys in source and resources:" + grep -rn 'javax\.' "$WORK/step2/src" | sed "s#$WORK/step2/##" || true + echo "--- META-INF files:"; (cd "$WORK/step2/src/main/resources/META-INF" && find . -type f | sort) + echo "--- explicit jakarta.servlet-api dependency added to the pom:"; grep -c 'jakarta.servlet-api' "$WORK/step2/pom.xml" + echo "--- RestTemplate usages (recipe does not touch them):"; grep -rn 'RestTemplate' "$WORK/step2/src" | sed "s#$WORK/step2/##" + echo "--- Spring Boot version the recipe stopped at:"; grep -A1 'spring-boot-starter-parent' "$WORK/step2/pom.xml" | grep version +} > "$OUT/06-what-it-left-behind.txt" + +rm -rf "$WORK/step3"; cp -r "$WORK/step2" "$WORK/step3"; rm -rf "$WORK/step3/target" +sed -i 's#4.0.[0-9]*#4.1.1#' "$WORK/step3/pom.xml" +{ echo "# 07-step3-boot-4.1.1: parent bumped by hand from the recipe's 4.0.x to 4.1.1, JDK 17"; grep -A1 'spring-boot-starter-parent' "$WORK/step3/pom.xml" | grep version; tests "$WORK/step3"; } > "$OUT/07-step3-boot-4.1.1.txt" + +# CI gate: dryRun with failOnDryRunResults. Fails on the legacy app, passes once migrated. +gate() { (cd "$1" && rm -rf target && mvn -B -q org.openrewrite.maven:rewrite-maven-plugin:6.46.1:dryRun \ + -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:6.37.1,org.openrewrite.recipe:rewrite-migrate-java:3.42.1 \ + -Drewrite.activeRecipes="$2" -D"$3"=true >/dev/null 2>&1; echo "exit code $?"); } + +rm -rf "$WORK/g1" "$WORK/g2"; cp -r legacy-app "$WORK/g1"; cp -r "$WORK/step2" "$WORK/g2" +{ echo "# 08-ci-gate (mvn rewrite:dryRun, exit code decides the CI job)" + echo "legacy app, -Drewrite.failOnDryRunResults=true (the name you would guess): $(gate "$WORK/g1" $R1 rewrite.failOnDryRunResults)" + echo "legacy app, -DfailOnDryRunResults=true (the real user property): $(gate "$WORK/g1" $R1 failOnDryRunResults)" + echo "migrated app, -DfailOnDryRunResults=true: $(gate "$WORK/g2" $R1 failOnDryRunResults)"; } > "$OUT/08-ci-gate.txt" + +{ echo "# 00-gotcha-gitignore: same recipe, same project, sources under a .gitignore'd directory" + rm -rf "$WORK/ign"; mkdir -p "$WORK/ign"; (cd "$WORK/ign" && git init -q && echo 'legacy/' > .gitignore && cp -r "$OLDPWD/legacy-app" legacy && cd legacy \ + && mvn -B org.openrewrite.maven:rewrite-maven-plugin:6.46.1:dryRun -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:6.37.1,org.openrewrite.recipe:rewrite-migrate-java:3.42.1 -Drewrite.activeRecipes=$R1 2>&1 | grep -E "Applying recipes|Parsing|patch file" ) +} > "$OUT/00-gotcha-gitignore.txt" +ls -1 "$OUT"