21. August 2015 | 5 min lesezeit

Automatische Refactorings mit Eclipse JDT

Migration von Swing zu JavaFX

Die Migration von einer mit Swing realisierten Anwendung zu einer neuen Technologie wie JavaFX hat üblicherweise Änderungen an einer größeren Codebasis zur Folge. Anstatt nun jede betroffene Codestelle mühsam von Hand umzuziehen, lässt sich diese Aufgabe mit den Java Development Tools (JDT) von Eclipse automatisiert erledigen.

Die Java Development Tools (JDT) von Eclipse sind ein mächtiges Werkzeug, das sämtliche Artefakte eines Projekts mit Hilfe gleich zweier APIs repräsentiert:

  • Einem schlanken und leicht handhabbaren Java Model , das sich auf wenige Kerninformationen zu Klassen, Feldern und Methoden beschränkt
  • und dem eher schwergewichtigen Abstract Syntax Tree (AST), der Zugriff auf alle erdenklichen Details des Codes, Typinformationen bis hin zu Zeilennummern ermöglicht.

Die grundsätzliche Vorgehensweise bei der Implementierung eines automatischen Refactorings mit JDT ist wie folgt:

  1. Im ersten Schritt verschafft man sich einen AST für die zu manipulierenden Klassen.
  2. Mit Hilfe der Klassen ASTRewrite und ImportRewrite aus dem Package eclipse.jdt.core.dom.rewrite werden alle gewünschte Änderungen am Code und den Importen aufgezeichnet.
  3. Im letzten Schritt werden die aufgezeichneten Änderungen mit Hilfe der Klassen eclipse.jface.text.Document und org.eclipse.text.edits.TextEdit tatsächlich auf den Code angewendet (weil die Schreibzugriffe auf den AST „teuer“ sind).

Für die wiederkehrenden Aufgaben automatischer Refactorings haben wir ein Toolset geschaffen, das zu migrierende Klassen im Projekt sucht, geparste ASTs bereitstellt und das Wegschreiben der Änderungen übernimmt. Zusätzlich werden weitere Hilfsfunktionen bereitgestellt, damit sich der Entwickler auf die Codemanipulation im engeren Sinne konzentrieren kann. Darüber hinaus haben wir ein XML-Konfigurationsformat entworfen, das es erlaubt, sehr häufig wiederkehrende Migrationsaufgaben rein deklarativ anzusteuern. Diese umfassen den Austausch von Typen, Austausch von Methodenaufrufen inkl. Anpassung der Parameter, Umwandlung von Konstanten, Ersetzung von Codefragmenten aufgrund von Pattern-Matching, Umschreiben von Methodendeklarationen sowie das Auskommentieren nicht mehr unterstützter Methodenaufrufe.

Nehmen wir an, eine Anwendung nutzt ein Komponenten-Framework auf Basis von Swing und daraus eine Klasse SwingTextfield. Diese soll durch ihr JavaFX-Gegenstück FxTextfield ersetzt werden:

Typen und Methoden finden

Um die beteiligten Konstrukte überhaupt erst einmal in die Hand zu bekommen, gibt es zwei Möglichkeiten: Entweder führt man eine hierarchische Suche mit der Java Model API (IProject/IJavaProject, IPackageFragment, ICompilationUnit) durch oder greift mit Hilfe des Visitor-Patterns gezielt auf einzelne Elemente zu. In der zweiten Variante wird die Klasse ASTVisitor abgeleitet und die passende Variante der Methode visit() überschrieben. In unserem Fall implementieren wir visit(SimpleType), um Referenzen auf SwingTextfield, und visit(MethodInvocation), um Aufrufe von setEnabled() zwischenzuspeichern. Das Zwischenspeichern ist auch mit Rücksicht auf den Speicherverbrauch wichtig, da geparste ASTs recht speicherintensiv sind.

Typen und Methodenaufrufe austauschen

Typreferenzen in einer CompilationUnit unit können mit dem Code in Listing 3 ausgetauscht werden.

Über eine AST-Fabrikmethode (newXYZ) wird ein neuer Knoten erstellt und beim ASTRewrite-Objekt der Austausch des alten (sourceTypeRef) durch den soeben erzeugten neuen (targetType) Knoten angefordert. Das ImportRewrite-Objekt speichert die zusätzlich erforderlichen (addImport) und obsolet gewordenen (removeImport) Importe.

  • Achtung 1: removeImport() prüft nicht, ob der Import im Zielcode tatsächlich überflüssig ist.
  • Achtung 2: Ggf. müssen auch die Konstruktor-Parameter (Listing 1 Zeile 3) auf den passend Zieltyp migriert werden.

Bei Methodenaufrufen kann die Namens-Eigenschaft direkt angepasst werden. Dies ist in der Beispielanwendung erforderlich, weil die der Methode “setEnabled“ in der Klasse Swing-Textfield korrespondierende Methode beim FxTextfield “setDisabled“ heißt. Die beiden Methoden erwarten zwar beide einen Parameter vom Typ boolean, verhalten sich jedoch unterschiedlich. Dies führt dazu, dass der boolesche Wert des Parameters invertiert werden muss. Bei derartigen Refactorings empfiehlt sich eine zusätzliche Sicherheitsüberprüfung, ob die Zielmethode setDisabled im Kontext des Zieltyps überhaupt definiert ist und ob der vorgefundene Methodenaufruf tatsächlich genau ein Argument vom Typ boolean hat.

Auskommentieren nicht mehr unterstützter Methoden

Wird eine Methode nach der Migration nicht mehr unterstützt, empfiehlt es sich, diese automatisiert auszukommentieren. Zusätzlich wird der Code mit einem TODO für ein späteres Review sowie einer @Deprecated-Annotation an der umgebenden Methode versehen (vgl. Listing 4).

Da Kommentare in JDT nur rudimentär unterstützt werden, muss man zu einem kleinen Trick greifen: ein StringPlaceholder ermöglicht es, beliebige Fragmente in den Code einzuschleusen:

Achtung: Der Entwickler muss sicherstellen, dass der eingefügte Code gültiger, compilefähiger Java-Code ist!

Über die ListRewrite-Instanz wird der Code (der TODO-Kommentar) an der richtigen Stelle platziert. Analog dazu kann auch die Deprecated-Annotation an die Methode angefügt werden.

Hinweis: Methoden wie ASTRewrite.replace(), ListRewrite.insertBefore() und ListRewrite.insertAt() akzeptieren als Argumente beliebige AST-Knoten (vom Typ ASTNode). Ob der Knotentyp im jeweiligen Kontext sinnvoll ist (im letzten Beispiel tatsächlich eine Annotation und nicht etwa eine MethodDeclaration), wird erst zur Laufzeit überprüft und ggf. mit einer Exception quittiert.

Fazit

Das einfache Beispiel lässt erahnen, welche Möglichkeiten JDT bietet. Weitere Information bietet der Artikel Unleashing the Power of Refactoring. Etwas Erfahrung vorausgesetzt, können auch große Anwendungen automatisiert auf eine neue Technologie migriert werden. Migrationen von J2EE zu EJB3.x/CDI sowie Swing auf JavaFX sind zwei Beispiele, mit denen wir in den letzten Jahren in Kontakt gekommen sind. Der dazu notwendige Code lässt sich relativ schnell implementieren, nachdem man eine gewisse Lernkurve hinsichtlich der gewöhnungsbedürftigen Sichtweise des AST auf den Code hinter sich gebracht hat. Für Einsteiger empfiehlt sich u.a. das Tutorial von Lars Vogel und Simon Scholz.

Zwei zentrale Lehren haben wir bei der praktischen Arbeit mit JDT ziehen können: Sowohl das Parsen des AST als auch das Vorhalten der geparsten Objekte im Speicher führt zu spürbaren Performanceeinbußen. Hier bietet sich zielgerichtetes Caching an.

Die Nützlichkeit von automatischen Refactorings mit JDT steht und fällt mit der Identifikation der Codestellen, welche einem bestimmten Refactoring zu unterwerfen sind und welche nicht. Gelingt hier im konkreten Projektkontext die Definition eines trennscharfen Anfassers, der auf möglichst viele Stellen zutrifft, entfaltet JDT seine ganze Mächtigkeit. Sind nur ein, zwei Stellen überhaupt betroffen, oder lässt sich für die betroffenen Codestellen nicht sauber ein gemeinsames Muster formulieren, wird man von JDT eher absehen und von Hand migrieren.


Keine Kommentare

Kontakt

OPEN KNOWLEDGE GmbH

Standort Oldenburg:
Poststraße 1, 26122 Oldenburg

Standort Essen:
II. Hagen 7, 45127 Essen