adesso Blog

Eigene Migrationen entwickeln mit Open Rewrite

Open Rewrite bietet Entwicklerinnen und Entwicklern eine Vielzahl von Migrationen für gängige Bibliotheken und Frameworks. Sie reduzieren den Migrationsaufwand erheblich und machen die Anwendung wiederholbar. Doch wie können Entwicklerinnen und Entwickler Rezepte für ihre eigenen Frameworks bereitstellen? Dieser Blog-Beitrag gibt eine Einführung in die Entwicklung von Open-Rewrite -Rezepten.

Die 3 Arten von Rezepten

In Open Rewrite werden Rezepte verwendet, um Migrationen zu definieren und wiederholbar auszuführen. Das Grundkonzept wurde in einem früheren Artikel beschrieben. Dabei gibt es drei verschiedene Abstraktionen mit unterschiedlicher Komplexität: deklarative YAML-Rezepte, Refaster-Templates und imperative Rezepte.

Deklarative Rezepte werden verwendet, um bestehende Rezepte zu konfigurieren und gemeinsam auszuführen. Im ersten Schritt wird das gewünschte Rezept im Open-Rewrite-Rezeptkatalog identifiziert und der vollqualifizierte Name ermittelt. Dieser Name entspricht dem Klassen- mit Paketnamen und wird auf der Dokumentationsseite direkt unter der Überschrift angegeben. Sollen Rezepte aus dem eigenen Bestand verwendet werden, so wird auch in diesem Fall der qualifizierende Name verwendet. Um beispielsweise eine Annotation zu entfernen, bietet sich das Rezept „Remove annotation“ mit dem qualifizierenden Namen org.openrewrite.java.RemoveAnnotation an. Die notwendige Konfiguration nehmen wir in einem neuen YAML-Rezept vor.

	
	---
		type: specs.openrewrite.org/v1beta/recipe
		name: de.adesso.houskeeping.ExampleRecipe
		displayName: Example Recipe to replace Service Call
		recipeList:
		  - org.openrewrite.java.RemoveAnnotation:
		      annotationPattern: '@java.lang.SuppressWarnings("deprecation")'
	

Durch den type wird es als Rezept für Open-Rewrite-Migrationen gekennzeichnet und kann über den qualifizierenden Namen hinter name angesprochen werden. Der displayName wird hauptsächlich für den automatisch generierten Rezeptkatalog verwendet. In der recipeList werden die auszuführenden Rezepte angegeben. Die möglichen Konfigurationen sind auf den jeweiligen Rezeptseiten angegeben. Wenn die vorhandenen Grundrezepte im Rezeptkatalog nicht ausreichen, müssen neue Rezepte geschrieben werden.

Ein mögliches Beispiel wäre der Ersatz eines Methodenaufrufs an einen Service. Hier bietet sich der Einsatz von Refaster Templates aus dem Error Prone Projekt an. Mit Error Prone Refaster können musterbasierte Refactorings durchgeführt werden. Open Rewrite unterstützt die Refaster Templates und bietet damit eine geeignete Abstraktion für die Anpassung von Methodenaufrufen.

	
	package de.adesso.houskeeping
		@RecipeDescriptor(
		    name = "Migrate to modern Method",
		    description = "Migrate the old method to modern"
		)
		static class SwitchToModernMethod {
		    @BeforeTemplate
		    void old(MyService srv, String msg, int times) {
		        srv.oldMethode(times, msg);
		    }
		    @AfterTemplate
		    void modern(MyService srv, String msg, int times) {
		        srv.otherMethode(msg, times);
		    }
		}
	

Ein Refaster Template Rezept wird durch den @RecipeDesciptor als Rezept markiert, mit einem Namen für die Dokumentation und einer Beschreibung versehen. In der mit @BeforeTemplate gekennzeichneten Methode wird die auszutauschende Code-Passage angegeben. Dabei werden die Serviceinstanz und die Aufrufparameter in der Methode typgerecht verwendet. Die mit @AfterTemplate markierte Methode definiert den Zielzustand und verwendet Typsicherheit, um die Reihenfolge der Parameter anzupassen.

Refaster Templates können nur Änderungen innerhalb eines Codeblocks vornehmen. Wenn darüber hinausgehende Anpassungen notwendig sind und diese nicht durch die Kombination vorhandener Rezepte möglich sind, kann ein imperatives Rezept erstellt werden. Diese imperativen Rezepte erweitern die abstrakte Klasse org.openrewrite.Recipe.

	
	public class TestRecipe extends org.openrewrite.Recipe {
		    @Override
		    public String getDisplayName() {
		        return "An example Recipe";
		    }
		    @Override
		    public String getDescription() {
		        return "This Recipe is an example for our Blog.";
		    }
		    @Override
		    public TreeVisitor<?, ExecutionContext> getVisitor() {
		        return super.getVisitor();
		    }
		}
	

Die Methode getDisplayName liefert den Anzeigenamen und getDescription die Beschreibung für die automatisch generierte Dokumentation. Die Methode getVisitor gibt eine Instanz eines TreeVisitors zurück. Ein TreeVisitor durchläuft den Lossless Semantic Tree (LST) und manipuliert einzelne Knoten in diesem Baum. Der LST ist die Repräsentation des gesamten Quellcodes innerhalb des Open Rewrite Tools. Der LST wird beim Start von Open Rewrite erzeugt und nach der Ausführung aller Rezepte verwendet, um die Änderungen wieder in Quellcode zu überführen. Für jede unterstützte Sprache gibt es eine eigene LST-Implementierung und angepasste Visitoren. Um Java Quellcode zu manipulieren, wird der org.openrewrite.java.JavaIsoVisitor verwendet. Dieser enthält für jeden Elementtyp im LST eine Visit-Methode, die das geänderte Element zurückgibt.

Durch die umfassende API gibt es eine sehr steile Lernkurve bei der Erstellung von Rezepten. Die Dokumentation des Open-Rewrite-Projekts und die Slack/Discord Kanäle der Open Rewrite Community helfen bei den ersten Schritten.

Testen von Rezepten

Eine Migration auf Spring Boot 3.2 ist zu umfangreich und generisch, um durch einen Test abgesichert zu werden. Stattdessen ist es sinnvoller, kleine Änderungen am Quellcode zu testen. Um dies zu ermöglichen, bietet Open Rewrite eine auf JUnit Jupiter basierende Möglichkeit, Tests für Rezepte zu erstellen.

	
	public class MigrateTestAnnotationTest implements RewriteTest {
		    @Override
		    public void defaults(RecipeSpec spec) {
		        spec.parser(JavaParser.fromJavaVersion())
			  .recipeFromResources("RecipesFQN")
		          .recipe(new MigrateTestAnnotation());
		    }
		    @Test
		    void migrateEmptyTestsWithBraces() {
		        rewriteRun(java("""
		          import org.testng.annotations.Test;
		          class MyTest {
		              @Test()
		              void test() {}
		          }
		          """, """
		          import org.junit.jupiter.api.Test;
		          class MyTest {
		              @Test
		              void test() {}
		          }
		      """));
		    }
		}
	

Durch die Implementierung des Interfaces org.openrewrite.test.RewriteTest wird die Testklasse um die notwendige Infrastruktur erweitert. Die Methode defaults wird verwendet, um Open Rewrite für die kommenden Testfälle zu konfigurieren. Dies geschieht über die Fluent API der RecipeSpec. In diesem Beispiel wird der Parser als Java Parser konfiguriert, dessen Version sich aus der verwendeten JDK Version ableitet. Mit der Methode recipe können in Java geschriebene Rezepte aktiviert werden. Mit recipeFromResources können YAML-Rezepte aus den Ressourcen geladen und aktiviert werden. Die eigentlichen Tests werden mit dem Methodenaufruf rewriteRun innerhalb der mit @Test annotierten Methode durchgeführt. Die Methode org.openrewrite.java.Assertions#java sorgt dafür, dass der als is übergebene Java-Quellcode in den should Code migriert wird. Neben Java gibt es weitere Assertions für jede andere unterstützte Sprache.

Verteilung eigener Rezepte

Ein großer Mehrwert für Organisationen entsteht, wenn die entwickelten Rezepte in anderen Kontexten wiederverwendet werden. Hierfür bietet sich ein Maven Artefakt an. Modern bietet mit dem Recipe Starter eine solide Basis mit ersten Beispielen für eine eigene Rezeptsammlung und einer Integration von Maven und Gradle.

Die in diesem Blog-Beitrag beschriebenen Konzepte habe ich unter anderem bereits in verschiedenen Konferenzbeiträgen und Workshops behandelt. Im letzten Teil meines Blog-Beitrags zum Thema Open Rewrite werde ich auf den skalierten Einsatz in Organisationen eingehen.

Auch interessant:

Bild Merlin Bögershausen

Autor Merlin Bögershausen

Merlin Bögershausen ist Software Entwickler, Speaker und Author bei adesso am Standort Aachen. Er beschäftigt sich mit Neuerungen im Java-Universum und der Softwareentwicklung im Public-Bereich.

Diese Seite speichern. Diese Seite entfernen.