Nachem ich im ersten Teil eine kurze Einführung in die Verwendung von Dependency Injection mit Guice gegeben habe, möchte ich im Folgenden zeigen, wie man aspektorientierte Programmierung (AOP) mit Guice einsetzen kann. Auch hier genügt die Kenntnis einiger weniger Klassen und Interfaces um zum Ziel zu gelangen.
Einführung in AOP mit Guice
Grundidee von AOP ist, bestimmte übergreifende Aspekte einer Anwendung (sogenannte Cross-Cutting Concerns) aus den einzelnen Methoden auszugliedern, um so den Code lesbarer zu machen und Wiederholungen zu vermeiden. Beispiele für solche Cross-Cutting Concerns sind zum Beispiel das Logging von Methodenaufrufen oder die Prüfung von erforderlichen Berechtigungen. Diese Aspekte sind zwar notwendig, tragen aber nicht zum Erfüllen der eigentlichen Aufgabe der jeweiligen Methode bei und sollten im Sinne von Separation of Concerns nicht vom eigentlichen Geschehen in der Methode ablenken und daher ausgegliedert werden.
In Google Guice kann die Ausgliederung solcher Belange durch Method Interception umgesetzt werden. Das Prinzip ist folgendes: Methodenaufrufe werden zur Laufzeit abgefangen, untersucht und gegebenenfalls wird ihnen der Code des passenden Aspekts vor- bzw. nachgeschaltet. Was zunächst vielleicht kompliziert klingt, läßt sich mit Guice sehr einfach umsetzen:
- Um festzustellen, ob einem Methodenaufruf ein bestimmter Aspekt vorangestellt bzw. angehängt wird, gibt es in Guice sogenannte Matcher. Das Interface Matcher sieht eine Methode matches vor, die als Parameter ein Objekt vom Typ T erhält und für dieses entscheidet, ob es von diesem Matcher akzeptiert wird oder nicht. Matcher erzeugt man am einfachsten über die Factory-Klasse Matchers.
- Für die Logik des auszugliedernden Aspekts implementiert man das Interface MethodInterceptor. In dessen invoke-Methode kann man dann festlegen, was vor bzw. nach dem Aufruf der Methode geschehen soll.
- Die im ersten Teil vorgestellte Klasse AbstractModule bietet dann die Möglichkeit, den Interceptor einzubinden. Hierfür verwendet man die Methode bindInterceptor. Diese erhält zwei Matcher und eine beliebige Anzahl von einzubindenden Method Interceptors. Der erste Matcher legt fest, welche Klassen betroffen sind, der zweite Matcher legt die betroffenen Methoden fest.
Beispiel
Das ganze soll nun anhand des Loggings von Methodenaufrufen und Rückgabewerten veranschaulicht werden. Für einfachere Fehlersuche werden Methoden häufig wie folgt “verziert”:
public Integer eineMethode (final Integer arg1, final Integer arg2) { if (LOGGER.isDebugEnabled() { LOGGER.debug("Aufruf von eineMethode mit Parametern " + arg1 + " " + arg2); } // (...) hier steht der eigentlich relevante Code der Methode if (LOGGER.isDebugEnabled() { LOGGER.debug("Methode eineMethode beendet. Ergebnis: " + returnValue ); } } |
Dieser Code für das Logging wiederholt sich häufig und bläht die Methoden unnötig auf. Um diese Wiederholungen zu vermeiden und den Code übersichtlicher zu gestalten soll dieses Logging nun ausgegliedert werden.
- Wir schreiben zunächst eine Klasse MethodCallLogger, die MethodInterceptor implementiert:
package util.logging; import java.lang.reflect.Method; import java.util.Arrays; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; public class MethodCallLogger implements MethodInterceptor { @Override public Object invoke(final MethodInvocation call) throws Throwable { // vor dem Aufruf: Parameter loggen Method method = call.getMethod(); Object[] args = call.getArguments(); StringBuilder loggingMessageBuilder = new StringBuilder(); loggingMessageBuilder.append("Aufruf von "); loggingMessageBuilder.append(method.getName()); loggingMessageBuilder.append(" mit Parametern "); loggingMessageBuilder.append(Arrays.deepToString(args)); System.out.println(loggingMessageBuilder.toString()); // eigentlichen Aufruf durchführen Object result = call.proceed(); // nach dem Aufruf: Ergebnis loggen loggingMessageBuilder = new StringBuilder(); loggingMessageBuilder.append(method.getName()); loggingMessageBuilder.append(" beendet. Ergebnis "); loggingMessageBuilder.append( result.toString()); System.out.println(loggingMessageBuilder.toString()); return result; } }
- In unserem Modul legen wir nun fest, an welche Klassen und Methoden wir den Interceptor binden. Wir machen es uns einfach und verwenden für beide Matcher Matchers.any():
package aop; import util.logging.MethodCallLogger; import com.google.inject.AbstractModule; import com.google.inject.matcher.Matchers; public class AOPModule extends AbstractModule { @Override protected void configure() { bindInterceptor(Matchers.any(), Matchers.any(), new MethodCallLogger()); } }
Das war alles: Die Methoden aller Klassen, die wir mit Hilfe unseres AOPModule instanzieren, werden nun automatisch von unserem MethodCallLogger umschlossen.
Man kann das Verfahren natürlich weiter verfeinern. Beispielsweise könnte man eine Annotation @MethodCallLogging schreiben, mit der man dann manuell festlegt, welche Methodenaufrufe geloggt werden sollen. Diese Annotation kann dann mit einem passenden Matcher abgefragt werden, den man mittels Matchers.annotatedWith() erhält.
Weitere Informationen findet man auf der Projektseite und in den JavaDocs.
Viel Spaß beim Ausprobieren!
Tags: AOP, Dependency Injection, Guice, Java