AspectJ trochę szczegółów – rodzaje porad
Wiemy już jak napisać aspekt i poznaliśmy dwa „rodzaje składni” aspektów. Czas na dokładniejsze przyjrzenie się temu jak wygląda składnia i jakie możliwości nam daje. Na chwilę obecną wiemy, że aspekt to jednostka kodu, która jest wstawiana za pomocą odpowiedniego narzędzia – kompilatora aspektowego lub agenta JVM do kodu programu. Pytanie jednak jakie mamy możliwości wstawiania kodu i jakie rodzaje porad możemy tworzyć.
Rodzaje porad
Porada jest to pojedyncza operacja wstawiana w odpowiedni punkt przecięcia. Generalnie istnieją trzy rodzaje porad (i ich zrozumienie jest naprawdę proste w porównaniu z definicjami punktów przecięcia). Pierwszy to znana nam już porada before. Wstawiana jest w kod przed wykonaniem danego działania. Po jej wykonaniu wykonywany jest kod punktu przecięcia.
Drugim rodzajem porad są porady after. Tu sprawy trochę się komplikują. Po pierwsze mogą wystąpić trzy podrodzaje porady. Pierwsze to zwykłe porady. Wykonane będą zawsze po kodzie punktu przecięcia. Jeżeli wysypie się on z jakimkolwiek błędem nasza porada zostanie wykonana i dopiero po niej zostanie wyrzucony błąd. Drugie to porady afterthrowing, które wykonają się wtedy i tylko wtedy, gdy kod punktu przecięcia wywali wyjątek. Kod porady wykona się przed zwróceniem wyjątku lub po, bo całość nie jest jakoś specjalnie synchronizowana. Trzeci rodzaj to porady afterreturning, które to wykonają się tylko wtedy gdy kod w punkcie przecięcia zakończy się prawidłowo. Poniżej przykładowy aspekt, który ilustruje działanie wszystkich tych dróg:
Listing 1. Porada typu after
package pl.koziolekweb.blog.aspectj;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class ErrorHandlerAspect {
@Pointcut("execution(* NKWD.*(..))")
public void handleError(){}
@After("handleError()")
public void handleErrorAdvice(){
System.out.println("Metoda wyrzuca błąd");
}
@AfterThrowing(value = "handleError()", throwing = "runtimeException")
public void handleErrorThrowsAdvice(RuntimeException runtimeException) {
System.out.println("Metoda wyrzuciła błąd: "
+ runtimeException.getLocalizedMessage());
}
@AfterReturning("handleError()")
public void handleErrorNotThrowAdvice(){
System.out.println("Metoda nie wyrzuciła błędu");
}
}
Pobaw się z dodawaniem deklaracji wyjątku do klasy NKWD i zobacz co się stanie.
Trzeci rodzaj porad to porady around. Są one szczególnie przydatne wszędzie tam, gdzie zazwyczaj do wykonywania jakiś czynności wykorzystujemy wzorzec metody szablonowej. Jednocześnie szablon nie realizuje samego algorytmu, a jedynie pozwala na wykonanie czynności takich jak logowanie startu i końca akcji, blokowanie obiektów (ukochany ReadWriteLock), rozpoczynanie i kończenie transakcji, proste profilowanie. Kolejnym wzorcem, który można zastąpić tym rodzajem porady jest dekorator. Bardzo dobrym pomysłem jest dekorowanie obiektów w sposób przezroczysty dla klienta. Niestety klient musi w takim wypadku korzystać z interfejsu do komunikacji z obiektem (co jest bardzo fajne) i uzyskiwać obiekty z jakiejś fabryki (co jest upierdliwe zarówno dla klienta jak i programisty). Oczywiście jest Spring, EJB3.x czy Pico, ale jakoś nie widzę sensu ich użycia tylko dla jednego małego dekoratora (pamiętaj, używanie frameworków powoduje gwałtowne rozrastanie się kodu wynikowego). Zresztą nie zawsze da się użyć frameworku.
Problemem z tym rodzajem porady jest konieczność wstawienia jakiegoś elementu, który pozwalał by na określenie jaki kod ma być wykonany przed, a jaki po wykonaniu kodu punktu przecięcia. AspectJ dostarcza odpowiedniego pseudo słowa kluczowego proceed(). Jest to pseudo słowo kluczowe ponieważ w praktyce jest to metoda 😉
Napiszmy profiler, który będzie sprawdzał ile czasu zajmuje użytkownikowi wpisanie hasła. Przy okazji mały refaktoring kodu NKWD:
Listing 2. Nowa wersja NKWD
package pl.koziolekweb.blog.aspectj;
import java.util.Scanner;
public class NKWD {
private boolean isAuth = false;
public void auth(){
if (isAuth) {
return;
} else {
String password = readPassword();
isAuth = validatePassword(password);
if (!isAuth) {
throw new RuntimeException("Wypad na Łubiankę");
}
}
}
private String readPassword() {
System.out.print("podaj hasło: ");
Scanner scanner = new Scanner(System.in);
String password = scanner.next();
return password;
}
private boolean validatePassword(String password) {
return "dupa".equals(password);
}
}
Czas na nasz nowy aspekt 😉
Listing 3. Aspekt profilujący.
package pl.koziolekweb.blog.aspectj;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class InputProfilingAspect {
@Pointcut("call(* java.util.Scanner.next(..))")
public void profile() {
}
@Around("profile()")
public Object profileAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
Object processResult = joinPoint.proceed();
long stop = System.nanoTime();
System.out.println("Czas wpisania hasła: "
+ ((stop - start) * 0.000000001) + " sekundy");
return processResult;
}
}
Kolejnym problemem jest konieczność zachowania sygnatury dekorowanej metody. Zatem w przypadku wykorzystania składni @AspectJ jak i .aj należy zadbać by porada zwracała odpowiedni obiekt. Na całe szczęście proceed zwraca Object, a odpowiednie rzutowanie zostaje wykonane poza naszą ingerencją przez weavera. Co jednak w przypadku gdy metoda przyjmuje parametry? Sprawdźmy ile czasu zajmie wypisanie komunikatu powitalnego dla konkretnego użyszkodnika.
Listing 3. Aspekt profilujący z parametrami.
package pl.koziolekweb.blog.aspectj;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class MessageProfilingAspect {
@Pointcut("call(* Service.welcome(String)) && args(string)")
public void profile(String string){}
@Around("profile(string)")
public void proflieAdvice(ProceedingJoinPoint joinPoint, String string) throws Throwable{
long start = System.nanoTime();
joinPoint.proceed(new Object[]{string});
long stop = System.nanoTime();
System.out.println("Czas wpisania wiadomości: "
+ ((stop - start) * 0.000000001) + " sekundy");
}
}
Jak widać coś jest nie tak. Czasy się nie zgadzają. Czas wypisania komunikatu jest dłuższy niż czas wpisywania hasła. Wniosek, czas jest liczony globalnie. Cóż… takie życie. Wynika to z modelu jaki przyjęto dla tworzenia aspektów. Opisze to później, a na teraz należy pamiętać, że domyślnie aspekty są tworzone jako singletony.
Podsumowania
Wiemy już jakie mamy rodzaje porad. Jeżeli nie rozumiesz jak to działa to nie przejmuj się. Zrozumienie tego modelu tak jak zrozumienie modelu punktów przecięcia wymaga dużo czasu i jeszcze więcej praktyki. W następnej części omówię właśnie model punktów przecięcia. Jest to bardzo ważna i bardzo trudna rzecz. Dlatego też skupię się na podstawowych rodzajach punktów przecięcia.