Autoproxing mit Spring
Das Applikation Framework Spring bringt ein sehr nützliches Feature mit: Autoproxing. Damit lassen sich Spring Beans – also eigentlich jede POJO-Klasse – um Aspekte erweitern. Aspekte sind gemäss AOP Belange eines Programms die sich quer zur eigentlichen Programmlogik erstecken (Cross-Cutting-Concerns).
In der Aspektorientierten Programmierung kann man diese Aspekte getrennt vom Rest des Programms formulieren und erst die AOP Umgebung "verwebt" den Aspektcode mit dem eigentlichen Programmcode. Hier gibt es prinzipiell zwei unterschiedliche Vorgehensweisen 1. zur Compile-Time (während des Kompilierens) oder 2. zur Run-Time (während der Laufzeit).
Der bekannteste Vertreter der 1. Variante ist sicher AspectJ, welches eine eigene Sprache formuliert und folglich auch einen eigenen Compiler mitbringt. Dieser Compiler sorgt für das Zusammenweben von Aspekten und restlichem Programmcode. Die Sprache liefert alle Mittel um Aspekte zu formulieren und darüber hinaus festzulegen an welchen Stellen der Aspect-Code eingebunden werden muss (Definition von sogenannten Pointcuts).
Beim Spring-Framework wird der Aspect-Code erst zur Laufzeit dem restlichen Programm hinzugefügt. Wie kann man sich das vorstellen? Da Spring Bean-Instanzen immer über die BeanFactory erzeugt werden, kann die Factory die erzeugten Instance beliebig verändern, solange sie für die Applikation nur "so aussehen" wie eine Bean vom Type XY. Bei Beans die ein bestimmtes Interface implentieren, bedeutet das, das die erzeugte Instance eben immer noch diese Interface implementieren muss. Wird kein Interface implementiert, so wird eine abgeleitete Klasse mindestens die gleichen "Public-Methods" bieten, wie die Basis-Klasse.
Mit Hilfe dieser Logik arbeitet das Proxy-Feature von Spring: Bei Klassen die ein Interface implentieren verwendet Spring sogenannte Dynamic-Proxies (seit JDK 1.4) ansonsten wird mit Hilfe der "Code-Generating-Library" CGLib dynamisch eine abgeleitete Klasse gebildet und diese instanziiert.
Auf diese Weise kann Spring nun auf Wunsch jeden Methodenaufruf einrahmen (wrappen), um vor oder nach dem Methodenaufruf (oder beides) Aspektcode auszführen.
Damit man dieses Verhalten nun nich explizit in die BeanFactory bei allen Bean konfigurieren muss, kennt Spring das AutoProxy-Feature. Einmal aktiviert, wird bei jeder Bean automatisch überprüft, welche Methoden mit welchen Aspekten zu versehen sind.
Dieser Zusammenhang wird nun nicht mit einer speziellen Sprache wie bei AspectJ festgelegt, sondern in nomalem Java "ausprogrammiert". Man schreibt einen sogenannten "Advisor", der das Interface org.springframework.aop.PointcutAdvisor implementiert. Sobald Spring nun einen potentiellen Kandidaten für das AutoProxing instanziieren soll, wird der Advisor der Reihe nach bei allen Methode gefragt, ob und mit welchem "Advise" die Methode versehen werden soll.
Das Nutzen von Aspekten soll nun besonders einfach gestaltet werden in dem man Annotationen verwendet. Über Annotationen können Klassen oder Methoden (ja selbst Parameter) mit Metainformatonen versorgt werden, die mit Hilfe des neuen Reflection API von Java 5 auch zur Laufzeit abgefragt werden können.
Ein einfacher Advisor
Leider bin ich bei meinem ersten Ansatz mit einem SpringAdvisor auf Probleme gestossen:
Solange es sich um Klassen handelte, die kein Interface implementierten und von daher mittels Code-Generation erweitert werden können, funktionierte noch alles wie erwartet.
Ein Problem trat auf als eine Klasse mit Annotationen versehen wurde, die ein oder mehrere Interfaces implementierte. Wenn man die Calls an den Advisor mitprotokolliert, kann man sehn, dass für Methoden, die Teil des Interfaces sind, der Advisor "zweimal" aufgerufen wird: einmal für die konkrete Implementierung der Klasse, die Spring konkret instanziiert, einmal für die abstracte Interface-Methode.
Ein solcher einfacher Advisor sieht so aus:
package com.rinke.solutions.spring.test.aop;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
/**
* advisor that adds advice to a class od method, when a special annotation is present.
* @author ster
*/
public class SimpleAnnotationSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {/** stores the annatation class to check.*/
private Class<? extends Annotation> annotationToCheck;
public void setAnnotationToCheck(Class<? extends Annotation> annotationToCheck) {
this.annotationToCheck = annotationToCheck;
}public boolean matches(Method method, Class clazz) {
return method.isAnnotationPresent(annotationToCheck);
}
}
Es wird einfach nur geprüft, ob bei der betroffenen Methode die "annotationToCheck" vorhanden ist.
Wenn der Advisor nun "den Fehler" macht, den Advice nicht auch für die abstracte Methode des Interfaces zu setzen, dann wird der spätere Aufruf aus der Applikation, immer dann ohne den Advice ausgeführt, wenn der Caller nur mit dem Interface arbeitet.
Da der Advisor sich nach den Annotationen richtet, mit denen die Methode versehen ist, kann man dem dadurch begegnen, dass man auch die abstrakte Interface-Methode annotiert. Allerdings ist es oft gar nicht möglich oder wünschenswert, das Interface zu annotieren. Der Aspekt ist Teil der konkreten Implementierung und hat mit dem Interface nichts zu tun.
Man kann dem aus dem Weg gehen, wenn man den Advisor etwas erweitert:
package com.rinke.solutions.spring.test.aop;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;import org.aopalliance.aop.Advice;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;/**
* advisor that adds advice to a class od method, when a special annotation is present.
* @author ster
*/
public class AnnotationSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {/** stores the annatation class to check.*/
private Class<? extends Annotation> annotationToCheck;
/**
* default ctor.
*/
public AnnotationSourceAdvisor() {
super();
}
/**
* sets the advice
* @see org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor#setAdvice(org.aopalliance.aop.Advice)
*/
public void setAdvice( Advice advice ) {
super.setAdvice(advice);
}
/**
* @param clazz the current class to check
* @param method the current method to check
* @return true, if the specified method has the annotation.
* @see org.springframework.aop.MethodMatcher#matches(java.lang.reflect.Method, java.lang.Class)
*/
public boolean matches(Method method, Class clazz) {
// this is tricky
if( clazz.equals(method.getDeclaringClass())) {
return method.isAnnotationPresent(annotationToCheck);
} else {
// if the methods declaring class is not the same as the implmenting class
// check the annotations on the implementing method not the asked method
boolean r = false;
if( Modifier.isAbstract(method.getModifiers() ) ) {
// try to get the implementing method from the real instance
try {
Method methodToCheck = clazz.getDeclaredMethod(method.getName(),method.getParameterTypes());
if( methodToCheck != null ) {
r = methodToCheck.isAnnotationPresent(annotationToCheck);
}
} catch (SecurityException e) {
} catch (NoSuchMethodException e) {
}
}
return r;
}
}public Class getAnnotationToCheck() {
return annotationToCheck;
}public void setAnnotationToCheck(Class<? extends Annotation> annotationToCheck) {
this.annotationToCheck = annotationToCheck;
}}
Jetzt wird unterschieden, ob die Methode um die es geht, von der instanzieerten Klasse selbst stammt oder eben in einem Interface deklariert wurde. Wenn die Methode nicht von der Klasse selst stammt, dann wird die Annotation nicht auf der Methode gesucht, die dem Advisor übergeben wurde, sondern auf der Methode, die die Interface-Methode implementiert.
Auf diese Weise funktioniert die Annotation auf der Implementierungsklasse auch dann, wenn diese Klasse ein oder mehrere Interfaces implementiert.
Ein vollständiges Beispiel läßt sich hier herunterladen.