Mit AspectJ und Equinox Weaving Probleme in Plug-ins umgehen

Wenn der Softwarebaukasten klemmt

Andreas Graf
©Shutterstock.com/Oksana Kuzmina

Die vielen Projekte der Eclipse Foundation bilden einen reichhaltigen Softwarebaukasten, mit dem Entwickler schnell neue Anwendungen aufbauen können. Nur manchmal klemmen die Teile und passen nicht ganz zusammen. Doch auch hierfür findet sich bei Eclipse ein brauchbares Werkzeug.

Eclipse hat sich nicht nur als Programmierumgebung sondern auch als Plattform für die unterschiedlichsten Engineering-Werkzeuge in vielen Branchen etabliert. Luft- und Raumfahrt, Automobilindustrie und Bahn setzen mit Initiativen wie Polarsys, der Automotive Industry Working Group und anderen auf Eclipse als strategische Technologie. Die Attraktivität liegt nicht zuletzt in dem umfangreichen Ökosystem von Projekten, die sich kombinieren lassen und so die Entwicklungskosten deutlich reduzieren. Idealerweise lassen sich Infrastrukturkomponenten wie EMF, Sphinx, CDO, EMF Compare zusammenstecken und als Grundlage für Eigenentwicklungen und zur Integration mit existierenden Anwendungen wie Anforderungserfassung (RMF), Modellierung (UML), Reporting (BIRT) etc. verwenden. Dies klappt in den meisten Fällen recht gut – aber manchmal enthalten die Projekte noch Bugs oder spielen doch nicht zu hundert Prozent zusammen. AspectJ und Equinox Weaving bieten hier gute Dienste, um solchen Fällen mit wenig Aufwand begegnen zu können.

Beispiel: RMF, CDO und EMF – Exception

Im öffentlich geförderten Forschungsprojekt haben wir ein prototypisches integriertes Werkzeug für die Entwicklung von Automotive-Software aufgebaut. Das Produkt kombiniert die unterschiedlichsten Projekte und eigenen Entwicklungen und bietet Editoren für den Entwicklungsprozess von Anforderungen bis hin zu Implementierung, kombiniert mit Änderungsmanagement und Traceability. Da die Industrie für wichtige Unternehmensdaten aus Gründen des Informationsschutzes oft eine 3-Tier-Architektur mit Datenbank-Backend fordert, integrieren wir CDO mit einer SQL-Datenbank als Backend. Als wir bestehende EMF-Modelle direkt in CDO bearbeiten wollten, stießen wir auf Integrationsprobleme.
CDO hat in den Versionen 4.2 und 4.3 wichtige neue Funktionalität bekommen. So ist der Transfer von XML-basierten EMF-Modellen in ein CDO Repository deutlich benutzerfreundlicher geworden. In unserem Fall konnten wir die im Requirements Modeling Framework (RMF) modellierten Anforderungen aus dem Workspace einfach mit Drag and Drop auf ein CDO Repository ziehen und der neue Transfermanager importierte die Modelle.
Die Ernüchterung kam für uns, als wir die Daten direkt aus dem CDO Repository bearbeiten wollten. Obwohl CDO-Modelle eigentlich kompatibel zum EMF-API sind und CDO uns bequemerweise die gültigen Editoren anbietet, wirft Eclipse beim Öffnen des Editors eine Null-Pointer-Exception.

Abb. 1: Öffnen des CDO-Editors

Die Analyse mit dem Debugger ergab, dass der Null-Wert in EMF in der Methode EditUiUitl.getURI() entsteht, denn sie wird mit einem CDOLobEditorInput aufgerufen, der in dem Codefragment nicht behandelt wird, da er nicht von IEditorInput abgeleitet ist (Listing 1).

public static URI getURI(IEditorInput editorInput)
{
  URI result = null;
  if (EMFPlugin.IS_ECLIPSE_RUNNING)
  {
    result = EclipseUtil.getURI(editorInput);
  }
  if (result == null)
  {
    if (editorInput instanceof URIEditorInput)
    {
      result = ((URIEditorInput)editorInput).getURI().trimFragment();
    }
    else
    {
      result = URI.createURI(editorInput.getName());
    }
  }
  return result;
}

Dabei fehlt zu der notwendigen Abbildung nicht viel, denn von CDOLobEditorInput könnten wir einen korrekten URI erhalten (Listing 2).

if (ip instanceof CDOLobEditorInput) {
  CDOLobEditorInput cdoip = (CDOLobEditorInput) ip;
  return cdoip.getResource().getURI();
}

An dieser Stelle bietet EMF leider keine Abbildung über den sonst dafür vorgesehenen Adaptermechanismus. Dabei funktionieren sowohl EMF, RMF als auch CDO für sich gesehen korrekt – nur in dieser Kombination ruckelt es.

Aufmacherbild: little cheerful child with construction set over white background von Shutterstock / Urheberrecht: Oksana Kuzmina

[ header = Seite 2: Der „korrekte Weg“ ]

Der „korrekte Weg“

Auf jeden Fall sollte der empfundene Mangel über Eclipse Bugzilla oder die Diskussionsforen mit den Committern der entsprechenden Projekte diskutiert werden. Um diese paar Codezeilen einzubringen, müssten wir nun:

• Das entsprechende EMF-Projekt auschecken und in unseren Entwicklungs-Workspace übernehmen.
• Den Quellcode modifizieren. Wenn wir obige Lösung wählen, führen wir eine Abhängigkeit zwischen EMF und CDO ein, was nicht korrekt wäre. Also sollten wir einen Adaptermechanismus einbauen und in einem weiteren Plug-in die Funktionalität des Adaptierens von CDOLobEditorInput auf URIEditorInput oder Ähnliches umsetzen.
• Die Lösung im lokalen Workspace testen.
• Die so modifizierten und neu erstellten Projekte in unseren Build mit einbauen.
• Testcases bereitstellen und integrieren.
• Unsere Patches manuell in neue Versionen von EMF nachpflegen.
• Unsere Änderungen zurück ins Projekt committen (wenn sie denn angenommen werden).

Das ist einiges an Arbeit. Insbesondere wissen wir zu diesem Zeitpunkt noch nicht, ob sich nicht vielleicht nach dem Fixen dieses Problems noch mehr im weiteren Verlauf ergeben. Ein einfacherer Mechanismus wäre nützlich.

AspectJ und Equinox Weaving

Hier kommen AspectJ und Equinox Weaving ins Spiel. Diese ermöglichen es, beim Laden einer Klasse deren Code zu modifizieren. Diesen Mechanismus machen wir uns zunutze, um unsere Implementierung der Methode getURI() einzuschieben. Bevor wir auf die Details eingehen, hier eine Übersicht: Zuerst definieren wir den Punkt, an dem wir uns einhängen wollen, den so genannten „Pointcut“ (Listing 3).

pointcut cdoPatch(IEditorInput ip) : 
execution(public static URI EditUIUtil.getURI(IEditorInput))  && args(ip);

Ohne schon auf alle Argumente eingehen zu wollen, sehen wir hier die Signatur der Methode, die wir modifizieren wollen. Mit einem so genannten Advice hängen wir uns jetzt ein (Listing 4).

URI around(IEditorInput ip): cdoPatch(ip ) {
  if (ip instanceof CDOLobEditorInput) {
    CDOLobEditorInput cdoip = (CDOLobEditorInput) ip;
    return cdoip.getResource().getURI();
  }
  return proceed(ip);
}

Wir hängen uns also ein, wenn der Pointcut cdoPatch(), d. h. die Methode EditUIUtil.getURI() ausgeführt wird. Die JVM führt jetzt unseren Code aus und gibt somit den URI aus dem CDOLobEditorInput zurück. Falls der Eingabewert nicht von diesem Typ ist, übergibt der Aspekt über den speziellen Aufruf proceed() den Kontrollfluss an die Originalfunktion. Und voila, damit öffnet sich der RMF-Editor und wir können direkt auf CDO arbeiten.

Über AOP/AspectJ

AspectJ ist eine Java-Variante der aspektorientierten Programmierung, bei der die so genannten querschnittlichen Belange („Cross-Cutting Concerns“ oder eben auch „Aspects“) von der Applikationslogik getrennt betrachtet werden können. Somit steigt die Modularität der Software. Das klassische Beispiel hierfür ist das Logging, denn der verwendete Logging-Ansatz hat Auswirkungen auf die gesamte Anwendung. Oft werden die Aufrufe zum Logging-Framework innerhalb der Methoden der Funktionslogik implementiert. Könnten wir diese Aufrufe herausziehen, würde nicht nur der Code klarer, sondern das Logging-Framework auch besser austauschbar sein. Weitere Beispiele für derartige Belange sind unter anderem Persistenz und Caching. AspectJ bietet hierzu eigene Konzepte.
Das grundlegende Konzept ist der „Join Point“, der einen bestimmten Punkt im Ablauf eines Programms bezeichnet. Die Typen der unterstützten Join Points hängen dabei sowohl von der verwendeten Programmiersprache als auch der Werkzeugunterstützung ab. Für unseren Anwendungsfall reicht es aus, die Ausführung einer Methode zu beeinflussen. AspectJ unterstützt aber auch weitere, wie z. B. den Zugriff auf ein Klassenattribut, den Aufruf eines Konstruktors etc.

Über die Pointcuts werden die Join Points bestimmt, die durch AspectJ modifiziert werden sollen. Im obigen Beispiel definieren wir mit dem Schlüsselwort execution, dass wir uns bei der Ausführung der Methode einhängen wollen. Dies ist leicht zu verwechseln mit dem Schlüsselwort call, das den Aufruf der Methode spezifiziert. Der Unterschied liegt darin, dass der Kontext von execution die aufgerufene Klasse ist, der Kontext von call jedoch der Aufrufer. In unserem Fall wäre call ein deutlicher Overhead, denn AspectJ müsste in allen Bundles und Klassen nach dem Aufruf suchen – mit execution muss es nur die modifizierte Klasse erwischen.
Manchmal soll eine Modifikation nur erfolgen, wenn die Methode von einem bestimmten Paket oder einer bestimmten Klasse aus aufgerufen wird. Hierzu können wir die Aufrufhierarchie mit cflow definieren. In diesem Beispiel definieren wir einen Pointcut für die Methode createResourceSet(), der nur angezogen wird, wenn er in der Hierarchie auf eine Ausführung der Methode getSourceResourceSet() folgt (Listing 5).

pointcut cdoCreateRSPatch(CDOTransfer.ModelTransferContext t, CDOTransferSystem system): 
execution(protected ResourceSet createResourceSet(CDOTransferSystem )) && 
this(t) && args(system) && 
cflow(execution(public final ResourceSet CDOTransfer.ModelTransferContext.getSourceResourceSet() ) );

Ein „Advice“ bestimmt dann letztendlich, was bei Erreichen des Pointcuts tatsächlich geschehen soll und enthält auch den auszuführenden Code. Wir können hier bestimmen, ob der Advice vor, nach, oder statt dem Join Point ausgeführt werden soll. Da wir unsere eigene Implementierung einbringen wollen, wählen wir Letzteres, was wir mit around definieren. Wie das Beispiel zeigt, handelt es sich auch bei dieser Sprache um Java, das um weitere Schlüsselwörter angereichert ist. Der Ausdruck proceed() ruft die Methode auf, die dem ausgewählten Join Point entspricht und delegiert somit zum originalen Code.

Die hier verwendeten Konstrukte sind nur eine Teilmenge von AspectJ – sie reichen aber für unseren Anwendungsfall aus. Detaillierte Informationen finden sich auf der Projektseite. Eine detaillierte Darstellung bietet auch das Buch „AspectJ Cookbook“ von Russ Miles, das bei O’Reilly erschienen ist.AspectJ integriert sich für den Entwickler sowohl in die Java-Entwicklungsumgebung als auch in einen Maven-basierten Build. Zur Laufzeit unterstützt es einfache Java- und OSGi/RCP-Anwendungen. Für unseren Einsatzzweck machen wir uns zunutze, dass AspectJ zwei Strategien zur Veränderung des Zielcodes unterstützt: Zum einen zur Compilezeit, aber auch die Modifikation bereits kompilierter Klassen zum Zeitpunkt des Classloadings. Damit können wir bestehende Bundles modifizieren.

[ header = Seite 3: AspectJ und Eclipse ]

AspectJ und Eclipse

Um AspectJ zu nutzen, ist etwas Konfiguration erforderlich. Zuerst installieren wir AspectJ von der Updatesite in die Entwicklungsumgebung. Aktuell wird das JDT in der Kepler-Version unterstützt, das Projektteam arbeitet aber schon an einer Luna-Version. Nun erstellen wir ein Plug-in-Projekt für unsere Aspekte. Wenn wir den Aspekt geschrieben haben, müssen wir in unserem Manifest angeben, welche Bundles modifiziert werden. Dies geschieht im Eclipse-Supplement-Bundles-Eintrag (Listing 6).

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Patching
Bundle-SymbolicName: de.itemis.imes.patching;singleton:=true
Bundle-Version: 1.0.0.qualifier
Export-Package: de.itemis.imes.patching
...
Eclipse-SupplementBundle: org.eclipse.emf.edit.ui
Bundle-ActivationPolicy: lazy

Dann müssen wir sicherstellen, dass die relevanten Bundles automatisch und zum richtigen Zeitpunkt gestartet werden. Das stellen wir in der Produktdefinition oder in der Launch-Konfiguration ein (Tabelle 1).

Bundle Run-Level Autostart
org.aspectj.runtime 2 True
org.aspectj.weaver 2 True
org.eclipse.equinox.weaving.aspectj 2 True

Tabelle 1: Bundles korrekt starten

Zu guter Letzt müssen wir über JVM-Argumente dem OSGi-System noch mitteilen, dass es den so genannten Weaving Hook, der von AspectJ verwendet wird, um sich in das Laden von Klassen und Bundles einzuhängen, zu aktivieren hat. Über weitere JVM-Argumente legen wir fest, wie viel Ausgabe das AspectJ-Framework auf die Konsole schreibt – dies ist besonders nützlich, um die Konfiguration beim ersten Mal zum Laufen zu bekommen (Listing 7).

-Dosgi.framework.extensions=org.eclipse.equinox.weaving.hook 
-Daj.weaving.verbose=true 
-Dorg.aspectj.weaver.showWeaveInfo=true 
-Dorg.aspectj.osgi.verbose=true

Performanz

Nachdem durch AspectJ beim Laden der Klassen Änderungen durchgeführt werden, stellt sich die Frage nach den Auswirkungen auf die Laufzeitgeschwindigkeit. Diese hält sich tatsächlich in Grenzen. Zusätzlich bietet das Framework einen Caching-Mechanismus, der bei Neustarts geringe Zeiten ermöglicht. Im IMES-Projekt bringen wir fünfzehn Advices beim Start der RCP-Anwendung ein und stellen keine nennenswerte Verzögerung fest. Die Ziel-Bundles stammen dabei aus EMF, CDO, Sphinx, und DAMOS.

Kontrakte

Ein weiterer nützlicher Einsatz neben dem Austausch von Code ist die Überprüfung der Korrektheit von Methodenparametern. Programmiersprachen, die „Design by Contract“ unterstützen, bieten Sprachkonstrukte, mit denen die Entwickler für Methoden Vor- und Nachbedingungen und Invarianten definieren können. Werden diese verletzt, so wird eine entsprechende Fehlerbehandlung angestoßen. Dies machen wir uns zunutze, um mit AspectJ zur Laufzeit falsche API-Verwendungen finden zu können. Nehmen wir an, eine Methode C.m() wirft eine Null-Pointer-Exception beim Zugriff auf ein Feld C.a. Der Fehler liegt jetzt nicht notwendigerweise in m, sondern kann in einem Codefragment liegen, dass beliebige Zeit vorher lief und a auf Null gesetzt hat. Wenn wir den Fehler reproduzieren können, verwenden wir einfach den Debugger, um festzustellen, wann und wo der falsche Wert gesetzt wird.
Mit AspectJ haben wir zusätzliche Möglichkeiten – denn mit einem Advice, das auf setA() oder sogar auf Modifikationen von a angesetzt ist, können wir entsprechende Aufrufe auch ohne Debugger identifizieren und protokollieren. Das macht die Suche sporadischer Fehler bei Anwendungen, die bereits im Einsatz sind, deutlich leichter. Darüber hinaus können wir die Softwarequalität steigern, in dem wir zum Softwaretest mit AspectJ Vor- und Nachbedingungen für unseren eigenen Code überprüfen können, dann aber für die Produktivversion eine Version ohne AspectJ Bundle ausliefern und somit keine Performanzverluste durch die Überprüfung mehr in Kauf nehmen müssen.

Nachteile

Der verwendete Ansatz bringt aber auch einige Nachteile mit sich. Verwendet man AspectJ, um orthogonale Aspekte wie Logging, Persistenz etc. einzubringen, kann es die Lesbarkeit des Codes erhöhen. In unserem Fall verwenden wir es aber, um die Ablauflogik des Programms zu modifizieren. Das führt natürlich dazu, dass der tatsächliche Ablauf nicht mehr alleine an Hand des Quellcodes nachvollzogen werden kann. Ein Entwickler, der versucht, den Code zu analysieren, ohne die AspectJ-Funktionalität im Hinterkopf zu haben, wird sich in viele falsche Richtungen bewegen. Der AspectJ-Ansatz muss daher dringend neuen Teammitgliedern vermittelt und auch immer in Erinnerung behalten werden. Der Debugger ist in diesem Fall keine große Hilfe. Wir haben es nicht geschafft, die Breakpoints so zu konfigurieren, dass beim Eintritt einer Methode der Debugger automatisch in den Advice springt. Hier müssen wir auch den Breakpoint in der Aspektklasse setzen.
Interessant ist auch die Beziehung zur Eclipse Public License. Diese ermöglicht es, die darunter veröffentlichten Projekte auch in kommerziellen Produkten einzusetzen. Wird allerdings ein EPL-Projekt modifiziert, so ist das modifizierte Projekt wiederum unter der EPL zu veröffentlichen. Das birgt eine interessante rechtliche Frage für die Modifikationen mit AspectJ, denn der Entwickler sieht auf Workspace-Ebene einfach das Original- und das AspectJ-Projekt, das eine Abhängigkeit darauf hat – wie bei jeder Verwendung eines Open-Source-Projekts auch. Zur Laufzeit aber modifiziert AspectJ die Klassen des Originalprojekts. Ohne eine juristische Bewertung abgeben zu wollen, liegt der Schluss nahe, dass damit der entsprechende Aspekt auch unter der EPL zu veröffentlichen wäre (aber nicht das Gesamtprodukt). Abgesehen davon profitiert von einer Veröffentlichung dann auch die Community um das Originalprojekt. Sie kann es weiterentwickeln und somit in Zukunft die Patches eventuell überflüssig werden lassen.
Denn ein derartiger schneller Fix birgt natürlich die Gefahr, dass er genau ein solcher bleibt. Wenn die Klippen umschifft sind und die gewünschte Funktionalität umgesetzt ist, erfordert es Disziplin, um den Quick Fix nicht auf immer einen solchen bleiben zu lassen. Dringend erforderlich ist es, die archetektonisch und konzeptionell richtige Lösung zu suchen. Nur wenn diese nicht mit vertretbarem oder organisatorischem Aufwand zu erreichen ist, darf diese Umsetzung als finale Lösung verstanden werden.

Geschrieben von
Andreas Graf
Andreas Graf
Andreas Graf ist Business Development Manager Automotive bei itemis.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: