JAXenter.de

Das Portal für Java, Architektur, Cloud & Agile
X

Alle Infos zur aktuellen JavaOne gibt's in unserer Serie: JavaOne 2014.

Realisierung von Mixins mittels AOP (AspectJ) oder Quellcodemodifikation (JaMoPP)

Mixins mit Java-Bordmitteln

Unter Mixins versteht man in der objektorientierten Programmierung eine definierte Menge an Funktionalität, die man einer Klasse hinzufügen kann. Ein wesentlicher Aspekt ist, dass man sich damit bei der Entwicklung mehr auf die Eigenschaften eines bestimmten Verhaltens als auf Vererbungsstrukturen konzentriert. In Scala findet man z. B. eine Variante der Mixins unter dem Namen „Traits“. Obwohl Java keine direkte Unterstützung für Mixins bietet, lässt sich diese leicht mit ein paar Annotationen, Interfaces und etwas Toolunterstützung nachrüsten.

Gelegentlich ist in einigen Artikeln im Internet zu lesen, dass mit Version 8 Mixins Einzug in Java halten werden. Dies ist leider nicht der Fall. Ein Feature des Projekts Lambda (JSR-335 [1]) sind die so genannten „Virtual Extension Methods“ (VEM). Diese sind zwar den Mixins ähnlich, haben aber einen anderen Hintergrund und sind deutlich limitierter in Bezug auf den Funktionsumfang. Die Motivation für die Einführung der VEM ist die Problematik der Abwärtskompatibilität bei Einführung von neuen Methoden in Interfaces [2]. Da also in der nahen Zukunft nicht mit „echten“ Mixins im Java-Sprachumfang zu rechnen ist, soll in diesem Artikel aufgezeigt werden, wie man auch schon heute mit einfachen Mitteln Mixin-Unterstützung in Java-Projekten schaffen kann. Dafür diskutieren wir zwei Ansätze: Per AOP mit AspectJ [3] und per Quellcodemodifikation mit JaMoPP [4].

Warum nicht einfach Vererbung?

James Gosling, der Erfinder von Java, soll auf einer Veranstaltung auf die Frage „Was würden Sie an Java ändern, wenn Sie es neu erfinden könnten?“ geantwortet haben, „Ich würde die Klassen weglassen“. Nachdem sich das Gelächter wieder gelegt hatte, erklärte er, was er damit meinte: Die Vererbung in Java, welche mit der „extends“-Beziehung ausgedrückt wird, sollte – wo immer möglich – durch Interfaces ersetzt werden [5]. Jeder erfahrene Entwickler weiß, was er damit gemeint hat: Mit Vererbung sollte man sparsam umgehen. Es passiert leicht, dass man sie als technisches Konstrukt missbraucht, um Code wiederzuverwenden, und nicht, um damit eine fachlich motivierte Eltern-Kind-Beziehung zu modellieren. Aber selbst wenn man eine solch technisch motivierte Codewiederverwendung als legitim betrachtet, stößt man schnell an Grenzen, da Java keine Mehrfachvererbung erlaubt.

Mixins sind immer dann hilfreich, wenn mehrere Klassen ähnliche Eigenschaften haben bzw. ein ähnliches Verhalten definieren, diese aber über schlanke Vererbungshierarchien nicht ohne Weiteres sinnvoll abbildbar sind. Im Englischen sind Begriffe, die auf „able“ enden (z. B. „sortable“, „comparable“ oder „commentable“), häufig ein Indiz für Einsatzfelder von Mixins. Auch wenn man beginnt „Utility“-Methoden zu schreiben, um eine Codeduplizierung bei der Implementierung von Interfaces zu vermeiden, kann dies ein Hinweis für einen sinnvollen Anwendungsfall sein.

AspectJ und "Inter-type declarations"
AspectJ erweitert Java um die Möglichkeit, Funktionalitäten, die sich quer durch den Code ziehen und damit nicht gut innerhalb der fachlichen Klassenhierarchie abzubilden sind, in eigene Codeteile auszulagern. Häufig genannte Bereiche für solche Aspekte sind „Logging“, Behandlung von Fehlern oder Performancemessungen. Der Quellcode von Aspekten wird getrennt von Java in einer eigenen Datei mit der Endung .aj gehalten und durch Manipulation des Bytecodes zur Compile- oder Ladezeit in den originalen Java-Code „eingewebt“.
Ein weniger bekanntes Feature von AspectJ sind die so genannten „Inter-type declarations“ [6]. Damit kann man – unter anderem – neue Instanzvariablen und Methoden zu beliebigen Zielklassen hinzufügen. Neben eigenem Code kann man damit sogar Bibliotheken von Fremdanbietern um neue Funktionen erweitern. Dazu wird entweder das originale JAR entpackt, mit AspectJ modifiziert und wieder in ein neues JAR gepackt oder die Aspekte werden erst später beim Laden der 3rd-Party-Klassen hinzugefügt.

Mixins mit AOP

Eine recht einfache Möglichkeit Mixins zu realisieren, bietet das Eclipse-Projekt AspectJ mit den so genannten „Inter-type declarations“ [6]. Damit kann man – unter anderem – neue Instanzvariablen und Methoden zu beliebigen Zielklassen hinzufügen. Dies soll im Folgenden anhand eines kleinen Beispiels in Listing 1 gezeigt werden. Dabei nutzen wir die folgenden Begrifflichkeiten:

  • Basis-Interface: Beschreibt das gewünschte Verhalten. Klassen, die das Mixin nicht nutzen sollen, können dieses Interface verwenden.
  • Mixin-Interface: Zwischeninterface, welches im Aspekt genutzt und von Klassen implementiert wird, die das Mixin verwenden sollen.
  • Mixin-Provider: Aspekt, der die Implementierung für das Mixin bereitstellt.
  • Mixin-User: Klasse, die ein oder mehrere Mixin-Interfaces nutzt (implementiert).
/** Basis-Interface */
public interface Named {
  public String getName();
}

/** Mixin-Interface */
public interface NamedMixin extends Named {
}

/** Mixin-Provider */
public aspect NamedAspect {
    private String NamedMixin.name;
    public final void NamedMixin.setName(String name) {
        this.name = name;
    }
    public final String NamedMixin.getName() {
        return name;
    }   
}

/** Mixin-User */
public class MyClass implements NamedMixin {
  // Kann weitere Methoden haben und mehrere Mixins implementieren
}

Listing 1 zeigt damit ein komplettes AOP-basiertes Mixin-Beispiel. Wenn AspectJ korrekt eingerichtet ist, sollte der folgende Quelltext fehlerfrei kompilieren und ablaufen:

MyClass myObj = new MyClass();
myObj.setName("Abc");
System.out.println(myObj.getName());

Mit der AOP-Variante kann man schon recht komfortabel arbeiten, aber es gibt auch einige Nachteile, die hier nicht verschwiegen werden sollen. Zunächst können die Inter-type-Deklarationen nicht mit generischen Typen in der Zielklasse umgehen. Das ist in vielen Fällen nicht zwingend notwendig, kann aber ganz praktisch sein. So könnte man das „Named“-Interface auch gut mit einem generischen Typ <T> anstelle von String definieren. Es würde dann das Verhalten für beliebige Namenstypen definieren. Die verwendende Klasse könnte dann festlegen, wie der Typ des Namens aussehen soll. Ein weiterer Nachteil ist, dass die von AspectJ generierten Methoden einer eigenen Namenskonvention folgen. Dies erschwert z. B. die Untersuchung der Klassen per Reflection, da man mit Methodennamen wie ajc$interMethodDispatch ... rechnen muss. Zu guter Letzt: Ohne Unterstützung der Entwicklungsumgebung sieht man den Quellcode in der Zielklasse nicht und ist alleine auf die Interfacedeklaration angewiesen. Dies kann man allerdings auch als Vorteil ansehen, da die nutzenden Klassen weniger Code enthalten.

Auftritt: Java Model Parser and Printer (JaMoPP)

Eine Alternative zur Realisierung von Mixins mit AspectJ bietet der Java Model Parser and Printer (JaMoPP). Vereinfacht gesagt kann JaMoPP Java-Sourcecode einlesen, als Objektgraph im Speicher darstellen und wieder in Text umwandeln, d. h. schreiben. Man kann mit JaMoPP demnach programmatisch Java-Code verarbeiten und so beispielsweise Refaktorings automatisieren oder eigene Codeanalysen implementieren. Technologisch basiert JaMoPP auf dem Eclipse Modeling Framework (EMF) [7] und EMFText [8]. JaMoPP wird gemeinsam von der TU Dresden und der DevBoost GmbH entwickelt und ist als Open-Source-Projekt frei auf GitHub verfügbar.

Mixins mit JaMoPP

Im Folgenden wollen wir das Beispiel aus den AOP-Mixins aufgreifen und etwas erweitern. Dazu definieren wir zunächst einige Annotationen:

  • @MixinIntf: Kennzeichnet ein Mixin-Interface.
  • @MixinProvider: Kennzeichnet eine Klasse, welche die Implementierung für ein Mixin bereitstellt. Als einziger Parameter wird das implementierte Mixin-Interface angegeben.
  • @MixinGenerated: Markiert Methoden und Instanzvariablen, die durch das Mixin generiert wurden. Einziger Parameter ist die Klasse des Mixin-Providers.

 Wir erweitern im Folgenden die Interfaces und Klassen aus Listing 1 auch gleich noch um einen generischen Typ <T > für den Namen. Erst die das Mixin nutzende Klasse legt dann fest, welchen konkreten Typ der Name tatsächlich haben soll.

/** Basis-Interface (erweitert um generischen Parameter) */
public interface Named<T> {
  public T getName();
}

/** Mixin-Interface */
@MixinIntf 
public interface NamedMixin<T> extends Named<T> {
}

/** Mixin-Provider */
@MixinProvider(NamedMixin.class)
public final class NamedMixinProvider<T> implements Named<T> {

  @MixinGenerated(NamedMixinProvider.class)
  private T name;

  @MixinGenerated(NamedMixinProvider.class)
  public void setName(T name) {
    this.name = name;
  }

  @Override
  @MixinGenerated(NamedMixinProvider.class)
  public T getName() {
    return name;
  }
  
}

/** Spezieller Namens-Typ (als Alternative zu String) */
public final class MyName {
  private final String name;

  public MyName(String name) {
    super();
    if (name == null) {
      throw new IllegalArgumentException("name == null");
    }
    if (name.trim().length() == 0) {
      throw new IllegalArgumentException("name is empty");
    }
    this.name = name;
  }

  @Override
  public String toString() {
    return name;
  }
}

In der Klasse, die das Mixin nutzen soll, wird nun wieder das Mixin-Interface implementiert, wie in Listing 3 dargestellt. Um die Felder und Methoden, die vom Mixin-Provider definiert werden, in die Klasse MyClass „einzumischen“, kommt ein Codegenerator zum Einsatz. Dieser modifiziert mithilfe von JaMoPP die Klasse MyClass und fügt die vom Mixin-Provider bereitgestellten Instanzvariablen und Methoden hinzu.

/** Mixin-User */
public class MyClass implements NamedMixin<MyName> {
  // Kann weitere Methoden haben und mehrere Mixins implementieren
}

Der Codegenerator geht dabei wie folgt vor. Er liest den Quellcode jeder Klasse ein, ähnlich wie es der normale Java-Compiler macht, und untersucht dabei die Menge der implementierten Interfaces. Ist ein Mixin-Interface dabei, d. h. ein Interface mit der Annotation @MixinIntf, wird der entsprechende Provider dazu gesucht und die Instanzvariablen sowie die Methoden in die das Mixin implementierende Klasse kopiert.

Um die Generierung des Mixin-Codes anzustoßen, gibt es aktuell zwei Möglichkeiten: Über ein Eclipse-Plug-in direkt beim Speichern oder als Maven-Plug-in im Rahmen des Builds. Eine Installationsanleitung und den Quellcode der beiden Plug-ins findet man auf GitHub im kleinen „SrcMixins4J“-Projekt [9]. Dort ist auch ein Screenvideo abrufbar, welches den Einsatz des Eclipse-Plug-ins demonstriert. Wie die modifizierte Zielklasse dann aussieht, ist in Listing 4 zu sehen.

Abb. 1: SrcMixins4J-Eclipse-Plug-in (Screenvideo [10])

/** Mixin-User */
public class MyClass implements NamedMixin<MyName> {

  @MixinGenerated(NamedMixinProvider.class)
  private MyName name;

  @MixinGenerated(NamedMixinProvider.class)
  public void setName(MyName name) {
    this.name = name;
  }

  @Override
  @MixinGenerated(NamedMixinProvider.class)
  public MyName getName() {
    return name;
  }
}

Wird das Mixin-Interface wieder aus der implements-Sektion entfernt, werden automatisch alle Felder und Methoden gelöscht, die mit @MixinGenerated des Providers annotiert sind. Generierter Code kann jederzeit überschrieben werden, indem man die @MixinGenerated-Annotation entfernt.

Fazit

Da mit nativer Unterstützung von Mixins im Java-Sprachstandard in absehbarer Zeit nicht zu rechnen ist, kann man sich momentan nur mit etwas AOP oder Quellcodegenerierung behelfen. Welche der beiden Varianten man wählt, hängt im Wesentlichen davon ab, ob man den Mixin-Code lieber getrennt vom eigenen Anwendungscode halten oder ihn direkt in der jeweiligen Klasse haben möchte. Auf jeden Fall erhöht man die Geschwindigkeit der Entwicklung damit deutlich und konzentriert sich weniger auf Vererbungshierarchien als vielmehr auf die Definition fachlichen Verhaltens. Beide Ansätze sind sicher nicht perfekt. Insbesondere werden Konflikte nicht automatisch aufgelöst. So führen Methoden mit gleicher Signatur aus verschiedenen Interfaces, die durch unterschiedliche Mixin-Provider bereitgestellt werden, bei einer Klasse, die beide Mixins nutzt, zu einem Fehler. Wer hier mehr will, muss auf eine andere Sprache mit nativer Mixin-Unterstützung wie z. B. Scala umsteigen.

 

Kommentare

von Fabian (Unveröffentlicht) am
"Da mit nativer Unterstützung von Mixins im Java-Sprachstandard in absehbarer Zeit nicht zu rechnen ist".. Was ist mit Interface Default Implementierungen in Java 8?

von Michael (Unveröffentlicht) am
Die Interface Default Implementierungen sind die am Anfang des Artikels beschriebenen „Virtual Extension Methods“ (VEM). Leider sind diese nicht so mächtig wie Mixins. Man kann z.B. keine Variable zu einer Klasse hinzufügen.

Bild des Benutzers Trepper
von Trepper am
Mixins mit Bordmitteln? Seit wann ist AsepectJ oder JaMoPP im JDK enthalten?! Immerhin wurde die Empfehlung gegeben, gleich auf Scala zu setzen, wenn man ernsthaft mit Mixins arbeiten will. Eine Sprache prägt das, was mit ihr ausgedrückt wird; und Java und Mixins sind bisher eben zwei Dinge, die nicht gut zusammenpassen. Mit Java 8 kommen in Form von virtuellen Erweiterungsmethoden Mixins-Light, aber so richtig rund ist das Konzept erst mit einer Sprache, die das Konzept von Grund auf unterstützt, wie eben Scala.

von Michael Schnell (Unveröffentlicht) am
Mit "Bordmitteln" ist gemeint, was in Java (inkl. Libraries) realisierbar ist :-) Natürlich ist es besser wenn eine Sprache benötigte Features direkt und ohne Umwege "nativ" unterstützt.

Bild des Benutzers Trepper
von Trepper am
Dann ergibt das Wort "Bordmittel" aber keinen Sinn mehr, denn was würde nach dieser Definition dann nicht mehr unter "Bordmittel" fallen?

von Det (Unveröffentlicht) am
Richtig, Trepper. Der Definition nach ist Scala ein "Bordmittel", denn es ist a) eine Library, und während JaMoPP einen speziell annotierten Java-Code einliest und neuen Code generiert, hat man mit Scala stattdessen eine b) Mixin-Sprache, die auch Code generiert ... Bytecode. Und der läuft auf einer unveränderten JVM. Bordmittel quasi :-)

Ihr Kommentar zum Thema

Als Gast kommentieren:

Gastkommentare werden nach redaktioneller Prüfung freigegeben (bitte Policy beachten).