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:
- Behebung von Problemen, die von statischen Analysewerkzeugen gemeldet wurden
- Automatisches Beheben von Checkstyle-Verstößen
- Migration zu Java 11 oder Java 17
- Upgrade von JUnit 4 auf JUnit 5
- Upgrade von Spring Boot
- und vieles mehr …
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:
- Der Baum speichert Informationen über die Leerzeichen vor und nach den Baum-Elementen. Dies wird verwendet, um das ursprüngliche Format des Codes bei der Ausführung der Rezepte beizubehalten.
- Er enthält detaillierte Typ-Informationen für alle Elemente im Baum, auch wenn diese Typen nicht in der Quelldatei selbst definiert sind.
- Die Elemente im Baum können auch Markierungen besitzen. Markierungen sind zusätzliche Metadaten, die zum Beispiel Informationen über Checkstyle-Einstellungen, die Java-Version des Quellcodes oder zusätzliche Styling-Informationen enthalten. Sie können auch verwendet werden, um benutzerdefinierte Informationen während einer Traversierung des Baums zu speichern.
- Der Baum ist vollständig serialisierbar, auch wenn er zyklische Elemente enthält. Dies ermöglicht, den Baum im Voraus im JSON-Format zu generieren und für die zukünftige Verarbeitung zu speichern.
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:
Der LST für diese Klasse sieht wie folgt aus:
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:
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.
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:
Beachte, dass wir dieses Rezept com.yourorg.UpgradeDependencies
genannt haben.
Bei Anwendung im Maven-Plugin sieht die Konfiguration folgendermaßen aus:
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:
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:
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.
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:
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:
Die folgenden Änderungen hat OpenRewrite an der Quelldatei vorgenommen:
-
ChangePackage
hat die Import-Angaben für diejavax
-Abhängigkeiten in die entsprechendenjakarta
-Varianten geändert. -
RenamePrivateFieldsToCamelCase
hat das Feldbase_message
inbaseMessage
umbenannt, da es eine gängige Konvention in Java-Code ist, alle Variablen in Camel-Case zu schreiben. Außerdem wurden alle Verweise auf dieses Feld entsprechend umbenannt. -
RemoveExtraSemicolons
entfernte das unnötige Semikolon nach der Deklaration der Variablesemicolon
. -
NoValueOfOnStringType
entfernte das unnötigeString.valueOf(...)
in der Deklaration vonsemicolon
. -
SimplifyBooleanExpression
vereinfachte dieif
-Bedingung. -
StaticMethodNotfinal
entfernte den nicht benötigtenfinal
-Modifier in der Deklaration der statischen MethodegetString
. -
RenameLocalVariablesToCamelCase
änderte außerdem den Namen des MethodenparametersMessage
inmessage
.
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.