This article is also available in English

Stell Dir vor, Du arbeitest in einem modernen Unternehmen und bist mitverantwortlich für die Pflege einer großen Anzahl von Softwareprojekten, die im Laufe der Jahre entstanden sind. Dies kann eine herausfordernde Aufgabe sein, insbesondere bei der hohen Release-Frequenz von Bibliotheken und Sicherheitspatches. In der Regel müssen zuerst die Build-Dateien der einzelnen Projekte manuell aktualisiert werden. Danach soll die Anwendung getestet werden, um sicherzustellen, dass alles funktioniert, und schließlich muss die neue Version in Produktion gebracht werden.

Große Framework-Upgrades können ebenfalls problematisch sein, insbesondere bei Softwareprojekten, die von mehr als einem Team entwickelt werden. Diese Migrationen werden normalerweise auf einem separaten Feature-Branch durchgeführt, der aufgrund von Funktionen, die von anderen Projektmitgliedern parallel entwickelt werden, ständig veraltet ist. Die Durchführung der endgültigen Integration ist daher fehleranfällig und erfordert viel Koordination innerhalb des Projekts.

OpenRewrite ist eine Bibliothek, die Dir helfen kann, die meisten dieser Aufgaben zu automatisieren. Ihre Hauptfunktion ist die automatische Änderung von Quellcode durch die Anwendung von “Rezepten” auf das Projekt. Diese Rezepte sind komplett in Java-Code definiert und können mit dem OpenRewrite Maven- oder Gradle-Plugin einfach in den Build-Prozess integriert werden. Es kann nicht nur Java Code anpassen, sondern auch die Maven pom.xml, Property Dateien (.properties oder .yml) und mehr ändern. Da es in den Build-Prozess integriert werden kann, ist es nicht notwendig, Feature-Branches zu verwenden, um Code-Änderungen und Framework-Upgrades durchzuführen. Durch die Verwendung einer separaten CI Pipeline- und/oder Build-Profils können die Änderungen direkt auf dem Master-Branch durchgeführt werden.

OpenRewrite bietet viele verfügbare Rezepte für Code-Wartung und Framework-Upgrades, zum Beispiel:

Eine ausführliche Liste aller Rezepte findest Du im Rezepte-Katalog. Anleitungen zu den beliebtesten Rezepten findest Du hier.

Wie funktioniert OpenRewrite

Wenn OpenRewrite Rezepte auf eine Code-Basis anwendet, konstruiert es eine Baumdarstellung des betreffenden Codes. Dieser Baum ist im Wesentlichen eine weiterentwickelte Version eines Abstract Syntax Tree (AST). Er liefert nicht nur die grundlegenden Informationen, die der Compiler benötigt, um den Code zu kompilieren, sondern hat auch die folgenden strukturellen Eigenschaften:

Ein AST mit diesen zusätzlichen Eigenschaften wird als “Lossless Semantic Tree” oder LST bezeichnet. Um diese Definition etwas weniger abstrakt zu machen, betrachten wir das folgende einfache Beispiel einer “Hello World”-Klasse, die eine hello()-Methode enthält:

package com.yourorg;

public class HelloWorld {

    public String hello() {
        return "Hello!";
    }
}
Hello World Beispiel

Der LST für diese Klasse sieht wie folgt aus:

#root
\---J.ClassDeclaration
    |---J.Modifier | "public"
    |---J.Identifier | "HelloWorld"
    \---J.Block
        \---#JRightPadded
            \---J.MethodDeclaration
                |---J.Modifier | "public"
                |---J.Identifier | "String"
                |---J.Identifier | "hello"
                |---#JContainer
                |   \---#JRightPadded
                |       \---J.Empty
                \---J.Block
                    \---#JRightPadded
                        \---J.Return | "return "Hello!""
                            \---J.Literal
Beispiel eines LST

Wie Du sehen kannst, sind alle Elemente im LST als interne Klassen (und Implementierungen) des Interfaces J definiert. Das ist die Baum-Implementierung für Java-Quelldateien. Um mit diesen LSTs zu arbeiten, verwendet OpenRewrite das Visitor-Pattern (implementiert durch die TreeVisitor-Klasse), um durch den Baum zu traversieren und die erforderlichen Transformationen anzuwenden, indem für jedes LST-Element die entsprechenden Callback-Methoden aufgerufen werden. Für das obige Beispiel lauten die relevanten Callback-Methoden in der allgemeinen Klasse JavaVisitor wie folgt:

class JavaVisitor<P> extends TreeVisitor<J, P> {

    ...
    public J visitClassDeclaration(J.ClassDeclaration classDecl, P p) {...}
    public J visitIdentifier(J.Identifier ident, P p) {...}
    public J visitBlock(J.Block block, P p) {...}
    public J visitMethodDeclaration(J.MethodDeclaration method, P p) {...}
    public J visitReturn(J.Return retrn, P p) {...}
    ...

}
Relevante Callback-Methoden für den 'Hello-World' LST

Innerhalb dieser Methoden kann auf alle Metadaten des jeweiligen LST-Elements zugegriffen werden und, was am wichtigsten ist, diese auch geändert werden, um Transformationen des Quellcodes zu erstellen. Beachtet, dass nicht alle LST-Elemente eine entsprechende Callback-Methode haben. Auf das Element J.Modifier kann beispielsweise nur über das übergeordnete Element zugegriffen werden (in diesem Fall J.ClassDeclaration oder J.MethodDeclaration).

Wenn Du eine Implementierung dieser Klasse erstellst, kannst Du Dein eigenes OpenRewrite-Rezept entwickeln. Wie das in der Praxis abläuft, erkläre ich in einem weiterführenden Artikel.

Die Verwendung von OpenRewrite in der Praxis

OpenRewrite kann leicht in den Build-Prozess integriert werden, indem das OpenRewrite Maven- oder Gradle-Plugin verwendet wird. In der Konfiguration des Plugins wird angegeben, welche Rezepte für das aktuelle Projekt aktiviert werden sollen.

<plugin>
    <groupId>org.openrewrite.maven</groupId>
    <artifactId>rewrite-maven-plugin</artifactId>
    <version>4.41.0</version>
    <configuration>
        <activeRecipes>
            <!-- Verwende den fully qualified name vom Rezept oder den Namen 
                 in rewrite.yml -->
            <recipe>...</recipe> 
            ...
        </activeRecipes>
    </configuration>
    <dependencies>
        <!-- Deklaration der Abhängigkeiten für Rezeptze, die nicht durch 
           OpenRewrite mitgeliefert werden -->
        ...
    </dependencies>
</plugin>
Verwendung des OpenRewrite Maven-Plugins

Durch die Ausführung von mvn rewrite:run werden die OpenRewrite-Rezepte ausgeführt. Nach Abschluss des Vorgangs erhältst Du eine Reihe von geänderten Dateien, die Du nach eingehender Überprüfung committen kannst. Wenn der Quellcode erst mal nicht geändert werden soll, kann auch der Befehl mvn rewrite:dryrun verwendet werden - dieser erzeugt nur eine Reihe von Diffs für alle Änderungen.

Wenn das Rezept, das ausgeführt werden soll, zusätzliche Konfigurationsparameter erfordert, muss eine Datei rewrite.yml definiert und im Hauptverzeichnis des Projekts (oder in META-INF/rewrite) abgelegt werden. In dieser Datei kann eine beliebige Anzahl von Rezepten oder Zusammenstellungen von Rezepten angegeben werden. Innerhalb der Konfiguration des Maven- oder Gradle-Plugins kann über den Namen genau das Rezept gewählt werden, das ausgeführt werden soll. Nehmen wir zum Beispiel an, dass Du die Apache POI-Bibliothek in Deinem Projekt von Version 5.2.2 auf 5.2.3 aktualisieren möchtest. Die rewrite.yml Datei für diese Änderung sieht dann wie folgt aus:

---
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.UpgradeDependencies
recipeList:
  - org.openrewrite.maven.UpgradeDependencyVersion:
      groupId: org.apache.poi
      artifactId: poi
      newVersion: 5.2.3
rewrite.yml für Dependency-Updates

Beachte, dass wir dieses Rezept com.yourorg.UpgradeDependencies genannt haben. Bei Anwendung im Maven-Plugin sieht die Konfiguration folgendermaßen aus:

<plugin>
    <groupId>org.openrewrite.maven</groupId>
    <artifactId>rewrite-maven-plugin</artifactId>
    <version>4.41.0</version>
    <configuration>
        <activeRecipes>
            <activeRecipe>com.yourorg.UpgradeDependencies</activeRecipe>
        </activeRecipes>
    </configuration>
</plugin>

Zur Veranschaulichung der oben besprochenen Konzepte und Techniken werden wir im nächsten Abschnitt ein komplizierteres Beispiel betrachten.

Aktualisieren von Spring-Boot-Anwendungen mit OpenRewrite

Lass uns nun eine kleine Spring Boot-Anwendung erstellen, um sowohl die Spring-Migrationsrezepte als auch die Rezepte zur Behebung häufiger Probleme bei der statischen Code-Analyse auszuprobieren. Die wichtigsten Änderungen beim Upgrade von Spring Boot 2.x auf Spring Boot 3.x sind das Upgrade von Java 8/11 auf Java 17 und der Wechsel vom javax zum jakarta Namespace. Daher möchten wir eine Spring Boot 2.x-Anwendung mit Java 11 und einem eingebetteten Tomcat-Server erstellen und auch einige schlecht geschriebene Codezeilen schreiben, die Klassen aus dem javax-Namespace referenzieren.

Nachdem wir unsere Anwendung mit dem Spring Initializr erstellt und Java 11 und die spring-boot-starter-web-Abhängigkeit ausgewählt haben, erhalten wir ein Maven-Projekt mit der folgenden pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
            https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>pietschijven</groupId>
    <artifactId>openrewrite-spring-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>openrewrite-spring-demo</name>
    <description>openrewrite-spring-demo</description>
    <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
pom.xml für das Spring Boot Projekt

Nun wollen wir einen einfachen Spring MVC Controller mit einem /hello-Endpunkt erstellen. Die Controller-Methode sollte die Klasse javax.servlet.ServletRequest verwenden, um eine Hallo-Nachricht mit Verweis auf die Anfrage-URL zurückzugeben. Wir schreiben den Code absichtlich auf schlechte Art und Weise, um zu sehen, wie OpenRewrite diese Stil-Fehler korrigiert. Der Controller sollte dann wie folgt aussehen:

package pietschijven.openrewritespringdemo.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;

@RestController
public class HelloController {

    private final static String base_message = "Hello request";

    @GetMapping("/hello")
    public String hello(ServletRequest servletRequest){
        String semicolon = String.valueOf(": ");;
        if ((servletRequest != null) == true) {
            return getString((HttpServletRequest) servletRequest, semicolon, 
                base_message);
        }
        return "no request";
    }

    private static final String getString(HttpServletRequest servletRequest, 
            String semicolon, String Message) {

        return Message + semicolon + servletRequest.getRequestURI();
    }

}
Spring MVC Controller für die /hello URL

Um OpenRewrite für die Korrektur unserer Stil-Fehler und für die Durchführung des Spring Boot-Upgrades zu verwenden, müssen wir es so konfigurieren, dass sowohl die CommonStaticAnalysis- als auch die UpgradeSpringBoot_3_0-Rezepte benutzt werden. Beides sind Rezepte, die sich aus vielen kleineren Refactoring-Rezepten zusammensetzen. Eine Liste aller in CommonStaticAnalysis enthaltenen Rezepte findest Du hier. Für das Rezept UpgradeSpringBoot_3_0 ist die Liste hier zu finden.

<plugin>
    <groupId>org.openrewrite.maven</groupId>
    <artifactId>rewrite-maven-plugin</artifactId>
    <version>4.41.0</version>
    <configuration>
        <activeRecipes>
            <activeRecipe>
                org.openrewrite.java.cleanup.CommonStaticAnalysis
            </activeRecipe>
            <activeRecipe>
                org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
            </activeRecipe>
        </activeRecipes>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.openrewrite.recipe</groupId>
            <artifactId>rewrite-spring</artifactId>
            <version>4.33.2</version>
        </dependency>
    </dependencies>
</plugin>
Konfiguration des OpenRewrite-Maven-Plugins für das Spring Boot Projekt

Bei Verwendung des dry-run-Befehls (mvn rewrite:dryRun) erzeugt OpenRewrite die Patch-Datei target/rewrite/rewrite.patch, die wir zur Überprüfung der Änderungen verwenden können. Für die Maven pom.xml hat diese Datei den folgenden Inhalt:

diff --git a/pom.xml b/pom.xml
index 8ded2e4..f89560b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@ org.openrewrite.java.migrate.javax.AddJaxbRuntime, /
    org.openrewrite.Recipe$AdHocRecipe, /
    org.openrewrite.maven.UpgradeDependencyVersion, /
    org.openrewrite.maven.AddDependency, /
    org.openrewrite.maven.UpgradeParentVersion, /
    org.openrewrite.java.migrate.UpgradeJavaVersion /

     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>2.7.9</version>
+        <version>3.0.4</version>
         <relativePath/> <!-- lookup parent from repository -->
     </parent>
     <groupId>pietschijven</groupId>
@@ -14,14 +14,23 @@
     <name>openrewrite-spring-demo</name>
     <description>openrewrite-spring-demo</description>
     <properties>
-        <java.version>11</java.version>
+        <java.version>17</java.version>
     </properties>
     <dependencies>
         <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
         <dependency>
+            <groupId>org.glassfish.jaxb</groupId>
+            <artifactId>jaxb-runtime</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
OpenRewrite diff für pom.xml

Wie Du sehen kannst, hat OpenRewrite die Java-Version auf 17 angehoben und die Spring Boot-Version auf 3.0.4 geändert. Außerdem hat es, wie erwartet, die Abhängigkeit zu jakarta-servlet:jakarta.servlet-api eingeführt, damit wir den neuen jakarta-Namespace in unserem HelloController verwenden können. In der Diff-Datei selbst erstellt OpenRewrite auch Kommentare für jedes Rezept, das zur Änderung der Datei verwendet wurde.

Das von OpenRewrite erzeugte Diff für den HelloController sieht wie folgt aus:

...
@@ -3,25 +3,25 @@ org.openrewrite.Recipe$AdHocRecipe, 
    org.openrewrite.java.cleanup.RenamePrivateFieldsToCamelCase, 
    org.openrewrite.java.cleanup.RenameLocalVariablesToCamelCase, 
    org.openrewrite.java.cleanup.SimplifyBooleanExpression, 
    org.openrewrite.java.cleanup.NoValueOfOnStringType, 
    org.openrewrite.java.ChangePackage, 
    org.openrewrite.java.cleanup.RemoveExtraSemicolons, 
    org.openrewrite.java.migrate.UpgradeJavaVersion, 
    org.openrewrite.java.cleanup.StaticMethodNotFinal

 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;

-import javax.servlet.ServletRequest;
-import javax.servlet.http.HttpServletRequest;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.http.HttpServletRequest;

 @RestController
 public class HelloController {

-    private final String base_message = "Hello request";
+    private final String baseMessage = "Hello request";

     @GetMapping("/hello")
     public String hello(ServletRequest servletRequest){
-        String semicolon = String.valueOf(": ");;
-        if ((servletRequest != null) == true) {
-            return getString((HttpServletRequest) servletRequest, semicolon, 
-                base_message);
+        String semicolon = ": ";
+        if ((servletRequest != null)) {
+            return getString((HttpServletRequest) servletRequest, semicolon, 
+               baseMessage);
         }
         return "no request";
     }

-    private static final String getString(HttpServletRequest servletRequest, 
-       String semicolon, String Message) {
-        return Message + semicolon + servletRequest
+    private static String getString(HttpServletRequest servletRequest, 
+       String semicolon, String message) {
+        return message + semicolon + servletRequest
                 .getRequestURI();
     }
OpenRewrite diff für HelloController

Die folgenden Änderungen hat OpenRewrite an der Quelldatei vorgenommen:

Wir sehen also, dass OpenRewrite eine Menge Stil-Fehler in unserem Quellcode gelöst und das Upgrade auf Spring Boot 3.x zu einem Kinderspiel gemacht hat.

Das Beispielprojekt ist hier auf Github zu finden.

Fazit

OpenRewrite ist ein leistungsfähiges Werkzeug, das bei der systematischen Wartung vieler Softwareprojekte Deines Unternehmens unterstützen kann. Es bietet eine große Anzahl von Refactoring-Rezepten, um die meisten Wartungsaufgaben zu automatisieren, wie z. B. die Aktualisierung von Abhängigkeiten und die Behebung von Problemen, die von statischen Code-Analyse-Tools gemeldet werden. Da OpenRewrite jedoch noch aktiv weiterentwickelt wird, kann es sein, dass die Rezepte nicht so stabil sind, wie Du gehofft hast. Mein Rat ist daher, immer die neuesten Versionen von OpenRewrite und den Rezepten zu verwenden.

Als Alternative zur manuellen Erstellung von rewrite.yml-Dateien für jedes Projekt kannst Du auch die SaaS-Lösung der OpenRewrite-Hersteller nutzen. Damit kannst Du all Deine Github/Gitlab-Repositories mit Deinem Konto auf der Plattform verbinden und umfangreiche Refactorings für alle Deine Projekte durchführen, die Ergebnisse anzeigen lassen und die Code-Änderungen zurück an die Repositories übergeben. Für eine vorausgewählte Reihe an Open-Source-Repositories, die auf Github verfügbar sind (z. B. Spring Boot), ist der Zugang auf der Plattform kostenlos.

Im nächsten Artikel zeige ich Dir, wie Du Deine eigenen OpenRewrite-Rezepte entwickeln kannst. Das wird Dir ein besseres Verständnis dafür geben, wie OpenRewrite intern funktioniert, und Dir helfen, bestehende Rezepte besser zu verstehen, wenn sie sich nicht so verhalten, wie Du es erwartest.