Wywołanie i wykonanie metody
Przygotowałem ostatnio plik pom.xml dla projektu korzystającego z AspectJ. Zanim przejdę do omawiania rzeczywistych problemów, przedstawię pokrótce budowę prostego aspektu. Żeby uzyskać jak najwięcej szczegółów, zamiast testów, użyję raczej niezbyt eleganckiego rozwiązania opartego o System.out.
Przygotowania
Na początek utworzę klasę, której dotyczyć będą punkty przecięcia użyte przy tworzeniu aspektu. Wystarczy mi jedna metoda, wyświetlająca komunikat:
public class FooClass {
public void fooMethod() {
System.out.println("FooClass.fooMethod() says \"Hello\".");
}
}
Metodę fooMethod wywoływać będę wewnątrz metody innej klasy. Dla uproszczenia będzie to główna klasa programu:
public class MethodExample {
public static void main(String... args) {
new MethodExample();
}
public MethodExample() {
new FooClass().fooMethod();
}
}
Aspekt
Pora utworzyć pierwszy aspekt. Nie będę tutaj omawiał starego podejścia, gdzie aspekty pisane były w specjalnym języku (przykłady można znaleźć w Wikipedii). Zamiast tego utworzę najzwyklejszą klasę, oznaczoną adnotacją @Aspect:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
@Aspect
public class MethodLogger {
}
Wykonanie metody
Na początku zajmę się punktem złączenia, odpowiadającym wykonaniu metody fooMethod, nie przyjmującej żadnych parametrów oraz nie zwracającej żadnej wartości. W AspectJ możemy go zapisać:
execution(void fooMethod())
Punkt przecięcia może składać się po prostu z jednego punktu złączenia. Tak też będzie w omawianym przykładzie. Ze względu na prostotę, nie będę definiował osobnego punktu przecięcia (może w kolejnych przykładach), ale przejdę od razu do rad.
Utworzę dwie rady dla omawianego punktu przecięcia – przed oraz po. Będą to metody klasy aspektu, przyjmujące jako parametr konkretny punkt złączenia, przy którym rada została wywołana. Aby wskazać, że metoda jest radą, użyję odpowiednich adnotacji:
@Before("execution(void fooMethod())")
public void beforeExecution(JoinPoint joinPoint) {
System.out.println("Before execution");
}
@After("execution(void fooMethod())")
public void afterExecution(JoinPoint joinPoint) {
System.out.println("After execution");
}
Po uruchomieniu programu, otrzymuję chyba oczywisty rezultat:
Before execution FooClass.fooMethod() says "Hello". After execution
Wywołanie metody
Nieco innym punktem złączenia jest wywołanie metody. Jego definicja będzie wyglądała następująco:
call(void fooMethod())
Do aspektu dodaję dwie nowe rady przed wywołaniem metody oraz po wywołaniu metody:
@Before("call(void fooMethod())")
public void beforeCall(JoinPoint joinPoint) {
System.out.println("Before call");
}
@After("call(void fooMethod())")
public void afterMethodCall(JoinPoint joinPoint) {
System.out.println("After call");
}
Tym razem rezultat może nie być już tak oczywisty (przynajmniej na pierwszy rzut oka):
Before call Before execution FooClass.fooMethod() says "Hello". After execution After call
Z powyższego przykładu płynie wniosek, że metoda jest najpierw wywoływana, a dopiero potem wykonywana.
Porównanie punktów złączenia
Aby dokładniej porównać omawiane punkty złączeń, utworzę dodatkową metodę, wywoływaną z poziomu każdej rady:
private void printJoinPointInfo(JoinPoint joinPoint) {
System.out.println("======== Join point: ========");
System.out.println("Signature: " +
joinPoint.getSignature().toShortString());
System.out.println("Kind: " + joinPoint.getKind());
System.out.println("This: " +
joinPoint.getThis().getClass().getSimpleName());
System.out.println("Target: " +
joinPoint.getTarget().getClass().getSimpleName());
System.out.println("=============================");
}
Tym razem na konsoli powinny pojawić się następujące komunikaty:
Before call ======== Join point: ======== Signature: FooClass.fooMethod() Kind: method-call This: MethodExample Target: FooClass ============================= Before execution ======== Join point: ======== Signature: FooClass.fooMethod() Kind: method-execution This: FooClass Target: FooClass ============================= FooClass.fooMethod() says "Hello". After execution ======== Join point: ======== Signature: FooClass.fooMethod() Kind: method-execution This: FooClass Target: FooClass ============================= After call ======== Join point: ======== Signature: FooClass.fooMethod() Kind: method-call This: MethodExample Target: FooClass =============================
Jak widać, dla tej samej metody obiekt docelowy (target) jest zawsze ten sam – jest to obiekt, w którym dana metoda się znajduje. Ale wskazanie obiektu bieżącego (this) nie jest już takie proste. W przypadku wykonania metody, jest to ten sam obiekt, co docelowy (zawierający daną metodę). Jednak przy wywołaniu metody, obiekt bieżący jest tym, z poziomu którego metoda jest wywoływana.
Podsumowanie
Omówiony przykład pokazuje, ile potencjalnych błędów może spowodować prosta obsługa wywołania lub wykonania metody. Wewnątrz konstruktora klasy MethodExample została wywołana metoda fooMethod, ale jej wykonanie odbywa się dla obiektu klasy FooClass. Czytelnikom polecam ponowne przeanalizowanie całego kodu.
Następnym razem spróbuję opisać jakieś realne zastosowanie AspectJ. Podam też przykłady innych rad i punktów złączenia.




Krótki acz treściwy wpis! Takich właśnie potrzeba mi najwięcej. Chciałoby się jeszcze prosić o skrinkast, aby zamiast czytać można było posłuchać i zobaczyć (akurat zobaczyć mogłem i tutaj, ale domyślam się, że dla bardziej skomplikowanych przykładów ilość pracy byłaby niewspółmiernie duża do nagrania).
Wielkie dzięki, bardzo cieszy mnie taka ocena, zwłaszcza ze strony zaprawionego blogera. Niestety słuchanie mnie mogłoby nie być zbyt przyjemne – obawiam się, że dużo lepiej piszę, niż mówię.