• Software-Modernisierung

{Write OpenRewrite: Binärschnittstelle zu Webservices}

In einem unserer Kundenprojekte werden ~100 Services über ein altes Binärdatenprotokoll aufgerufen. Diese Services sollen nun über eine Webschnittstelle kommunizieren, daher müssen auch die Aufrufe entsprechend verändert werden. Diese Aufgabe ist händisch aufwändig und fehleranfällig zu lösen, daher haben wir uns dazu entschieden, OpenRewrite zu verwenden. 

Bei OpenRewrite handelt es sich um ein Tool, mit dem man automatisierte Refactorings durchführen kann. Rezepte sind dabei die einzelnen Bausteine des Refactorings, die jeweils eine Aufgabe kapseln. OpenRewrite erstellt dafür eine Baumstruktur deines Codes (der Lossless Semantics Tree, kurz LST) und implementiert das Visitor-Pattern für Zugriffe auf diesen. Ein Visitor definiert die Lese- und Schreibzugriffe auf gewisse Arten von Knoten im LST (z.B. Methodenaufrufe, Klassendeklarationen).

Dieser Blogeintrag soll einen Überblick über die Herausforderungen und Hilfsmittel geben, auf die wir gestoßen sind. Falls du wissen möchtest, wie man Rezepte überhaupt entwickelt, möchten wir auf die offizielle Dokumentation von OpenRewrite, bzw. zu den Sourcen der offiziellen Rezepte verweisen.

Die neue Schnittstelle

Die Webschnittstelle läuft über SOAP. Wir exponieren Services als Webservices und holen uns die standardmäßig erzeugten WSDLs. Ein WSDL Dokument ist eine Schnittstellenbeschreibung eines Webservices, aus der man mittels Werkzeugen wie dem Apache CXF-Codegen Maven Plugin Code generieren kann. Natürlich ändern sich dadurch einige Aspekte, wie Packages, Objektstruktur, Datentypen oder die Objekterzeugung.

Die genauen Details, wie wir auf jeden dieser Punkte eingegangen sind, möchten wir dir natürlich ersparen. Wir möchten allerdings ein Muster vorstellen, das sich durch unsere gesamte Lösung durchzieht.

Das ScanningRecipe

Die Anzahl der neuen Web-Services, die wir aufrufen wollen, befindet sich im hohen zweistelligen Bereich. Wir haben uns also dagegen entschieden, diese unserem Rezept als einfache Liste zu übergeben. Dann wären wir auch beim Testen der Rezepte unflexibel. Wir wollen also die Namen der verfügbaren Web-Services aus dem Code extrahieren und darauf aufbauend Anpassungen treffen.

Diese Informationen sind ad hoc nicht innerhalb eines Zyklus eines Rezeptes herauslesbar. Ein Rezept durchläuft dabei den gesamten LST – jede Stelle wird dabei nur einmal betrachtet. In den meisten Fällen werden wir für Transformationen Informationen aus Klassen benötigen, die schon zuvor verarbeitet wurden. Daher müssen wir das Extrahieren der Informationen in einen eigenen Zyklus auslagern.

Genau dabei helfen uns die ScanningRecipes. Diese teilen eine Rezeptausführung in drei Phasen auf:

  • Scan: Informationen extrahieren
  • Generate: Neue Source-Files erstellen
  • Visit: Sourcen manipulieren

Wir möchten für unseren Anwendungsfall die Scan- und Visit-Phase nutzen. Dabei erstellen wir eine Klasse mit folgendem Aufbau:

*“services” ist dabei unser geteilter Zustand.

Die Scanning-Phase

Wir möchten uns letztendlich für das Auslesen der Services direkt auf den Klassennamen stützen, mit dem wir das Webservice auch aufrufen können. Dazu implementieren wir die Methode “visitClassDeclaration()” im anonymen JavaIsoVisitor der Scan-Phase. 

Grundsätzlich können wir uns darauf verlassen, dass die WebServices alle in demselben Package generiert werden. Dementsprechend können wir die Suche auf dieses Package mit Hilfe folgender Bedingung eingrenzen:

Innerhalb dieses Package “com.gepardec” liegen pro Service dessen Klassen in einem weiteren Package. Das beinhaltet aber nicht nur den Ausgangspunkt des Services, sondern auch alle DTOs, mit denen der Service kommuniziert. Allerdings gibt es in jedem generierten Package immer eine Klasse, die das Interface “Service” implementiert. Das ist jene Klasse, die man für die Kommunikation mit dem jeweiligen Web-Service benötigt

Wir können uns nun sicher sein, dass wir uns über Klassennamen nur noch Namen von Web-Service-Clients auslesen. Jetzt müssen wir das ganze nur noch speichern:

Die Visiting-Phase

Wir möchten nun demonstrieren, wie man mit Hilfe der Servicenamen in der Visit-Phase einen Aufruf ändert. Zuerst müssen wir uns ansehen, wie Aufrufe der alten Binärschnittstelle im Code aussehen. Das passiert über Klassen, aus einem bestimmten Package, die bei uns den Namen *SvcProxy haben. Über eine Methode call() wird dann eine Request verschickt. Z.B.:

Wir implementieren für diese die Visit-Methode  Transformation “visitMethodInvocation” für Methodenaufrufe. Mit einem MethodMatcher können wir auf diese call-Methoden filtern. OpenRewrite ermöglicht mit dieser Funktionalität, dass wir schnell nach Methodenaufrufen einer bestimmten Signatur filtern können. In unserem Fall sieht das so aus:

Die Expression „com.gepardec.binary..* call(..)“ matched dabei nur Aufrufe von Methoden namens “call”, die aus Klassen in unserem Binärformat-Package stammen. Eine Referenz für diese sog. MethodPatterns findet man hier.

Jetzt möchten wir noch nachsehen, ob ein passendes Webservice existiert:

Die Methode trimSvcProxySuffix(String) schneidet lediglich die Endung “SvcProxy” der Klassennamen der Binärschnittstelle ab, damit wir sie mit den Web-Klassennamen vergleichen können. Die Implementierung dieser Methoden sparen wir an diesem Punkt aus. Damit iterieren wir über alle bekannten Web-Service-Namen und filtern Binäraufrufe heraus, für die es kein passendes Web-Service gibt. 

Jetzt müssen wir nur noch den neuen Aufruf einfügen. Wir nutzen dafür ein JavaTemplate:

Dieser Befehl baut das JavaTemplate. Die Methode “builder” übernimmt dabei das Template mit Platzhaltern, die einerseits einfache Strings (“#{}”) erwarten, oder ein LST-Element mit einem Java-Typ. “#{any()}” verzichtet auf eine Spezifikation des Java-Typs, aber erwartet ein LST-Element.

Tipps für Typsicherheit

OpenRewrite  kann den neuen Methodenaufruf mit Typ-Metadaten annotieren. Dafür müssen wir dem Template einen Parser übergeben, der die verwendeten Typen kennt. Einem Parser spezifizieren wir das benötigte JAR mit “classpathFromResources”. Dazu muss das JAR mit den Sourcen in src/main/resources/META-INF/rewrite/classpath liegen.

In unserem Fall ist das webservices-1.0.0.jar erforderlich – wobei sich dieser Aufruf nur den eindeutigen Anfang des Namens erwartet. Mit .imports() geben wir noch den genauen Typen an. Das bedeutet nicht, dass ein Import im Source-File erstellt wird, sondern ist nur eine Information für den Parser.

Prinzipiell ist das Spezifizieren von Typen im JavaTemplate nicht verpflichtend – es kann auch ohne Typisierung Code eingefügt werden. Allerdings sind diese Elemente dann im LST ohne Typinformation hinterlegt, was Probleme in nachfolgenden Rezepten erzeugen kann. Z.B. würde der von uns verwendete MethodMatcher per default unbekannte Typen ignorieren.

Bevor wir unseren neuen Code einfügen, müssen wir noch die Imports ersetzen:

Das “maybe” bedeutet hier, dass der Import nur eingefügt/gelöscht wird, wenn der Typ nach dem Rezept verwendet bzw. nicht verwendet wird.

Das JavaTemplate muss nur noch befüllt und verarbeitet werden:

OpenRewrite führt einen Cursor, der im LST mitwandert. JavaTemplate#apply benötigt einen solchen Cursor, um zu wissen, auf welches LST-Element es angewandt wird. “updateCursor” lässt das im Cursor gespeicherte LST-Element neu evaluieren.

Des Weiteren erwartet sich das Template für die Anwendung eine JavaCoordinate. Das spezifiziert nochmals genau, wo das Template im LST-Element angewandt wird. In unserem Fall definieren wir mit replaceMethod einen kompletten Ersatz der Methode – allerdings lassen sich damit auch nur einzelne Parameter ersetzen oder einfach “vor/nach der Methode” ausdrücken. Danach folgt eine Auflistung der Argumente für die Platzhalter im Template.

Somit wird folgende Zeile Code:

mit Ausführung unseres Rezepts zu dieser hier umgewandelt:

Weitere Aufgaben

Unser Beispiel ist sehr vereinfacht und erzeugt ohne weiteres keinen kompilierenden Code, da es nur die interessantesten Fragestellungen veranschaulichen soll. Es deckt auch nur eine einzelne Anforderung ab. Wir haben uns noch um viele weitere Fälle im Aufruf kümmern müssen – zudem haben sich natürlich die DTOs geändert, mit denen die Services kommunizieren, was wir ebenfalls durch ein Rezept aufgedeckt haben. Auch kam ein Rezept hinzu, mit dem wir die Services serverseitig zu Webservices machten.

Zu Beginn entwickelten wir Testfälle im Vorher/Nachher Prinzip und schrieben Rezepte, die die Testanforderungen erfüllen. Mit der Zeit haben wir uns schrittweise an den echten Code herangetastet.  

Wir mussten kontinuierlich evaluieren, für welche Fälle es sinnvoll ist, 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.

Prinzipiell sind unsere Rezepte für einen einmaligen Anwendungsfall entstanden und damit lediglich “Wegwerfcode”, der nach dem Erfüllen seines Zwecks nie mehr verwendet wird. Man sollte seine Ansprüche an sauberen, les- und erweiterbaren Code zwar nicht vollständig vernachlässigen, aber man kann darauf zumindest weniger Wert als auf herkömmlichen Code in der Applikation legen.

geschrieben von:
Simon
WordPress Cookie Plugin von Real Cookie Banner