OpenRewrite ist nicht mehr ganz neu für uns. Bereits in unseren Projekten zum JBoss EAP 8 Upgrade und bei Quarkus Updates haben wir OpenRewrite schon verwendet. Allerdings nur als reine Anwender von fertigen OpenRewrite Rezepten. Jetzt wollten wir uns ansehen, wie wir selbst Rezepte schreiben können.
- Learning Friday
- Software-Modernisierung
Write OpenRewrite
Automatisierte Code-Transformationen meistern – Ein Erfahrungsbericht.
Wo bringt OpenRewrite Mehrwert?
Wir arbeiten an großen Anwendungen, die teilweise schon in die Jahre gekommen sind. Wenn in solchen Anwendungen grundsätzliche Änderungen durchgeführt werden sollen, z.B. Ändern von APIs, dann betrifft das schnell 100+ Stellen, die geändert werden müssen. Dies manuell zu ändern macht uns keinen Spass, dauert lange und ist fehleranfällig. Eventuelle Learnings später in der Umsetzung werden aufgrund des Aufwandes nicht genutzt. Also automatisiert umschreiben. Also schreiben wir Open Rewrite Rezepte. Write OpenRewrite (WOR) war dann auch der Name unseres Learning Friday Projektes.
Die Use Cases
Wir haben zwei primäre Use Cases aus unseren Projekten ins Auge gefasst.
Der erste Use Case betrifft eine Intergrationsapplikation, in der Schnittstellen eines Binärprotokolls, das die Zeiten von COBOL und Tuxedo überdauert hat, in Webservice Schnittstellen umgeschrieben werden sollen.
Der zweite Use Case betrifft QueryDSL. Hierbei sollten einige Versionen übersprungen werden. Die APIs weisen zum Teil wesentliche Unterschiede auf.
Zusätzlich wollten wir ein bisschen mehr über den Umgang mit OpenRewrite lernen. Z.B. wie man POMs damit ändert, wie man XML-Dateien anpasst oder wie man OpenRewrite in Multi-Module Maven Projekten einsetzt.
Java Rezepte Schreiben
Der Use Case bezüglich Umschreiben von Schnittstellen eines Binärprotokoll zu Webservices ist einer, der sehr viele unterschiedliche Anforderungen mit sich bringt. Das Rezept soll in der Lage sein, eine beliebige Anzahl an unterschiedlichen Services der neuen Schnittstelle zu erkennen und daraufhin die Nutzungen der alten Schnittstelle ersetzen. Die neue Schnittstelle ist an einigen Stellen etwas anders zu bedienen. Herausforderungen waren daher u.a. veränderte Datentypen, fehlende Unterstützung von Lazy-Loading und neue Verschachtelungen.
Wir mussten kontinuierlich abgleichen, für welche Fälle es sich lohnt, automatisiert durch ein Rezept angepasst zu werden. Letztendlich hat das Rezept den Großteil der Anpassungen durchgeführt. Einige Spezialfälle, die selten vorkommen und/oder nur mit viel Aufwand im Rezept umsetzbar waren, wurden händisch umgesetzt. Letztendlich sind die Rezepte für solche Anwendungsfälle “Wegwerfcode”, der nach dem Erfüllen seines Zwecks üblicherweise nicht mehr verwendet wird. Dadurch sollte man seine Ansprüche an sauberen, wartbaren Code nicht unbedingt wegwerfen, aber ein wenig zurückschrauben.
Es gab aufgrund dieses breiten Anwendungsfalles eine Vielzahl an Erkenntnissen. Kleine Rezepte schneiden ist ein sehr wichtiger Grundsatz, da die Rezepte schnell kompliziert und fehleranfällig werden. Ev. sollte man sich aber überlegen, manche Teilaufgaben noch weiter zu spalten, auch wenn das auf Kosten der Performance passiert. Zudem hatten wir hier ein paar Probleme bezüglich mangelnder Dokumentation und teilweise unvorhersehbarem oder instabilen Verhalten. Für einige Teilaufgaben, wie z.B. eine Annotation zu einer Liste an Klassen hinzufügen, ist OpenRewrite ein sehr schneller, effizienter Lösungsweg.
Beispiel: Eine “simple” Checkbox-Anpassung
Eines unserer Hauptanwendungsgebiete für OpenRewrite ist das Upgrade von Jakarta 8 auf 10. Um Deploymentdeskriptoren (z.B. persistence.xml) auf die aktuelle Version hochzuziehen, gibt es bereits Rezepte. Allerdings fehlt darin das Hochziehen von orm.xml Deskriptoren. Eine gute Gelegenheit so ein Rezept zu schreiben.
Wir starten mit einem Test, in dem wir Ist- und Soll angeben:
Als Rezept wollen wir JavaxOrmXmlToJakartaOrmXml verwenden:
Darin wollen wir Namespace, Schema-Location und Version anpassen. Dies ist einfach über ein Declarative Recipe zu erledigen:
Wir speichern die Datei z.B. in META-INF/rewrite/ormxml.yml. Damit sind wir auch schon fertig, um den Test erfolgreich laufen zu lassen. Einfache Änderungen an XML-Dateien durchzuführen ist also erfreulich unkompliziert.
Arbeiten mit Maven Projekten
In der Praxis arbeiten wir häufig mit großen Maven-Projekten, die viele Module besitzen. Dependencies werden bestenfalls gemanagt in einer „Parent-pom.xml“, im schlimmsten Fall sind sie verstreut über mehrere `pom.xml`-Dateien.
Um die eigens geschriebenen Rezepte möglichst leicht in solchen Projekten anzuwenden, können die geschriebenen Rezepte gesammelt in einem neuen Sub-Modul hinzugefügt werden. Dieses Modul kann als Dependency in der Konfiguration des OpenRewrite Maven Plugins wie folgt verwendet werden, sodass die aufgelisteten Rezepte auf das ganze Modul (inkl. Submodule) angewendet werden
Die Anwendung des Rezeptes erfolgt dann mittels
Auf diese Art können auch Rezepte von anderen Projekten über deren Artefakte aus dem Maven Repository verwendet werden.
Um den Maven-Build nicht zu verändern, kann alternativ auch auf die Plugin-Konfiguration im `pom.xml`-File verzichtet werden.
Anmerkung: Rezepte mit Konfigurationsoptionen müssen immer über eine `rewrite.yml`-Datei gewrappt werden. Rezepte ohne jegliche Optionen können direkt unter `<activeRecipes>` angeführt werden.
Scopes der Rezepte begrenzen
Die ursprüngliche Annahme war, dass Rezepte per Sub-Modul mittels der Plugin-Konfiguration definiert werden können, sodass der Scope der eingesetzten Rezepte eingeschränkt werden kann. Die Plugin-Konfigurationsoption <runPerSubmodule>true</…> hat zu anfangs auch den Anschein gemacht, das zu ermöglichen. Es hat sich allerdings schnell gezeigt, dass diese Option fehleranfällig zu sein scheint, indem wir unterschiedliche Deltas zwischen einem `rewrite:dryRun` und `rewrite:run` beobachtet haben, die sich nicht leicht erklären lassen.
Stattdessen scheint der einfachste Weg zu sein, sämtliche Konfiguration des OpenRewrite Maven Plugin’s bei einem Multi-Module Projekt in dessen „Parent-pom.xml“ zu schreiben. Pro Rezept kann dann noch eine oder mehrere `precondition`s gesetzt werden (wie oben demonstriert). Dabei können verschiedene Rezepte konfiguriert werden, welche die Anwendung der darauffolgenden Rezepte auf die Dateien einschränken, für die alle Precondition-Rezepte zutreffen.
Fazit
Während das Anwenden von OpenRewrite Rezepten einfach ist und gut dokumentiert, sieht es beim Schreiben von Java-Rezepten etwas anders aus. Man hat ein mächtiges und komplexes Werkzeug in der Hand, aber die Dokumentation dazu ist dürftig. Um es zu meistern, sollte man keine Scheu haben, in den Code einzutauchen und vorhandene Rezepte zu lesen, um sich nützliche Tricks anzueignen. Erfreulicherweise ist die Community sehr hilfsbereit und im Slack-Channel wird rasch geholfen. Um etwas komplexere Aufgaben zu lösen, bedarf es dennoch eines beträchtlichen Einarbeitungsaufwands. Wir halten es daher für sinnvoll, das Wissen bei einigen (wenigen) Entwicklern seiner Organisation zu bündeln, falls man selbst Rezepte schreiben will. Mit einem erfahrenen OpenRewrite Entwickler an der Seite gelingt es dann viel schneller, gängige Probleme zu lösen. Ob sich das Zeitinvestment für Automatisierung lohnt oder ob man lieber manuell Code anpasst, muss jeder anhand seiner Use Cases entscheiden.
Für uns war “Write OpenRewrite” einge gut investierte Zeit, da wir das in unserem Auto-Update Service einsetzen.