JEXP                       JEXP

Vorstellung

Hallo erstmal. Sie finden es sicher ungewöhnlich, auf diesen Seiten einen neuen Namen zu lesen. Heinz Kabutz hat nach 3 Jahren "Effective Java" Kolumnen neben seinen Tätigkeiten als Trainer, Consultant, Speaker und "Java Specialists Club"-Betreiber nicht mehr die Zeit und Muße, umfassende Kolumnen über dieses spannende Thema zu schreiben. Natürlich wird er weiterhin im Java Specialists Newsletter darüber und über viele weitere Details Java’s und der JVM berichten. Dankenswerter Weise hat er mich der Redaktion als neuen Kolumnen-Autor empfohlen. Heinz und ich kennen uns schon eine Weile, haben uns im letzten Jahr bei ihm auf Kreta getroffen und immer viel Spass beim Diskutieren über JVM Themen gehabt.

Ich hoffe, ich kann die hohen Ansprüche, die Heinz an diese Kolumne geschaffen hat gerecht werden und immer wieder mit spannenden Artikeln aus den Tiefen des Programmieralltags aufwarten.

Auf jeden Fall freue ich mich über Feedback per E-Mail (javaspektrum@jexp.de), Twitter (@mesirii) oder GitHub Kommentar - Ich werde die Code-Beispiele auf http://github.com/jexp/javaspektrum veröffentlichen.

Einführung AOP

Wir beginnen mit etwas leichter Kost zum Aufwärmen. Ich möchte zeigen, was sich in der Aspekt Orientierten Programmierung (AOP) in den letzten Jahren getan hat und wie man aus einem simplen POJO einen universalen Superhelden machen kann, ohne den Java-Code anzufassen. Das ist natürlich ein Extrembeispiel, soll aber die Möglichkeiten aufzeigen, die zur Verfügung stehen.

Aspektorientierung hat schon ein paar Jahre auf dem Buckel. Seit es 1980 von Gregor Kiczales und Kollegen in Xerox PARC als solches ersonnen wurde, hat es in den verschiedensten Sprachen seine Höhen und Tiefen erlebt.

Wie mit jeder Technologie gab es nach Anlaufschwierigkeiten einen ziemlichen Hype um AOP, der einem pragmatischen Einsatz Platz gemacht hat. Heutzutage wird es meist in Frameworks wie Spring unter der Haube eingesetzt um die typischen übergreifenden Belange (cross-cutting-concerns), wie Transaktionen, Logging und Security abzuhandeln. Aber auch exotischere Anwendungen, wie Dependency Injection in per "new" erzeugten POJOs (annotiert mit @Configurable) und die massive Nutzung von Inter Type Declarations in Spring ROO sind fest etabliert.

Ich möchte die Gelegenheit nutzen um nach einer kurzen Wiederholung der Begrifflichkeiten und Bestandteile von AOP (insbesondere AspektJ) auf die aktuellen Entwicklung, neuen Möglichkeiten der Pointcut-Sprache, neue Fähigkeiten und auch Implementierungsdetails einzugehen. In anderen (dynamischen) Sprachen werden solche Anpassungen mittels Metaprogrammierung angegangen oder sind durch die Struktur der Sprache und Sprachkonstrukte (LISP, Scala) einfach umsetzbar.

Zum Tooling - AspectJ war schon immer etwas schwergewichtig bezüglich der notwendigen Tools. Zum einen sind da der Compiler (ajc), Runtime (aspectjrt.jar) und Weaver (ajc oder LTW). Zum anderen die IDE-Unterstützung, die in Eclipse traditionell sehr gut durch die AJDT abgedeckt wird (Installation von AJDT von http://download.eclipse.org/tools/ajdt/36/dev/update). Ein interessanter Fakt am Rande - AJDT sind eigentlich nur die JDT (Java Development Tools) mit Aspekten angereichert, so dass sie alle AspectJ Besonderheiten unterstützen. IntelliJ IDEA hat nur rudimentären AspectJ Support, der laut JetBrains aber im Rahmen der Spring ROO Unterstützung bis zur Version 11 massiv verbessert werden soll.

Ich habe für die Beispiele STS mit aktuellstem AJDT Plugin, sowie gradle eingesetzt, dessen AspectJ Unterstützung sich mit einem simplen Plugin realisieren lässt, das auf dem Ant-Task basiert.

Was ist AOP für ein Paradigma? Wie in alle anderen Softwareentwicklungs-Paradigmen wird eine Möglichkeit geboten, Programme besser zu strukturieren und modularisieren. Besonderer Wert liegt dabei auf den Aspekten, die sich auf die gleichen Belange beziehen, aber im gesamten Programmcode verstreut sind und immer wieder eingebunden werden. Diese, auch "cross-cutting-concerns" genannten, übergreifenden Programmbestandteile vernünftig zu separieren und dann nach Bedarf automatisch an die Stellen zu verteilen, wo sie benötigt werden, ist der Kern von AOP. Desweiteren können statische Strukturen verändert und erweitert, sowie die Einhaltung von Architektur-Regeln angemahnt bzw. erzwungen werden.

Begrifflichkeiten

Wenn es um AOP geht sind, wird meist über 4 Dinge gesprochen - JoinPoints, PointCuts, Advices und Aspects.

JoinPoints sind alle möglichen Stellen im Programm, an denen man Strukturen und Abläufe verändern kann. Variablenzugriffe, Methodenaufrufe, Konstruktion von Objekten, sowie die Strukturen innerhalb von Objekten oder Methoden sind Beispiele dafür.

PointCuts sind Selektionen dieser JoinPoints. Ähnlich wie SQL Abfragen oder CSS Selektoren bilden sie eine Abfragesprache mit der man aus der Vielzahl von möglichen JoinPoints in einem Programm die heraussuchen kann, an denen die übergreifende Funktionalität eingefügt werden soll. Pointcuts gibt es in verschiedensten Ausprägungen, die mittels Operatoren (&&,||, !) miteinander kombiniert werden können. Als Platzhalter werden ".." für beliebig viele und "*" für ein Element genutzt. Subklassen werden mit "+" eingeschlossen. Ein kleiner, auszugweiser Überblick folgt:

  • Typ-Signatur: [Annotation] [Package].Typ z.B. @Entity com.example.domain.*

  • Methoden-Signatur: [Modifier] [Annotation] [ReturnTyp] [Package].Typ.Methode(Parameter) [throws Exception] z.B. public @Request (*) com.example.web.*Controller.handle*@RequestParameter (@Comparable \*,..)

  • Feld-Signatur: [Modifier] [Annotation] [Typ] [Package].Typ.Feld

    • z.B. !static @NotNull Object+ @Entity *.*

  • Ausführung von Methoden/Konstruktoren: execution(Methoden-Signatur), call(Methoden-Signatur)

  • Zugriff auf Variablen: set() get()

  • Initialisierung: staticinitialization(Typ), (pre)initialization(Konstruktor-Signatur)

  • Exception-Handling: handler(Exception-Signatur)

  • Ausführungskontext: cflow(), cflowbelow()

  • strukturell: within(Typ), withinCode(Methoden-Signatur)

  • Kontext binden: target(Instanz) this(Instanz), args(Argumente), @this(Annotation der Instanz), …​

Advices hingegen sind der Code, der an den gewünschten (durch Pointcuts selektierten) Stellen eingebracht wird, sie können davor, danach oder stattdessen (before, after, around) angewandt werden. Mit der mächtigsten around-Advice können die anderen Typen ebenfalls abgebildet werden. Man hat die Wahl, den umschlossenen Code 0..n mal auszuführen. Sie ermöglicht so spannende Anwendungen wie Caching, Retry, Transaktionen, Locking und zusätzliches Exception Handling.

Beispiel, für simplen Argument-Check für Setter mit @NotNull:

` before(arg) : call(@NotNull void .set(*)) && args(arg) { if (arg == null) { throw new IllegalArgumentException("Methode: "thisJoinPointStaticPart.getSignature()" wurde mit null aufgerufen"); } } `

Im Aspekt kommt dann alles zusammen, PointCuts und Advices sowie die im folgenden beschriebenen Inter-Type-Deklarationen. Aspekte können in der AspectJ eigenen Sprache, oder auch in Java Klassen mittels der @Aspect Syntax entwickelt werden.

Aspekte, Pointcuts und Advices können als abstrakt definiert und in konkreten, abgeleiteten Aspekten konkretisiert werden. Aspekte können neben den bekannten Zugriffsmodifikatoren auch privileged sein und dann auf private Teile der Zieltypen zugreifen.

Strukturelle Änderungen

Inter-Type-Deklarationen umfassen das Hinzufügen neuer Member in einer Klasse, das können Variablen, Annotationen (auch auf Variablen und Methoden), Methoden, Konstruktoren und seit neuestem auch innere Klassen (nur static) sein. Mittels des declare Schlüsselworts können neue Superklassen oder Interfaces, sowie Annotationen eingefügt werden.

Beispiele für ITDs:

````java public int Konto.getWert() { return wert; } private Auditor Konto.auditor

public static Comparable.Comparator<? extends Comparable> { public int compare(Comparable o1, Comparable o2) { return o1.compareTo(o2); } } ``

Beispiele für declare:

`` declare @field : !static Object+ com.example.domain.. : @NotNull;

declare parents : (com.example.domain.*) implements java.io.Serializable;

declare @type : (com.example.domain.*) : @Domain; ``

Ein typisches Muster ist, annotierten Klassen mittels declare ein neues Interface zu verpassen und auf diesem dann neue Variablen und Methoden einzuführen. Eine andere typische Anwendung sind Mixins (aus anderen Sprachen auch als Traits oder Module bekannt), die hier über Interfaces, die einen Aspekt enthalten, der diesem Interface konkretes Verhalten mitgibt, abgebildet werden.

Wie kommt die Advice nun an die Stelle im Code, an die sie soll? Der Prozess der die kompilierten Aspekte und den Java-Bytecode miteinander verwebt (weaving) kann während des Kompilierens erfolgen (mittels des ajc Compilers oder build-tool plugins). Es gibt auch die Möglichkeit mittels der Java-Instrumentierungs-API (JVMTI) das zur Laufzeit beim Laden der Klassen vorzunehmen (Load-Time-Weaving). Der Weaver produziert sehr effizienten Code, da er auf viele statische Informationen zurückgreifen kann, nur für einige wenige, dynamische Pointcuts werden Laufzeit-Checks integriert.

Domänenobjekt-Superheld

Da es in diesem Artikel um "Domänenobjekt Superhelden" geht, will ich zuerst einmal kurz das Domänenobjekt unter Verdacht vorstellen. Wie in vielen AOP Beispielen ist es ein Konto, das - hoffentlich - Geld enthält und eine Methode um Geld abzuheben (oder auch einzuzahlen). Ich halte es mit Absicht ganz einfach, um Schritt für Schritt Zusatzfunktionalitäten per AOP anzuflanschen.

````java package com.example.domain;

@Entity public class Konto { private @Min(10) int wert; private String name;

public Konto(int wert, String name) {
	this.wert = wert;
	this.name = name;
}
@Transactional public void buche(int delta) {
	wert += delta;
}
	public String toString() {
		return String.format("%s: %d %s",name, wert , KontoRenderer.CURRENCY);
	}
}
````
Es gibt nun eine Menge von neuen Anforderungen, die das arme Konto mit einer Vielzahl an Zusatzaufgaben belasten würden.

Konto++:

  • Konto soll eine getWert() Methode erhalten

  • das wert Feld des Kontos soll stets davor geschützt sein, negativ zu werden

  • die buche Methode soll verhindern, dass das Konto ins Negative abrutscht

  • nach erfolgreicher Buchung, soll der Vorgang geloggt werden

Domänenobjekt Superhelden:

  • Bedingung: DomänenObjekte sind mit der Annotation @Entity annotiert

  • Domänenobjekte sollen serialisierbar sein (Serializable)

  • Felder mit @Min und @Max annotationen sollen vor dem Setzen des neuen Wertes geprüft werden (ala JSR-303)

  • nicht primitive Felder von Domänenobjekten sollen eine @NotNull Annotation erhalten und damit einen Schutz vor Null-werten

  • mit @NotNull annotierte Methoden dürfen keine Nullwerte-Parameter gesetzt bekommen und auch kein Null zurückgeben

  • Domänen-Klassen mit @Entity annotation sollen automatisch Methoden für persist(), delete() erhalten

  • Methoden mit @Transactional sollen in einem Transaktionskontext laufen und bei Misserfolg zurückgerollt werden

  • Zugriff auf UI-Layer aus den Domänen-Objekten ist untersagt

hausgemachte Dependency Injection:

  • Klassen mit @Inject Annotation bekommen bei Konstruktion auf alle Felder mit @Inject annotation eine Dependency injected (s. JSR-330)

Und als Sonderwusch:

  • Jede Klasse die von Iterable ableitet soll automatisch Methoden wie map, reduce usw. erhalten

Die letzten beiden Anforderungen werden aus Platzgründen nur im Beispiel-Code enthalten sein.

Statt das Konto und andere Domänenobjekte mit den notwendigen, verstreuten Code-Schnipseln auszustatten, werden wir diese in konkreten Aspekten deklarieren und vom Weaver über den Code verteilen lassen. Wir verteilen die Anforderungen an Konto und allgemeine Domänenobjekte auf zwei Aspekte: KontoAspekt und DomaenenAspekt.

Aber immer schön der Reihe nach, zuerst der Konto-Aspekt.

````java public priviledged aspect KontoAspekt {

public int Konto.getWert() { return this.wert; }
// pointcut fuer Aufrufe der Buchen-Methode, sammelt aktuelle Instanz
// und Argument ein und stellt sie als Parameter zur Verfügung
pointcut buchung(int delta, Konto konto) : call(void Konto.buche(int)) && args(delta) && this(konto);
  // before-Advice mit o.a. pointcut, enthält Überziehungs-Check
  	before(int delta, Konto konto) : buchung(delta,konto) {
	if (delta < konto.wert) throw new IllegalArgumentException("Konto würde mit "+delta+" überzogen.");
}
	// "Audit" Subsystem, hier als neue Variable im Konto eingeführt
    private PrintStream Konto.audit = System.out;
    // Audit-Augabe nach _erfolgreicher_ Ausfürhrung der Buchen Methode
    after(int delta, Konto konto) returning : buchung(delta,konto) {
		audit.printf("Konto %s Buchung erfolgt: %d",konto.name, delta);
	}
}
````

Das Verhalten des Kontos bisher:

`java > konto = new Konto(10,"test") > konto.getWert() ⇒ 100 > konto.buche(2) ⇒ Konto "test" Buchung erfolgt: 2 > konto.buche(-20) ⇒ IllegalArgumentException(Konto würde mit -20 überzogen.) `

Es geht weiter mit dem Domänen-Aspekt. Man kann neben Annotationen wie @Entity, natürlich auch Packages oder Type-Namens-Konventionen nutzen, um die Domänenobjekte zu identifizieren. Mixin-Methoden werden auf dem neuen "DomaenenObjekt"-interface eingeführt.

````java package com.example.support;

public privileged aspect DomaenenAspekt {

// alle Domänenobjekte erhalten zusätzlich das Serializable interface
declare parents : @Entity * implements java.io.Serializable;
// alle Domänenobjekte erhalten zusätzlich das DomaenenObjekt interface
declare parents : @Entity * implements DomaenenObjekt;
// alle nicht statischen Objekt-Referenz Felder von DomänenObjekt Subklassen
// erhalten eine @NotNull Annotation
declare @field : !static Object+ DomaenenObjekt+.* : @NotNull;
// Felder mit @NotNull annotation bekommen einen Schutz vorm Null-Setzen
before(Object neu) : set(@NotNull (Object+) *.*) && args(neu) {
	if (neu == null) throw new IllegalStateException("Feld "+thisJoinPointStaticPart.getSignature()+
						" sollte null gesetzt werden! ");
}
// Felder mit @Min annotation werden vorher auf den Mindest-Wert geprüft
before(int neu, Min min) : set(@Min (int || long) *.*) && args(neu) && @target(min) {
	if (neu < min.value()) throw new IllegalStateException("Feld "+thisJoinPointStaticPart.getSignature()+
						" kannt nicht kleiner als das Minimum "+min+" gesetzt werden!");
}
// Persistenz-Manager wird global im Aspekt gehalten
private PersistenceManager pm = new PersistenceManager();
// Transaktions-Support, für mit @Transactional annotierte Methoden ind
// Entitäten, nutzt die Aspekt-Variable
Object around() : execution(@Transactional (*) @Entity *.*(..)) {
	pm.begin();
	try {
		Object result=proceed();
		pm.commit();
		return result;
	} catch(RuntimeException e) {
		pm.rollback();
		throw e;
	} catch(Exception e) {
		pm.rollback();
		throw new RuntimeException(e);
	}
}
// persist Methode eingeführt, die an den PersistenceManager delegiert
@Transactional public void DomaenenObjekt.persist() {
	aspectOf().pm.persist(this);
}
// delete Methode eingeführt, die an den PersistenceManager delegiert
public void DomaenenObjekt.delete() {
	aspectOf().pm.delete(this);
}
// @Transactional annotation an delete() Methode angehängt
declare @method: void DomaenenObjekt+.delete() : @Transactional;
// Null-Prüfung für Parameter und Rückgabewert der mit @NotNull annotierten Methode
Object around() : call(@NotNull (*) @Entity *.*(..)) {
	Object[] params=(Object[]) thisJoinPoint.getArgs();
	for (int i=0;i<params.length;i++) {
		if (params[i] == null) {
			MethodSignature signature= (MethodSignature)thisJoinPointStaticPart.getSignature();
			throw new IllegalArgumentException("Parameter "+
					signature.getParameterTypes()[i]+ " "+ signature.getParameterNames()[i]
					+ " ist null!");
		}
	}
	Object result=proceed();
	if (result==null) {
		throw new IllegalStateException("Methode "+(MethodSignature)thisJoinPointStaticPart.getSignature()+
					"gab null für die Argumente "+Arrays.toString(thisJoinPoint.getArgs()));
	}
	return result;
}
// erzeugt Kompilier-Fehler wenn aus dem domain-Package auf das ui-Package zugegriffen wird
declare error : within(com.example.domain.*) &&
(call(* com.example.ui.*.*(..)) || set(* com.example.ui.*.*) || get(* com.example.ui.*.*)): "Architektur: Domain ruft UI Layer";

} ``

Wie verhält sich unser Konto jetzt, da der DomaenenAspekt eingebunden ist?:

`java > Serializable.class.isAssignableFrom(Konto.class) ⇒ true > DomaenenObjekt.class.isAssignableFrom(Konto.class) ⇒ true > new Konto(10,null) ⇒ IllegalStateException("Feld Konto.name sollte null gesetzt werden! ") > new Konto(-1,"konto1") ⇒ IllegalStateException("Feld Konto.wert kannt nicht kleiner als das Minimum 10 gesetzt werden!") > konto = new Konto(20,"konto1") > konto.buche(5) ⇒ begin TX ⇒ commit TX > konto.persist() ⇒ begin TX ⇒ "konto1 persistiert" ⇒ commit TX > konto.delete() ⇒ begin TX ⇒ "konto1 gelöscht" ⇒ commit TX > return String.format("%s: %d %s",name, wert , KontoRenderer.CURRENCY); ⇒ Compile-Error: "Architektur: Domain ruft UI Layer" > Änderung auf: > return String.format("%s: %d %s",name, wert , Waehrung.EUR); ⇒ kompiliert `

Weitere spannende Eigenschaften von Aspekten sind das Scoping (Association) - an welchen Gültigkeitsbereich wird eine neue Aspekt-Instanz gebunden. Dies wird mittels einer zusätzlichen Deklaration am Aspekt definiert. Standard - static singleton, über Aspekt-Instanz pro Typ (pertypewithin), pro Instanz (perthis) oder sogar pro Kontrollfluss-Ausführung (percflow) ist alles möglich.

Weitere Bestandteile von AspectJ, wie die alternative @AspectJ Syntax, die Kontrolle des Load-Time-Weavers mittels aop.xml, Nutzung in Aspekt-Bibliotheken, Reihenfolge von Aspekten und vieles mehr sind aussen vor geblieben. Falls Interesse an diesen Themen besteht, dann bitte ich um Feedback, so dass es in einer späteren Kolumne eine Fortsetzung des Themas geben kann.t

Die Entwicklung steht auch bei AspectJ nicht still und so habe ich den Projekt Lead Andy Clement, der sich bei SpringSource um die AspectJ Weiterentwicklung kümmert, gefragt welche bemerkenswerten Neuerungen es so in letzter Zeit gegeben hat.

Mit Java 5 kamen zum Funktionsumfang von AspectJ die Unterstützung von Annotationen und Generics hinzu, die sukzessive ausgebaut wurden. Das bezieht sich sowohl auf die Pointcut Sprache und die Aspekte selbst als auch auf die möglichen strukturellen Modifikationen. Besonders auf Annotationen ruht als Code-Metadaten (Markern) ein besonderes Gewicht.

Spring ROO und Spring Insight waren die stärksten Treiber der Weiterentwicklung. Nicht verwunderlich, da AspectJ auch bei SpringSource/VMware angesiedelt ist. Durch Roo hat vor allem ITDs vorangetrieben, z.b. das Entfernen von Annotationen mittels declare und das Hinzufügen von inneren Klassen. Roo bringt auch eine weitere interessante Funktion mit, das "Push-In Refactoring", das es erlaubt sämtlichen, in Aspekten enthaltenen Code an die entsprechenden Stellen im Sourcecode einzufügen und die Aspekte zu löschen.

Die meisten der Weiterentwicklungen ab Version 1.6 bezogen sich auf Performance und Speicherverbrauch sowie die Anwendbarkeit auf nur teilweise kompilierbaren Code in der IDE. Performance bezogene Erweiterungen (-showWeaveInfo, -timers -verbose, AJDT Event Trace View).

Neuerdings kann man auch über Parameter-Annotationen selektieren und Annotations-Attribute statisch, zum Weaving-Zeitpunkt binden.

Für Java 7 liegen die Herausforderungen im neuen InvokeDynamic Schlüsselwort der JVM, später wird es dann noch einmal rund um die AspectJ Unterstützung für Closures interessant.

Man sollte sich AspectJ in kleinen Schritten nähern, zuerst einmal für den lokalen Einsatz in der Entwicklung (z.b. Tracing, Mocking) über den Einsatz von Frameworks die AspectJ benutzen (Spring) bis zu (vielleicht) der Nutzung von Aspekten in Produktiv-Code, um die notwendigen Cross-Cutting-Concerns abzubilden oder sogar um zusätzliche Geschäftslogik zu implementieren.

Ich habe tiefgehendere Erfahrungen mit AspectJ während der Entwicklung der Objekt-Graph-Mapping-Bibliothek ("Spring Data Graph für Neo4j" sammeln können und war von der Mächtigkeit schon beeindruckt. Diese "Magie" ist aber auch ein zweischneidiges Schwert, da sie von vielen Nutzern nicht verstanden wird, manchmal unvorhergesehene Auswirkungen hat (falsche Pointcuts) und eine gute IDE-Unterstützung bedingt.

Gregor Kiczales meinte in dem Software-Engineering-Radio Interview, dass der wichtigste Auswirkung von AOP wäre, übergreifende Belange als solche zu erkennen und in der Entwicklung der Software entsprechend zu berücksichtigen. Der Einsatz von AOP-Werkzeugen wäre "nice-to-have" aber nicht unabdingbar.

Referenzen:

Last updated 2011-05-23 11:09:27 CEST
Impressum - Twitter - GitHub - StackOverflow - LinkedIn