@AspectJ i adnotacje Java

Jak wspominałem wczoraj poza plikami .aj AspectJ posiada też drugi rodzaj składni oparty o czysty kod Java. Jest to o tyle fajne rozwiązanie, że nie trzeba mieć na pokładzie kompilatora aspektowego, a wystarczy tylko zwykły javac + biblioteka z adnotacjami. Jest to o tyle ważne, że przy dużej aplikacji statyczne wstawianie aspektów jest dość żmudne. O sposobach wstawiania aspektów jeszcze będę pisał. Generalnie nie zawsze chcemy by aspekty były nam potrzebne. Załóżmy, że mamy do wykonania dużą ilość testów jednostkowych dla klas biznesowych. Kompilowanie dodatkowego kodu jest nam niepotrzebne. Tak samo jak uruchamianie niestandardowego kompilatora. Jednocześnie pliki .aj będą traktowane jako wróg publiczny przez standardowy kompilator i cały proces będzie się sypał. W takim przypadku najlepszym rozwiązaniem jest użycie składni pure java.

Aspekt jako klasa Java

Każdy plik .java jest prawidłowym plikiem dla kompilatora ajc. Z drugiej strony plik .aj nie jest akceptowany przez kompilator java, chyba że nie ma w nim żadnych elementów aspektowych (co jest głupie w takim przypadku). Mamy jednak do dyspozycji mechanizm adnotacji, który pozwala na tworzenie kodu aspektów za pomocą standardowych mechanizmów języka. Sięgając do przykładu ze wczoraj przepiszmy nasz aspekt na czysty kod javowy.

Listing 1. Aspekt jako klasa Java

package pl.koziolekweb.blog.aspectj;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SecurityAspect {

	private NKWD nkwd = new NKWD();

	@Pointcut("call( * Service.welcome(..))")
	public void auth() {
	}

	@Before("auth()")
	public void authBefore() {
		System.out.println("NKWD działa");
		nkwd.auth();
	}

}

Jak widać wszystko jest bardzo przejrzyste i jasne. Jedynym wrzodem na dupie jest tu konieczność tworzenia nadmiarowego kodu do definiowania punktów przecięcia. Po co ta pusta metoda? Ja nie wiem i nie rozumiem.
Generalnie wszytko bangla. Co ciekawe jak w linii poleceń zrobimy sobie coś takiego:

Listing 2. Aspekt kompilowany javac

C:\Documents and Settings\Asia\workspace\SimpleAspectJ\src>javac -d ..\bin pl\koziolekweb\blog\aspectj\*.java -classpath g:\Bartek\eclipse\plugin\org.aspectj.runtime_1.6.6.20090930185500\aspectjrt.jar;.

To po uruchomieniu kodu całość będzie działać, a jedyną różnicą będzie brak aspektów. No właśnie jak działa kompilator…

Mechanizm waevingu AspectJ

Istnieją trzy metody wstrzykiwania kodu aspektu do kodu programu. Pierwszy z nich to mechanizm statyczny oparty o analizę kodu źródłowego. Jest on bardzo dobry jeżeli chcemy wstawić kod „na mur beton”. Dodatkowo za pomocą analizy statycznej można rozszerzyć klasę o dodatkowe interfejsy wraz z implementacją (też będzie o tym). Z drugiej strony statycznie nie uzyskamy kontekstu wywołania aspektu co powoduje, że odpadają nam pewne fajne feature’y. Metoda ta jest stosowana przez bezpośrednie wywołanie kompilatora ajc w eclipse.
Drugą metodą jest wstawianie w bytecode. Jeżeli mamy skompilowany kod javowy i aspektowy w osobnych bibliotekach to za pomocą kompilatora ajc możemy dokonać modyfikacji kodu bajtowego i wstawić kod aspektu. To też jest mechanizm statyczny, jednak umożliwia wykonanie wiązania na kodzie do którego nie mamy źródeł. Jest to bardzo przydatne rozwiązanie gdy chcemy dokonać reverse engineeringu na małą skalę tzn. chcemy zmienić sposób działania biblioteki bez modyfikacji źródeł i pisania kodu związanego (rozszerzającego) z tą biblioteką. Mechanizm ten można porównać do linkera znanego z C/C++. Nie jest to to samo, ale generalnie efekt jest zbliżony.
Trzecią metodą jest dynamiczne wiązanie kodu aspektu z kodem aplikacji. Jest to upierdliwe, ale bardzo dobre jeżeli zależy nam na aspektach wykonywanych w ramach kontekstu wykonania (o tym też będzie). Generalnie sprowadza się to do napisania pliku XML i umieszczeniu go w katalogu META-INF. Następnie wystarczy uruchomić JVM z odpowiednim parametrem.

Listing 3. Plik aop-ajc.xml



	
		
	

Listing 4. Użycie agenta

C:\Documents and Settings\Asia\workspace\SimpleAspectJ\bin>java -classpath g:\Bartek\eclipse\plugins\org.aspectj.runtime_1.6.6.20090930185500\aspectjrt.jar;.;.\META-INF -javaagent:g:\Bartek\eclipse\plugins\org.aspectj.weaver_1.6.6.20090930185500\aspectjweaver.jar pl.koziolekweb.blog.aspectj.Main

Tu uwaga ważna jest kolejność parametrów najpierw classpath, potem javaagent, a na końcu klasa uruchamiana. Inaczej nie będzie banglać.
Ostatnim elementem naszej zabawy w dniu dzisiejszym będzie odpowiedź na pytanie jak używać aspektów odnośnie klas adnotowanych. Skoro już umiemy definiować aspekty jako klasy java za pomocą adnotacji to należy uzupełnić ten kawałek wiedzy.

Po co nam adnotacje?

W naszym przykładzie byliśmy proszeni o hasło przed uruchomieniem metody welcome(), ale chcemy, żeby system jednak pozwalał nam na wyświetlenie komunikatu powitalnego i po zalogowaniu komunikatu dla konkretnego użyszkodnika. W pierwszym odruchu można zmienić definicję punktu przecięcia, ale jest to do dupy. Dwa główne powody to ograniczenie zasięgu aspektu do metod o konkretnej nazwie co może i jest fajne, ale niestety mało przydatne oraz pozbawianie się przenośności. W dużym systemie ten drugi powód jest szczególnie widoczny, ponieważ musimy definiować punkty przecięcia dla wielu klas z różnych pakietów. Powoduje to niepotrzebną komplikację kodu i utrudnia późniejszą jego konserwację.
Na początek dopiszmy jeszcze jedną klasę, które będzie siedziała w innym pakiecie niż nasza aplikacja oraz będziemy wykorzystywać jej metody podobnie jak klasy Service:

Listing 5. Klasa SomeService

package pl.koziolekweb.blog.aspectj.services;

public class SomeService {

	public void message() {
		System.out.println("wiadomość");
	}
	
	public void messageToHex(Long long1) {
		System.out.println("Wartość hex: " + Long.toHexString(long1));
	}
}
// zmiany w Main
	service.welcome("Koziołek");
	SomeService someService  = new SomeService();
	someService.message();
	someService.messageToHex(new Long(666l));

Jeżeli teraz trzeba było by dla każdej metody, którą chcemy zabezpieczyć, definiować pountcut to jedyną słuszną drogą było by pocięcie się. Zdefiniujmy sobie zatem adnotację @Secure

Listing 5. Adnotacja Secure

package pl.koziolekweb.blog.aspectj;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface Secure {

}

Oznaczymy teraz za jej pomocą metodę Service.welcome(String) i klasę SomeService. Następnie zmieńmy nasz aspekt na taki jak poniżej:

Listing 6. Nowa wersja aspektu.

package pl.koziolekweb.blog.aspectj;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SecurityAspect {

	private NKWD nkwd = new NKWD();

	@Pointcut("execution( @Secure * *(..)) || call( * (@Secure *).*(..))")
	public void auth() {
	}

	@Before("auth()")
	public void authBefore() {
		System.out.println("NKWD działa");
		nkwd.auth();
	}

}

Uruchamiamy

Listing 7. Wynik działania programu

Witamy w systemie
NKWD działa
podaj hasło: dupa
Witaj Koziołek
NKWD działa
wiadomość
NKWD działa
Wartość hex: 29a

Jak widać wszystko zadziałało zgodnie z oczekiwaniami. Jedynym dużym wrzodem jest tutaj definicja podana w punkcie przecięcia, ale brak mi jeszcze wprawy by zapisać ładniej warunek. Z drugiej strony jest on moim zdaniem jaśniejszy, bo w kolejnych elementach wyrażenia warunkowego widać co odnosi się do metody, a co do typu.
Ostatnią kwestia jest różnica pomiędzy execution i call. Generalnie żadna nie jest widoczna w działaniu. W przypadku execution kod aspektu wstawiany jest do ciała metody (na początku lub końcu). W przypadku call kod jest wstawiany w każdym miejscu wywołania.

Podsumowanie

Jak widać programowanie aspektowe daje ogromne możliwości, a odpowiednie zastosowanie jego elementów ułatwia życie.

Napisz odpowiedź

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax