<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Istvan Verhas</title>
    <description>The latest articles on DEV Community by Istvan Verhas (@verhasi).</description>
    <link>https://dev.to/verhasi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024500%2Fc8de53ed-9bb0-4a43-9b6a-eed4433ffaa4.jpg</url>
      <title>DEV Community: Istvan Verhas</title>
      <link>https://dev.to/verhasi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/verhasi"/>
    <language>en</language>
    <item>
      <title>Simplify Java and Spring Boot Migration with OpenRewrite (2026 Refresh)</title>
      <dc:creator>Istvan Verhas</dc:creator>
      <pubDate>Fri, 25 Sep 2026 11:40:04 +0000</pubDate>
      <link>https://dev.to/verhasi/simplify-java-and-spring-boot-migration-with-openrewrite-2026-refresh-6eb</link>
      <guid>https://dev.to/verhasi/simplify-java-and-spring-boot-migration-with-openrewrite-2026-refresh-6eb</guid>
      <description>&lt;h1&gt;
  
  
  How This Post Was Created
&lt;/h1&gt;

&lt;p&gt;There's a delightful irony at the heart of this article.&lt;/p&gt;

&lt;p&gt;Back in March 2025, &lt;strong&gt;&lt;a href="https://dev.to/hgky95"&gt;Ky Huynh&lt;/a&gt;&lt;/strong&gt; wrote an excellent post, &lt;a href="https://dev.to/hgky95/simplify-java-and-springboot-migration-with-openrewrite-g3d"&gt;&lt;em&gt;Simplify Java and SpringBoot migration with OpenRewrite&lt;/em&gt;&lt;/a&gt;, showing how to jump a legacy app from Java 8 / Spring Boot 2.x to Java 21 / Spring Boot 3.3. &lt;strong&gt;Huge kudos to Ky Huynh — the original article is genuinely great, and everything below stands on the shoulders of that work: the sample codebase, the structure, and the clear step-by-step walkthrough are all his.&lt;/strong&gt; But it's now over a year and a half old, and in the fast-moving Java world that's practically a geological era. The target it recommended — &lt;strong&gt;Java 21 and Spring Boot 3.3&lt;/strong&gt; — is itself already legacy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java 25&lt;/strong&gt; is the current LTS (released September 2025). Java 21 stops receiving free public updates in September 2026.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spring Boot 4.1&lt;/strong&gt; is out (June 2026), and the entire Spring Boot 3.x line left OSS support in June 2026.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, the post that taught us how to escape end-of-life software has itself aged into end-of-life advice. OpenRewrite can rewrite your code, but there's no recipe to rewrite a blog post — so I asked an AI coding agent (Kiro CLI) to do it. It fetched the original article, checked the current versions of Java, Spring Boot, and every OpenRewrite module, discovered a brand-new distribution wrinkle (more on that below), and then — crucially — &lt;strong&gt;actually re-ran the whole migration&lt;/strong&gt; on Ky's original sample project: cloning it, confirming the Spring Boot 2.7 / Java 8 baseline still builds, applying the new recipes to reach Java 25 / Spring Boot 4 / JUnit 6, and verifying the tests still pass. Every screenshot below is real output from that run, not a mock-up.&lt;/p&gt;

&lt;p&gt;Consider this the "human-in-the-loop recipe" for keeping content current. Now, on to the actual migration.&lt;/p&gt;

&lt;h1&gt;
  
  
  Challenges of Migration
&lt;/h1&gt;

&lt;p&gt;Older versions like Spring Boot 2.x and 3.x have reached &lt;a href="https://endoflife.date/spring-boot" rel="noopener noreferrer"&gt;end-of-life&lt;/a&gt; and no longer receive OSS support, so migrating to newer versions is essential for security, compatibility, and performance. The process still comes with the same familiar challenges:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Breaking Changes:&lt;/strong&gt; Major upgrades introduce breaking changes. Spring Boot 3.x already required &lt;strong&gt;Java 17&lt;/strong&gt; and the move from &lt;code&gt;javax.*&lt;/code&gt; to &lt;code&gt;jakarta.*&lt;/code&gt;. Spring Boot 4.x raises the bar again, requiring &lt;strong&gt;Java 17+&lt;/strong&gt; (with Java 21/25 recommended) and building on Spring Framework 7.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Deprecated APIs:&lt;/strong&gt; Commonly used APIs and patterns get deprecated and need replacements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Manual Updates:&lt;/strong&gt; Traditional migration means manually bumping dependencies, refactoring code, and fixing compatibility issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Time-Consuming:&lt;/strong&gt; Large codebases can take weeks or months, increasing project cost and risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Testing Burden:&lt;/strong&gt; Every change must be thoroughly tested to ensure functionality is intact.&lt;/p&gt;

&lt;p&gt;So how do we simplify and accelerate this? This is where OpenRewrite comes in.&lt;/p&gt;

&lt;h1&gt;
  
  
  OpenRewrite
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://docs.openrewrite.org/" rel="noopener noreferrer"&gt;OpenRewrite&lt;/a&gt; is an open-source tool for automated, large-scale code refactoring that helps teams pay down technical debt. It provides prebuilt refactoring recipes for framework migrations, security fixes, and code styling, cutting effort from hours (or weeks) to minutes.&lt;/p&gt;

&lt;p&gt;Plugins for Maven and Gradle make it easy to apply these changes across repositories. Originally focused on Java, the community keeps expanding support to more languages and frameworks.&lt;/p&gt;

&lt;p&gt;Key Features&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automated Refactoring:&lt;/strong&gt; Automatically updates code syntax, dependencies, and patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recipe-based:&lt;/strong&gt; Uses declarative recipes to define transformation rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Style Preservation:&lt;/strong&gt; Maintains original code formatting and comments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Large-Scale Changes:&lt;/strong&gt; Processes entire codebases consistently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensible:&lt;/strong&gt; Supports custom recipes for specific migration needs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OpenRewrite modifies &lt;a href="https://docs.openrewrite.org/concepts-and-explanations/lossless-semantic-trees" rel="noopener noreferrer"&gt;Lossless Semantic Trees (LSTs)&lt;/a&gt;, which represent your source code, then prints them back into source.&lt;/li&gt;
&lt;li&gt;You review the changes and commit them as needed.&lt;/li&gt;
&lt;li&gt;Modifications are made by &lt;strong&gt;Visitors&lt;/strong&gt;, grouped into &lt;strong&gt;Recipes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Recipes keep changes minimally invasive and preserve the original formatting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚠️ New in 2026: the Code Genome Project repository
&lt;/h2&gt;

&lt;p&gt;One thing that has changed significantly since the original post: &lt;strong&gt;OpenRewrite recipe artifacts are moving from Maven Central to the &lt;a href="https://docs.openrewrite.org/running-recipes/running-rewrite-on-a-maven-project-without-modifying-the-build#configure-the-code-genome-project-repository" rel="noopener noreferrer"&gt;Code Genome Project&lt;/a&gt; repository, which requires authentication.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;rewrite-maven-plugin&lt;/code&gt; itself still resolves normally, but if a recipe module doesn't resolve for you, add the Code Genome Project repository (with credentials) to your &lt;code&gt;pom.xml&lt;/code&gt; or &lt;code&gt;settings.xml&lt;/code&gt;. If you're pinning older recipe versions that are still on Maven Central, you may not hit this yet — but it's the single biggest "gotcha" for anyone following an older tutorial today.&lt;/p&gt;

&lt;h1&gt;
  
  
  Practice
&lt;/h1&gt;

&lt;p&gt;In this post I'll demonstrate how to migrate a simple CRUD Spring Boot application built with Java 8, Spring Boot 2.x, and JUnit 4 to &lt;strong&gt;Java 25, Spring Boot 4, and JUnit 6&lt;/strong&gt; using OpenRewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codebase
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) pom.xml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;project&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0"&lt;/span&gt;
         &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
         &lt;span class="na"&gt;xsi:schemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;modelVersion&amp;gt;&lt;/span&gt;4.0.0&lt;span class="nt"&gt;&amp;lt;/modelVersion&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;parent&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-parent&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.7.14&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;relativePath/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/parent&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.example&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;demo&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.0.1-SNAPSHOT&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;name&amp;gt;&lt;/span&gt;demo&lt;span class="nt"&gt;&amp;lt;/name&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;description&amp;gt;&lt;/span&gt;Demo project for Spring Boot Migration&lt;span class="nt"&gt;&amp;lt;/description&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;java.version&amp;gt;&lt;/span&gt;1.8&lt;span class="nt"&gt;&amp;lt;/java.version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- dependencies: starter web, data-jpa, etc --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/project&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) UserController.java
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RequestMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GET&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getAllUsers&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RequestMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;POST&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Valid&lt;/span&gt; &lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;savedUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/{id}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RequestMethod&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GET&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getUserById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;notFound&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/username"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getUserByUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;userService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;notFound&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) UserService.java
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;findByUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByUsernameNative&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4) UserRepository.java
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Repository&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JpaRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT * FROM users WHERE username = ?1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nativeQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;findByUsernameNative&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5) User.java
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;javax.persistence.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;javax.validation.constraints.NotNull&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GenerationType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AUTO&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@NotNull&lt;/span&gt;
    &lt;span class="nd"&gt;@Column&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nullable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Column&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// setter, getter&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6) UserControllerTest.java
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RunWith&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SpringRunner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@SpringBootTest&lt;/span&gt;
&lt;span class="nd"&gt;@AutoConfigureMockMvc&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserControllerTest&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;MockMvc&lt;/span&gt; &lt;span class="n"&gt;mockMvc&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Before&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;deleteAll&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;testCreateUser&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"{\"username\":\"testuser\",\"email\":\"test@example.com\"}"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;mockMvc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;perform&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MockMvcRequestBuilders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userJson&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;andExpect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MockMvcResultMatchers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isOk&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Test&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;testGetUser&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"testuser"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test@example.com"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;mockMvc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;perform&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MockMvcRequestBuilders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/users/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;andExpect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MockMvcResultMatchers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;isOk&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;andExpect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MockMvcResultMatchers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jsonPath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$.username"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"testuser"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Manual migration
&lt;/h3&gt;

&lt;p&gt;Before reaching for OpenRewrite, let's see what happens if we manually jump straight to Java 25 and Spring Boot 4.&lt;/p&gt;

&lt;p&gt;First, update &lt;code&gt;pom.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;parent&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-parent&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;relativePath/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/parent&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;java.version&amp;gt;&lt;/span&gt;25&lt;span class="nt"&gt;&amp;lt;/java.version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, clean and build the project with Maven:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn clean &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll be greeted by a wall of compilation errors — &lt;code&gt;javax.*&lt;/code&gt; imports that no longer exist, deprecated APIs, JUnit 4 annotations that don't compile against JUnit 5, and Spring configuration properties that were renamed or removed across two major versions. Doing this by hand across a real codebase is exactly the slog OpenRewrite was built to eliminate.&lt;/p&gt;

&lt;p&gt;Here's the actual output from doing exactly that on the sample project — bumping the parent to Spring Boot 4 and &lt;code&gt;java.version&lt;/code&gt; to 25, then running &lt;code&gt;mvn clean compile&lt;/code&gt; on JDK 25:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzyrtllpi1ohuuhfg52r8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzyrtllpi1ohuuhfg52r8.png" alt="Manual migration compile failure: 12 errors — javax.persistence / javax.validation packages do not exist, and @Entity, @Table, @Id, @GeneratedValue, @Column, @NotNull, @Valid cannot be found" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Twelve compilation errors before you've fixed a single line by hand. Okay, let's revert those changes and migrate with OpenRewrite instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration with OpenRewrite
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Add the OpenRewrite plugin
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;pom.xml&lt;/code&gt;, add the OpenRewrite Maven plugin (Gradle users can add the Gradle plugin). Note the version bump — the original post used &lt;code&gt;6.3.2&lt;/code&gt;; the plugin used for this walkthrough is &lt;code&gt;6.46.1&lt;/code&gt; (the current line is &lt;code&gt;6.4x&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;build&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.openrewrite.maven&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;rewrite-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;6.46.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/build&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Choose the recipes for migration
&lt;/h3&gt;

&lt;p&gt;To discover all the available recipes, check the &lt;a href="https://docs.openrewrite.org/recipes" rel="noopener noreferrer"&gt;Recipe catalog&lt;/a&gt;, which lists everything for Java, Spring Boot, Hibernate, Quarkus, and more.&lt;/p&gt;

&lt;p&gt;For a full modernization, I'll chain four recipes: &lt;strong&gt;Java 8 → 25&lt;/strong&gt;, &lt;strong&gt;JUnit 4 → 5&lt;/strong&gt;, &lt;strong&gt;JUnit 5 → 6&lt;/strong&gt;, and &lt;strong&gt;Spring Boot → 4.0&lt;/strong&gt;. A nice detail here: the Spring Boot 4 recipe internally runs the intermediate upgrades (2.x → 3.x → 4.x) for you, so you don't have to stage them manually.&lt;/p&gt;

&lt;p&gt;Add the recipes and their recipe modules to &lt;code&gt;pom.xml&lt;/code&gt;. The versions below are the ones actually used for this walkthrough; recipe modules move quickly (roughly a release every 2–4 weeks), so always cross-check the &lt;a href="https://docs.openrewrite.org/reference/latest-versions-of-every-openrewrite-module" rel="noopener noreferrer"&gt;latest versions of every OpenRewrite module&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.openrewrite.maven&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;rewrite-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;6.46.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;exportDatatables&amp;gt;&lt;/span&gt;true&lt;span class="nt"&gt;&amp;lt;/exportDatatables&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;activeRecipes&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;recipe&amp;gt;&lt;/span&gt;org.openrewrite.java.migrate.UpgradeToJava25&lt;span class="nt"&gt;&amp;lt;/recipe&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;recipe&amp;gt;&lt;/span&gt;org.openrewrite.java.spring.boot2.SpringBoot2JUnit4to5Migration&lt;span class="nt"&gt;&amp;lt;/recipe&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;recipe&amp;gt;&lt;/span&gt;org.openrewrite.java.testing.junit6.JUnit5to6Migration&lt;span class="nt"&gt;&amp;lt;/recipe&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;recipe&amp;gt;&lt;/span&gt;org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0&lt;span class="nt"&gt;&amp;lt;/recipe&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/activeRecipes&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.openrewrite.recipe&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;rewrite-migrate-java&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;3.42.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.openrewrite.recipe&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;rewrite-spring&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;6.37.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.openrewrite.recipe&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;rewrite-testing-frameworks&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;3.44.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on Java 25:&lt;/strong&gt; the &lt;code&gt;rewrite-migrate-java&lt;/code&gt; module ships incremental recipes — &lt;code&gt;UpgradeToJava17&lt;/code&gt;, &lt;code&gt;UpgradeToJava21&lt;/code&gt;, and now &lt;code&gt;UpgradeToJava25&lt;/code&gt;. The &lt;code&gt;UpgradeToJava25&lt;/code&gt; recipe transparently chains the earlier steps (25 → 21 → 17 → 11), so you can target the current LTS directly, as this walkthrough does. The Spring Boot 4 upgrade also independently ensures your build targets a Java version compatible with Spring Boot 4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note on the Code Genome Project:&lt;/strong&gt; at the time of writing, the recipe module versions above still resolved fine from Maven Central — but OpenRewrite is actively migrating recipe distribution to the &lt;a href="https://docs.openrewrite.org/running-recipes/running-rewrite-on-a-maven-project-without-modifying-the-build#configure-the-code-genome-project-repository" rel="noopener noreferrer"&gt;Code Genome Project repository&lt;/a&gt;, which requires authentication. If a newer recipe module fails to resolve, add that repository (with credentials) to your &lt;code&gt;pom.xml&lt;/code&gt; or &lt;code&gt;settings.xml&lt;/code&gt;. This distribution change is the biggest thing to watch for since the 2025 version of this tutorial.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Already partway there? The same config still works
&lt;/h3&gt;

&lt;p&gt;You might already have done part of this journey — say you migrated to &lt;strong&gt;Java 17, Spring Boot 3, and JUnit 5&lt;/strong&gt; a while ago and only now want to reach Java 25 / Spring Boot 4 / JUnit 6. The good news: &lt;strong&gt;you can use the exact same configuration above.&lt;/strong&gt; OpenRewrite recipes are effectively idempotent — they operate on the Lossless Semantic Tree and only make a change where one is actually needed. Code that has already been upgraded simply won't be touched.&lt;/p&gt;

&lt;p&gt;So the &lt;code&gt;UpgradeToJava25&lt;/code&gt; recipe leaves your already-Java-17+ code alone (it only applies the steps you still need), &lt;code&gt;SpringBoot2JUnit4to5Migration&lt;/code&gt; does nothing if there's no JUnit 4 left, and the Spring Boot 4 recipe skips the 2.x → 3.x steps you've already completed and picks up only from where you are. There's no need to build a different, "resume-from-here" configuration for partially-migrated projects — point the same recipes at the codebase, run &lt;code&gt;dryRun&lt;/code&gt; to confirm the scope, and only the genuinely outstanding changes show up in the diff.&lt;/p&gt;

&lt;p&gt;Now install the plugin and its recipes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn clean &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Preview the migration
&lt;/h3&gt;

&lt;p&gt;OpenRewrite provides a &lt;code&gt;dryRun&lt;/code&gt; mode so you can preview changes before applying them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn rewrite:dryRun
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This writes a patch file (by default under &lt;code&gt;target/rewrite/rewrite.patch&lt;/code&gt;) that you can inspect with &lt;code&gt;git diff&lt;/code&gt; or any diff viewer — no source files are touched. The console prints the full recipe tree it &lt;em&gt;would&lt;/em&gt; apply, so you can see exactly how the high-level recipes decompose into the intermediate steps:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqji67sgydc44j6qvin1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbqji67sgydc44j6qvin1.png" alt="mvn rewrite:dryRun output showing the recipe tree — UpgradeToJava25 chaining down to Java 21/17/11, UpgradeSpringBoot_4_0 chaining down through 3.5 → 3.0 → Spring Framework 6.0 → Jakarta EE 10, and the JUnit 4 → 5 migration — ending in BUILD SUCCESS with " width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice how &lt;code&gt;UpgradeSpringBoot_4_0&lt;/code&gt; transparently runs the whole 3.5 → 3.4 → … → 3.0 chain, then Spring Framework 6.0, then the Jakarta EE 10 &lt;code&gt;javax.*&lt;/code&gt; → &lt;code&gt;jakarta.*&lt;/code&gt; migration. You didn't have to stage any of that. OpenRewrite even prints an "Estimate time saved: 2h 34m" — for a five-file toy project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Apply the migration
&lt;/h3&gt;

&lt;p&gt;When you're happy with the preview, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn rewrite:run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use your IDE or a diff checker to review the changes. Here's what actually happened on the sample project (Spring Boot 2.7.14 / Java 8 / JUnit 4 → Spring Boot 4.0.8 / Java 25 / JUnit 6):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Updated pom.xml&lt;/strong&gt; — the Spring Boot parent jumps from &lt;code&gt;2.7.14&lt;/code&gt; to &lt;code&gt;4.0.8&lt;/code&gt;, &lt;code&gt;java.version&lt;/code&gt; from &lt;code&gt;1.8&lt;/code&gt; to &lt;code&gt;25&lt;/code&gt;, &lt;code&gt;spring-boot-starter-web&lt;/code&gt; becomes the new Boot 4 modular &lt;code&gt;spring-boot-starter-webmvc&lt;/code&gt;, and the old JUnit 4 dependency is replaced with &lt;code&gt;spring-boot-starter-webmvc-test&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdk9eug55bw1h3h02vmje.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdk9eug55bw1h3h02vmje.png" alt="pom.xml diff: parent 2.7.14 → 4.0.8, java.version 1.8 → 25, spring-boot-starter-web → spring-boot-starter-webmvc, and the JUnit 4 dependency replaced by spring-boot-starter-webmvc-test" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Updated Controller&lt;/strong&gt; — migrates &lt;code&gt;javax.validation.Valid&lt;/code&gt; to &lt;code&gt;jakarta.validation.Valid&lt;/code&gt;, and swaps every &lt;code&gt;@RequestMapping(method = ...)&lt;/code&gt; for the dedicated &lt;code&gt;@GetMapping&lt;/code&gt; / &lt;code&gt;@PostMapping&lt;/code&gt; annotations:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2st6ieoevclutphqlr47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2st6ieoevclutphqlr47.png" alt="UserController.java diff: javax.validation.Valid → jakarta.validation.Valid; @RequestMapping(method = RequestMethod.GET) → @GetMapping; @RequestMapping(method = RequestMethod.POST) → @PostMapping; @RequestMapping(value = " width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Updated Entity and Repository&lt;/strong&gt; — the JPA/validation imports on the &lt;code&gt;User&lt;/code&gt; entity move from &lt;code&gt;javax.*&lt;/code&gt; to &lt;code&gt;jakarta.*&lt;/code&gt;, and — a nice bonus — the repository's &lt;code&gt;@Query(..., nativeQuery = true)&lt;/code&gt; is rewritten to the newer dedicated &lt;code&gt;@NativeQuery&lt;/code&gt; annotation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F423m5c5yczk098uvujz8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F423m5c5yczk098uvujz8.png" alt="User.java and UserRepository.java diff: javax.persistence.* / javax.validation.constraints.NotNull → jakarta.*, and @Query(nativeQuery = true) → @NativeQuery" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Updated Unit Test&lt;/strong&gt; — replaces JUnit 4's &lt;code&gt;@RunWith(SpringRunner.class)&lt;/code&gt; and &lt;code&gt;@Before&lt;/code&gt; with JUnit Jupiter's &lt;code&gt;@BeforeEach&lt;/code&gt; (dropping the now-unnecessary &lt;code&gt;SpringRunner&lt;/code&gt; entirely), and updates &lt;code&gt;AutoConfigureMockMvc&lt;/code&gt; to its new Spring Boot 4 &lt;code&gt;spring-boot-webmvc-test&lt;/code&gt; package:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hxh0fk1dyw6698h03xf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3hxh0fk1dyw6698h03xf.png" alt="UserControllerTest.java diff: org.junit imports and @RunWith(SpringRunner.class) removed; @Before → @BeforeEach with org.junit.jupiter.api imports; AutoConfigureMockMvc moved to org.springframework.boot.webmvc.test.autoconfigure" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the Spring Boot 4 recipe runs the intermediate 2.x → 3.x → 4.x steps, you also get the Spring Framework 6/7 property renames and configuration-key migrations handled along the way.&lt;/p&gt;

&lt;h4&gt;
  
  
  Verify it actually works
&lt;/h4&gt;

&lt;p&gt;The real test of any migration isn't the diff — it's whether the result compiles and the tests still pass. On JDK 25, with the migrated Spring Boot 4.0.8 / JUnit 6 code:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm9moizlsdv2cycq3jfi4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm9moizlsdv2cycq3jfi4.png" alt="mvn clean test on JDK 25 — Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS" width="800" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Green across the board. The same two tests that passed on Spring Boot 2.7 / JUnit 4 now pass on Spring Boot 4 / JUnit 6, without a single line changed by hand.&lt;/p&gt;

&lt;h1&gt;
  
  
  Limitations
&lt;/h1&gt;

&lt;p&gt;OpenRewrite successfully carries the app to Java 25 and Spring Boot 4, but the limitations from the original post still hold — and are worth restating:&lt;/p&gt;

&lt;p&gt;Because OpenRewrite relies on predefined recipes, it supports many common frameworks but not all of them. If you depend on a third-party library that lacks a recipe (the classic example being something like Ehcache2 → Ehcache3), you'll need to either write a custom recipe or handle that part manually.&lt;/p&gt;

&lt;p&gt;If you write a custom recipe, consider contributing it back to the OpenRewrite community to help others facing the same migration.&lt;/p&gt;

&lt;p&gt;A newer, practical limitation: with recipes moving to the Code Genome Project, first-time setup now involves configuring an authenticated repository. Budget a little extra time for that in CI environments.&lt;/p&gt;

&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;p&gt;OpenRewrite still dramatically simplifies Java and Spring Boot migration by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automating repetitive code changes&lt;/li&gt;
&lt;li&gt;Reducing migration time and effort&lt;/li&gt;
&lt;li&gt;Minimizing human error&lt;/li&gt;
&lt;li&gt;Standardizing the migration approach&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What has changed in the year and a half since the original post is the &lt;em&gt;destination&lt;/em&gt;: the sensible target is now &lt;strong&gt;Java 25, Spring Boot 4, and JUnit 6&lt;/strong&gt;, applied with a much newer plugin (&lt;code&gt;6.46.1&lt;/code&gt;) and recipe modules that are migrating from Maven Central to the Code Genome Project. The workflow — add plugin, pick recipes, &lt;code&gt;dryRun&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;, review — is reassuringly the same.&lt;/p&gt;

&lt;p&gt;OpenRewrite still &lt;strong&gt;doesn't eliminate the need for testing and validation&lt;/strong&gt;, but it removes most of the mechanical toil so you can focus on the genuinely tricky parts: business logic and the corners no recipe covers yet.&lt;/p&gt;

&lt;p&gt;And if you take one meta-lesson from this refresh: content ages just like code. The difference is that code has OpenRewrite — content still needs a human (or a helpful agent) to keep it current.&lt;/p&gt;

&lt;h1&gt;
  
  
  Credits
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;All the real credit for the approach goes to &lt;a href="https://dev.to/hgky95"&gt;Ky Huynh&lt;/a&gt;.&lt;/strong&gt; This is a 2026 refresh of his original post, &lt;a href="https://dev.to/hgky95/simplify-java-and-springboot-migration-with-openrewrite-g3d"&gt;&lt;em&gt;Simplify Java and SpringBoot migration with OpenRewrite&lt;/em&gt;&lt;/a&gt; — the walkthrough structure and the sample codebase (&lt;a href="https://github.com/hgky95/TIL/tree/main/java-springboot-migration" rel="noopener noreferrer"&gt;hgky95/TIL&lt;/a&gt;) are his, so please go read (and give some love to) the original.&lt;/p&gt;

&lt;p&gt;Unlike the versions and recipe numbers, the &lt;strong&gt;screenshots in this post are all freshly captured&lt;/strong&gt; — the migration was actually run on Ky's sample project (Spring Boot 2.7.14 / Java 8 / JUnit 4) against Java 25, Spring Boot 4.0.8, and JUnit 6, and every diff, the &lt;code&gt;dryRun&lt;/code&gt; recipe tree, the manual-failure output, and the passing test run are the genuine console/&lt;code&gt;git diff&lt;/code&gt; output from that run, rendered for readability. In other words: the versions were verified, the migration was executed end-to-end, the tests pass, and the images show exactly what happened — drafted with an AI coding agent and reviewed by a human.&lt;/p&gt;

&lt;h1&gt;
  
  
  References
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/hgky95/simplify-java-and-springboot-migration-with-openrewrite-g3d"&gt;Original post — Ky Huynh (Mar 2025)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openrewrite.org/" rel="noopener noreferrer"&gt;OpenRewrite docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openrewrite.org/recipes/java/spring/boot4" rel="noopener noreferrer"&gt;Migrate to Spring Boot 4.0 recipe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openrewrite.org/reference/rewrite-maven-plugin" rel="noopener noreferrer"&gt;OpenRewrite Maven plugin configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openrewrite.org/reference/latest-versions-of-every-openrewrite-module" rel="noopener noreferrer"&gt;Latest versions of every OpenRewrite module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://endoflife.date/spring-boot" rel="noopener noreferrer"&gt;Spring Boot end-of-life dates&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>openrewrite</category>
      <category>migration</category>
    </item>
    <item>
      <title>Happy 8th Birthday, JEP 8209434 — Here's Your Preview Implementation</title>
      <dc:creator>Istvan Verhas</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:03:09 +0000</pubDate>
      <link>https://dev.to/verhasi/happy-8th-birthday-jep-8209434-heres-your-preview-implementation-4de9</link>
      <guid>https://dev.to/verhasi/happy-8th-birthday-jep-8209434-heres-your-preview-implementation-4de9</guid>
      <description>&lt;p&gt;Today, August 13, 2026, marks exactly &lt;strong&gt;8 years&lt;/strong&gt; since Brian Goetz filed &lt;a href="https://openjdk.org/jeps/8209434" rel="noopener noreferrer"&gt;JEP 8209434: Concise Method Bodies&lt;/a&gt; — a draft proposal to align method body syntax with lambda expression syntax in Java.&lt;/p&gt;

&lt;p&gt;Eight years. Still in draft status. Still not in any Java release.&lt;/p&gt;

&lt;p&gt;So I built it myself with Kiro.&lt;/p&gt;

&lt;h2&gt;
  
  
  What JEP 8209434 Proposes
&lt;/h2&gt;

&lt;p&gt;Two new forms for method bodies that eliminate boilerplate in delegation and simple methods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Expression form: -&amp;gt; &lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Method reference form: =&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nl"&gt;aList:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These expand to exactly what you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;aList&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever written a decorator, adapter, or any class that delegates to another object, you know the pain. The JDK's own &lt;code&gt;Collections.UnmodifiableCollection&lt;/code&gt; has 14 methods that are nothing but &lt;code&gt;{ return c.something(); }&lt;/code&gt;. With concise method bodies, they become one-liners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Wait?
&lt;/h2&gt;

&lt;p&gt;The JEP has been in "Draft" status since 2018. No target release. No preview flag. Meanwhile, C# shipped expression-bodied members in 2015. Kotlin has had single-expression functions since day one.&lt;/p&gt;

&lt;p&gt;Java developers deserve this too. And there's nothing preventing us from having it &lt;em&gt;today&lt;/em&gt; — as a source code preprocessor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Preprocessor
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/verhasi/java-composition" rel="noopener noreferrer"&gt;java-composition&lt;/a&gt; is a source-to-source transformer. You write concise method bodies in your &lt;code&gt;.java&lt;/code&gt; files, and the preprocessor expands them into standard Java before &lt;code&gt;javac&lt;/code&gt; ever sees them.&lt;/p&gt;

&lt;p&gt;Your IDE sees the concise source. The compiler sees standard Java. Any Java version from 8 onwards.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Expression Form (&lt;code&gt;-&amp;gt;&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Purely syntactic — no type resolution needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Non-void: wraps in return&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Void: expression statement&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Method Reference Form (&lt;code&gt;=&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The method reference is &lt;em&gt;invoked&lt;/em&gt; with the method's parameters. The preprocessor infers which method is being referenced and generates the correct invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Bound instance — receiver is the expression before ::&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nl"&gt;aList:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// → return aList.size();&lt;/span&gt;

&lt;span class="c1"&gt;// Unbound instance — first parameter becomes receiver  &lt;/span&gt;
&lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nl"&gt;String:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// → return s.isEmpty();&lt;/span&gt;

&lt;span class="c1"&gt;// Static — all parameters become arguments&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nl"&gt;Math:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// → return Math.max(a, b);&lt;/span&gt;

&lt;span class="c1"&gt;// Constructor&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Foo&lt;/span&gt; &lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nl"&gt;Foo:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// → return new Foo(a, b);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Example: JDK's UnmodifiableCollection
&lt;/h2&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UnmodifiableCollection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="no"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;                 &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;();}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;          &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;();}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;);}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;         &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toArray&lt;/span&gt;&lt;span class="o"&gt;();}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;     &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;);}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;          &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;();}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 8 more like this&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UnmodifiableCollection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="no"&gt;E&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;                 &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;          &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;         &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toArray&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;     &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toArray&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;          &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 8 more like this&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same semantics. Half the noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Under the hood, the tool:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Forks JavaParser&lt;/strong&gt; (the standard Java parser library) and extends its grammar to recognize &lt;code&gt;-&amp;gt; Expression ;&lt;/code&gt; and &lt;code&gt;= MethodReference ;&lt;/code&gt; as valid method bodies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stores concise bodies in the AST&lt;/strong&gt; without expanding them — this enables future transformations to build on top&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs transformation passes&lt;/strong&gt; that expand concise forms into standard &lt;code&gt;BlockStmt&lt;/code&gt; bodies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pretty-prints&lt;/strong&gt; the result as standard Java&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the &lt;code&gt;=&lt;/code&gt; form, the preprocessor infers the referenced method from the source tree and classpath to generate the correct invocation. This is the only part that requires configuration (a classpath parameter).&lt;/p&gt;

&lt;h2&gt;
  
  
  API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Expression form only (no classpath needed)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;preprocessor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Preprocessor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourceRoot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetRoot&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;preprocessor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com/example/MyClass.java"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Method reference form (classpath needed for type resolution)&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;preprocessor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Preprocessor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sourceRoot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetRoot&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"lib/dependency.jar"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;preprocessor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com/example/MyClass.java"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Works on Any Java Version
&lt;/h2&gt;

&lt;p&gt;The output is plain Java. No lambdas, no &lt;code&gt;var&lt;/code&gt;, no records — just method bodies with &lt;code&gt;return&lt;/code&gt; statements. If your project targets Java 8, the output compiles with Java 8. If you target 21, same.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;preprocessor itself&lt;/em&gt; runs on Java 8 and above — it has no runtime dependency on modern Java features.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is just the beginning. The project roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maven plugin&lt;/strong&gt; — drop-in build integration (&lt;code&gt;generate-sources&lt;/code&gt; phase)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradle plugin&lt;/strong&gt; — same for Gradle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shaded JAR&lt;/strong&gt; — single dependency with no classpath conflicts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Published to Maven Central&lt;/strong&gt; — &lt;code&gt;guru.mocker:java-composition&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And beyond that, the real dream: &lt;strong&gt;wildcard delegation&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nl"&gt;List:&lt;/span&gt;&lt;span class="o"&gt;:*&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nl"&gt;delegate:&lt;/span&gt;&lt;span class="o"&gt;:*;&lt;/span&gt;  &lt;span class="c1"&gt;// All List methods delegate to 'delegate'&lt;/span&gt;

    &lt;span class="c1"&gt;// Override only what you need&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adding: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line to generate dozens of forwarding methods. Composition as a first-class pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The code is at &lt;a href="https://github.com/verhasi/java-composition" rel="noopener noreferrer"&gt;github.com/verhasi/java-composition&lt;/a&gt;. Clone, build, and start writing concise method bodies today.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone git@github.com:verhasi/java-composition.git
&lt;span class="nb"&gt;cd &lt;/span&gt;java-composition
&lt;span class="nb"&gt;cd &lt;/span&gt;javaparser &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./mvnw clean &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-DskipTests&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ..
mvn clean &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-pl&lt;/span&gt; preprocessor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 31 tests pass. Real-world tested against the JDK's own &lt;code&gt;Collections.java&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Happy 8th birthday, JEP 8209434. We got tired of waiting.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>jep</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>OpenAPI Spec Validation in Maven Is Now a First-Class Citizen (v7.24.0)</title>
      <dc:creator>Istvan Verhas</dc:creator>
      <pubDate>Mon, 20 Jul 2026 12:18:37 +0000</pubDate>
      <link>https://dev.to/verhasi/openapi-spec-validation-in-maven-is-now-a-first-class-citizen-v7240-1bbc</link>
      <guid>https://dev.to/verhasi/openapi-spec-validation-in-maven-is-now-a-first-class-citizen-v7240-1bbc</guid>
      <description>&lt;p&gt;OpenAPI Generator &lt;a href="https://github.com/OpenAPITools/openapi-generator/releases/tag/v7.24.0" rel="noopener noreferrer"&gt;v7.24.0&lt;/a&gt; shipped with 170+ enhancements, but one addition quietly fills a gap that's been annoying Java teams for years: &lt;strong&gt;a dedicated &lt;code&gt;validate&lt;/code&gt; goal for the Maven plugin.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Until now, validating an OpenAPI specification in a Maven workflow meant either running the full &lt;code&gt;generate&lt;/code&gt; goal (slow, unnecessary I/O) or pulling in external plugins. If your team maintains an "API Contract" module — where specs live without immediate code generation — you had no lightweight way to verify those contracts as part of the build.&lt;/p&gt;

&lt;p&gt;That changes now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Consider a typical API-first project structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-service/
├── api-contract/          ← OpenAPI specs live here (no code generation)
│   └── src/main/resources/
│       └── openapi.yaml
├── service-impl/          ← Generated code + business logic
└── service-client/        ← Generated client SDK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;api-contract&lt;/code&gt; module exists purely to version and validate specs. But without a &lt;code&gt;validate&lt;/code&gt; goal, your CI pipeline couldn't catch spec errors until the downstream &lt;code&gt;generate&lt;/code&gt; step — minutes later, buried in code generation failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: &lt;code&gt;validate&lt;/code&gt; Goal
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.openapitools&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;openapi-generator-maven-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;7.24.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;phase&amp;gt;&lt;/span&gt;validate&lt;span class="nt"&gt;&amp;lt;/phase&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;goals&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;goal&amp;gt;&lt;/span&gt;validate&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/goals&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;inputSpec&amp;gt;&lt;/span&gt;${project.basedir}/src/main/resources/api/openapi.yaml&lt;span class="nt"&gt;&amp;lt;/inputSpec&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Your spec is validated at the very beginning of the Maven lifecycle — before compile, before generate, before anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Fail-Fast CI/CD
&lt;/h3&gt;

&lt;p&gt;Broken specs halt the pipeline in &lt;strong&gt;seconds&lt;/strong&gt; rather than waiting minutes for downstream code generation and compilation failures. The validation runs the same rule-based engine (&lt;code&gt;OpenApiEvaluator&lt;/code&gt;) that powers the CLI &lt;code&gt;validate&lt;/code&gt; command — deep structural and semantic checks, not just YAML syntax.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. No Unnecessary Work
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;validate&lt;/code&gt; goal performs zero I/O beyond reading the spec. No file generation, no template processing, no output directories. Pure validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Bulk Validation
&lt;/h3&gt;

&lt;p&gt;Got a multi-API repository? Use &lt;code&gt;inputSpecRootDirectory&lt;/code&gt; to validate all specs in one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inputSpecRootDirectory&amp;gt;&lt;/span&gt;${project.basedir}/src/main/resources/apis/&lt;span class="nt"&gt;&amp;lt;/inputSpecRootDirectory&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more separate execution blocks per file. All validation issues across your entire spec ecosystem in a single Maven invocation.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Eclipse/m2e Integration
&lt;/h3&gt;

&lt;p&gt;The goal includes proper &lt;code&gt;lifecycle-mapping-metadata.xml&lt;/code&gt; configuration, so Eclipse users get incremental validation on workspace file changes. Edit your spec, save, see errors immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Behavior
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Errors&lt;/strong&gt; (structural/semantic violations): Fail the build with actionable messages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recommendations&lt;/strong&gt; (best practice suggestions): Logged but non-blocking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip support&lt;/strong&gt;: Honors the same skip flags as the &lt;code&gt;generate&lt;/code&gt; goal&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Bigger Picture: Contract-First Development
&lt;/h2&gt;

&lt;p&gt;This feature enables a cleaner separation in contract-first (API-first) development:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;mvn validate    ← Spec is correct (new goal!)
mvn generate    ← Code is generated from spec
mvn compile     ← Generated + hand-written code compiles
mvn test        ← Everything works together
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each phase does one thing. The &lt;code&gt;validate&lt;/code&gt; goal ensures you never waste time generating code from a broken spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;Upgrade to &lt;code&gt;openapi-generator-maven-plugin&lt;/code&gt; 7.24.0 and add the execution block above. If you maintain API contract modules that don't generate code, this is the missing piece for your build pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/OpenAPITools/openapi-generator/releases/tag/v7.24.0" rel="noopener noreferrer"&gt;Release notes: v7.24.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/OpenAPITools/openapi-generator/pull/23911" rel="noopener noreferrer"&gt;PR #23911: Add validate Mojo to Maven Plugin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-maven-plugin" rel="noopener noreferrer"&gt;OpenAPI Generator Maven Plugin docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Full disclosure: I authored this contribution (&lt;a href="https://github.com/OpenAPITools/openapi-generator/pull/23911" rel="noopener noreferrer"&gt;PR #23911&lt;/a&gt;). The implementation leverages the existing &lt;code&gt;OpenApiEvaluator&lt;/code&gt; rule engine from the CLI and wraps it as a Maven lifecycle component.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>api</category>
      <category>mvn</category>
      <category>openapi</category>
    </item>
    <item>
      <title>Onboarding Your AI Pair Programmer: From Spec-Driven Development to Kiro Powers</title>
      <dc:creator>Istvan Verhas</dc:creator>
      <pubDate>Sat, 11 Jul 2026 17:12:43 +0000</pubDate>
      <link>https://dev.to/verhasi/onboarding-your-ai-pair-programmer-from-spec-driven-development-to-kiro-powers-pdm</link>
      <guid>https://dev.to/verhasi/onboarding-your-ai-pair-programmer-from-spec-driven-development-to-kiro-powers-pdm</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Every developer who's used an AI coding assistant has experienced this: you ask it to contribute to your project and it produces something that &lt;em&gt;works&lt;/em&gt; but doesn't &lt;em&gt;belong&lt;/em&gt;. The code compiles, the tests pass, but a seasoned contributor would never write it that way. The AI doesn't know your project's conventions, architectural decisions, or the unwritten rules that make a contribution feel native.&lt;/p&gt;

&lt;p&gt;We solved this problem for human contributors decades ago. It's called onboarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Insight: AI Needs Onboarding Too
&lt;/h2&gt;

&lt;p&gt;When a new developer joins a project, they don't start writing code immediately. They:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the documentation&lt;/strong&gt; — CONTRIBUTING.md, wiki pages, architecture guides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Study merged PRs&lt;/strong&gt; — Understanding "the way things are done"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Study the source code&lt;/strong&gt; — Internalizing patterns and conventions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe rejected PRs&lt;/strong&gt; — Learning what NOT to do&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internalize patterns&lt;/strong&gt; — Building a mental model of project culture&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply knowledge&lt;/strong&gt; — Contributing effectively with fewer review cycles&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An AI assistant that skips this process is like a contractor who starts coding without reading the project docs. Technically capable, but culturally oblivious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The difference:&lt;/strong&gt; A human stores onboarding knowledge in memory. An AI needs it stored in structured documentation that persists between sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spec-Driven Development Meets AI Memory
&lt;/h2&gt;

&lt;p&gt;If you practice spec-driven development — writing requirements, design documents, and quality criteria before implementation — you already have the mindset for this. AI onboarding is simply extending that discipline to cover &lt;em&gt;how your AI team member should think about your project&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The deliverable isn't code. It's a &lt;strong&gt;steering documentation system&lt;/strong&gt; — a structured knowledge base that transforms your AI from a generic code generator into a project-aware contributor.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Story: A Real-World Wake-Up Call
&lt;/h2&gt;

&lt;p&gt;We (Kiro and me) developed this approach while contributing to &lt;a href="https://github.com/javaparser/javaparser" rel="noopener noreferrer"&gt;JavaParser&lt;/a&gt;, a 6,000+ star open source project with strict architectural standards. After receiving maintainer feedback on our first PR that highlighted 8 code quality issues (not bugs — the code worked, but it bypassed project abstractions), we realized: the AI needed to be onboarded properly.&lt;/p&gt;

&lt;p&gt;The maintainer's feedback boiled down to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the metamodel API, not raw reflection&lt;/li&gt;
&lt;li&gt;Test behavioral contracts, not implementation details&lt;/li&gt;
&lt;li&gt;One PR = One concern, no exceptions&lt;/li&gt;
&lt;li&gt;Provide evidence for every claimed fix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these were documented bugs. They were &lt;strong&gt;cultural knowledge&lt;/strong&gt; — the kind you only learn by being part of the project. Exactly what onboarding is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture: Modular Steering System
&lt;/h2&gt;

&lt;p&gt;We designed a layered knowledge system that balances always-available guidance with context-specific depth:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three layers:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Core Principles&lt;/strong&gt; — Non-negotiable rules that apply to every contribution. Architecture patterns, code style requirements, process essentials. Always loaded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quick Reference&lt;/strong&gt; — Actionable guidance for common scenarios. Step-by-step workflows, checklists, decision trees, anti-patterns with corrections. Always loaded.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;On-Demand Modules&lt;/strong&gt; — Deep knowledge for specific contexts. Only loaded when relevant, keeping the AI's context window efficient for actual work.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Memory Efficiency
&lt;/h3&gt;

&lt;p&gt;With a 1M token context window, the always-loaded core takes ~0.35% of available context. Even with an on-demand module loaded, total steering overhead stays under 3%. The AI has 97%+ of its capacity available for actual work.&lt;/p&gt;

&lt;p&gt;Compare this to a human: you can't "unload" your project knowledge when working. The AI's modular memory is actually more efficient than ours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Process: Phased Onboarding
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 0: Documentation Study (~1 session)
&lt;/h3&gt;

&lt;p&gt;Read all existing project documentation and extract patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contributing guidelines, README, wiki pages&lt;/li&gt;
&lt;li&gt;CI/CD configurations (reveals automated quality gates)&lt;/li&gt;
&lt;li&gt;Build scripts (reveals process dependencies)&lt;/li&gt;
&lt;li&gt;Representative source files (reveals implementation patterns)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deliverable:&lt;/strong&gt; Working steering system — immediately usable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Recent PR Analysis (~2-3 sessions)
&lt;/h3&gt;

&lt;p&gt;Study 50-100 recent merged PRs to learn implicit patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintainer feedback themes (what gets called out repeatedly)&lt;/li&gt;
&lt;li&gt;Success patterns (what gets approved smoothly)&lt;/li&gt;
&lt;li&gt;Communication norms (how discussions are conducted)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deliverable:&lt;/strong&gt; Priority modules for highest-impact areas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Deep Historical Analysis (~3-4 sessions)
&lt;/h3&gt;

&lt;p&gt;Comprehensive pattern extraction from project history:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Architectural decisions and their rationale&lt;/li&gt;
&lt;li&gt;Pattern evolution over time&lt;/li&gt;
&lt;li&gt;Anti-patterns from rejected approaches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deliverable:&lt;/strong&gt; Complete module library covering all major contribution scenarios.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Community Integration (~1-2 sessions)
&lt;/h3&gt;

&lt;p&gt;Optimization and publication for broader adoption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Module cross-linking and performance optimization&lt;/li&gt;
&lt;li&gt;Integration with project's existing documentation&lt;/li&gt;
&lt;li&gt;Validation through real-world contributions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deliverable:&lt;/strong&gt; Published, community-shareable resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality Assurance: How Do You Know It Works?
&lt;/h2&gt;

&lt;p&gt;This is where spec-driven thinking pays off. We defined quality gates before starting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source Validation&lt;/strong&gt; — Only official, maintainer-authored content informs the guide&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pattern Consistency&lt;/strong&gt; — No contradictory guidance across modules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage Baseline&lt;/strong&gt; — Gaps explicitly identified and tracked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usability Test&lt;/strong&gt; — Simulate a real contribution scenario using only the guide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The usability test is the critical one. After creating our steering system, Kiro walked through our actual PR scenario (fixing a visitor generator) using only the guide. Every decision point was covered. Every common mistake was documented with corrections.&lt;br&gt;
This kind of task to step out of the context is hard for a human, but any AI can handle it easily.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Evolution: From Files to Kiro Powers
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. While building this system, we faced a practical problem: where do these steering files live? They don't belong in the project repo (the maintainers didn't ask for them). They need to be available regardless of which branch you're on. And ideally, other contributors could reuse them.&lt;/p&gt;

&lt;p&gt;Then we discovered &lt;a href="https://kiro.dev/powers/" rel="noopener noreferrer"&gt;Kiro Powers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Powers are Kiro's native mechanism for packaging specialized knowledge — exactly what we'd built. A Power bundles steering files with optional MCP tools, hooks, and activation keywords. When you mention relevant topics in conversation, Kiro loads the appropriate knowledge automatically.&lt;/p&gt;

&lt;p&gt;Our steering system mapped almost 1:1 to the Power format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;javaparser-contributor/
├── POWER.md                          # Core principles + steering routing
└── steering/
    ├── quick-reference.md           # Common scenarios
    ├── generator-development.md     # On-demand: generator work
    ├── pr-workflow.md               # On-demand: PR process
    └── metamodel-patterns.md        # On-demand: metamodel changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This solved every problem at once:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No git conflicts — Power lives in its own repo&lt;/li&gt;
&lt;li&gt;Available across all branches — Kiro loads it independently&lt;/li&gt;
&lt;li&gt;Community shareable — One-click install from GitHub&lt;/li&gt;
&lt;li&gt;Context-aware — Keywords trigger the right modules automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The methodology we developed independently turned out to align perfectly with Kiro's product architecture. We were building a Power without knowing it.&lt;br&gt;
Maybe Kiro knew about this approach all along, but kept it well hidden.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real Results (Phase 0)
&lt;/h2&gt;

&lt;p&gt;Before onboarding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintainer flagged 8 issues across code quality, test quality, and scope management&lt;/li&gt;
&lt;li&gt;Multiple review cycles needed&lt;/li&gt;
&lt;li&gt;Time spent learning project patterns through trial and error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After completing Phase 0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Guide covers all 8 feedback patterns that were raised&lt;/li&gt;
&lt;li&gt;Metamodel-first principle is the first thing the AI checks&lt;/li&gt;
&lt;li&gt;Scope discipline is enforced at every decision point&lt;/li&gt;
&lt;li&gt;Evidence requirements built into workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Phase 0 investment: ~17 credits (about 1 focused session).&lt;br&gt;
The estimated total investment (Phase 0-3): ~2,000 credits across 7-10 sessions.&lt;br&gt;
The return: Every future session starts with "experienced contributor" knowledge, improving further with each phase.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Broader Vision: AI as a Real Team Member
&lt;/h2&gt;

&lt;p&gt;This isn't about making AI produce better code in isolation. It's about making AI a &lt;strong&gt;real team member&lt;/strong&gt; — one that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understands the project's architecture and respects it&lt;/li&gt;
&lt;li&gt;Follows established patterns without being told each time&lt;/li&gt;
&lt;li&gt;Communicates with maintainers in appropriate style&lt;/li&gt;
&lt;li&gt;Makes decisions consistent with project history&lt;/li&gt;
&lt;li&gt;Gets better over time as new patterns are learned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you onboard a human contributor, you invest days or weeks. The return is years of productive contribution. AI onboarding takes hours. The return is immediate, perpetual, and reusable — the knowledge never degrades, never gets forgotten, and can be shared with every contributor who uses AI assistance.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Minimal approach (30 minutes):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ask your AI assistant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Read all my project documentation and create a steering system with:
1. Core principles - Non-negotiable project rules
2. Quick reference - Common scenarios with guidance
3. Pattern index - Keyword lookup and gap tracking

Validate by simulating one realistic contribution scenario.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Full approach (Kiro Power):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a Power following the &lt;a href="https://kiro.dev/docs/powers/create/" rel="noopener noreferrer"&gt;Kiro Power format&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write a &lt;code&gt;POWER.md&lt;/code&gt; with frontmatter, core principles, and steering routing&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;steering/&lt;/code&gt; files for on-demand knowledge modules&lt;/li&gt;
&lt;li&gt;Push to GitHub and install via the Kiro Powers panel&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; Your AI assistant's quality ceiling isn't its model capability — it's how well it understands your specific project. Onboarding closes that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: Work in Progress
&lt;/h2&gt;

&lt;p&gt;Spec-driven development taught me to think before we code. AI onboarding extends that principle: &lt;strong&gt;think about what your AI needs to know before it contributes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We completed Phase 0 and packaged it as a &lt;a href="https://github.com/verhasi/kiro-powers" rel="noopener noreferrer"&gt;Kiro Power&lt;/a&gt;. We're now gathering real-world experience using it during actual development. The questions we're exploring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the guide reduce maintainer feedback cycles in practice?&lt;/li&gt;
&lt;li&gt;Which gaps surface first during real contributions?&lt;/li&gt;
&lt;li&gt;Is the modular architecture valuable, or is the core system sufficient for most work?&lt;/li&gt;
&lt;li&gt;When does the investment in Phase 1+ pay for itself?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Based on these experiences, we'll decide whether and when to proceed with deeper phases. The phased/agile approach means we can stop at any point with a useful deliverable — there's no all-or-nothing commitment.&lt;/p&gt;

&lt;p&gt;The result isn't just better AI output. It's a reusable knowledge artifact that benefits every contributor — human or AI (but mainly for AI nowadays) — who works on your project. It's institutional knowledge preservation. It's accelerated onboarding. It's quality at scale.&lt;/p&gt;

&lt;p&gt;Your AI pair programmer is ready to learn. The question is whether you'll invest in teaching it your project's way of working, or keep accepting "works but doesn't belong" code forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links &amp;amp; Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaParser Contributor Power:&lt;/strong&gt; &lt;a href="https://github.com/verhasi/kiro-powers" rel="noopener noreferrer"&gt;github.com/verhasi/kiro-powers&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiro Powers documentation:&lt;/strong&gt; &lt;a href="https://kiro.dev/docs/powers/create/" rel="noopener noreferrer"&gt;kiro.dev/docs/powers/create&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaParser project:&lt;/strong&gt; &lt;a href="https://github.com/javaparser/javaparser" rel="noopener noreferrer"&gt;github.com/javaparser/javaparser&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kiro:&lt;/strong&gt; &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;kiro.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This methodology was developed during contributions to the JavaParser open source project using &lt;a href="https://kiro.dev/cli/" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt;. The approach is language-agnostic and applicable to any codebase with established conventions. Follow-up posts will share what we learn from real-world usage and deeper onboarding phases.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>kiro</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
