Korišćenje aspekata
swagl05
27 Jul 2009
Ovaj članak opisuje korišćenje aspekata upotrebom biblioteke AspectJ.
Šta su aspekti
Aspekti se koriste da dodaju funkcionalnost postojećim klasama, obično
proširivanjem postojećih metoda, ali i dodavanjem potpuno novih metoda, polja
ili interfejsa.
Tipičan primer korišćenja aspekata je presretanje poziva određenih metoda u
cilju izvršavanja operacija
kao što su logovanje, security ili debagovanje. Osim toga, aspekti se mogu
koristiti i za implementaciju višestrukog nasleđivanja u Javi. Neki frejmvorci,
kao što je Spring, značajan deo svoje funkcionalnosti implementiraju korišćenjem
aspekata.
Praktičan primer
Prikazaćemo korišćenje aspekata u standalone Java aplikacijama, uz naznake kako
se oni mogu koristiti i u Spring okruženju ili u aplikacionom serveru.
Uvođenje aspekata u projekat nije složeno i ne zahteva posebnu podršku IDE-a,
ili izmenu build procesa i korišćenje posebnog kompajlera. Biblioteka AspectJ se
sastoji iz dva .jar fajla: aspectjrt.jar i aspectjweaver.jar.
Jedino što je dodatno potrebno je da se prilikom startovanja programa navede
dodatni parametar -javaagent koji inicijalizuje AspectJ. Ako je
aspectjweaver.jar u našem projektu smešten u direktorijum lib onda će se program
startovati sa
java -javaagent:lib/aspectjweaver.jar ....
U slučaju Eclipse potrebno je u dijalogu Run->Open Run Dialog->Arguments->VM
arguments uneti:
-javaagent:lib/aspectjweaver.jar
Sledeći primer ilustruje upotrebu aspekata za presretanje poziva metoda:
package p1;
public class MyService {
public int operationPlus(int a, int b) { return a + b; }br /> public int operationPlus(int a, int b, int c) { return a + b + c; }
public int operationNegation(int a) { return -a; }
}
public class Main {
public static void main(String[] args) {
System.out.println("Calling some operations");
MyService service = new MyService();
int i1 = service.operationPlus(10, 20);
int i12 = service.operationPlus(10, 20, 30);
int i2 = service.operationNegation(30);
System.out.println("Bye");
}
}
Imamo postojeću klasu MyService, i pozive njenih metoda. Želimo napraviti aspekt
koji presreće pozive određenih operacija.
package p2;
import org.aspectj.lang.annotation.*;
@Aspect
public class MyLoggingAspect {
@Before("execution(int p1.MyService.operationPlus(int, int))")
public void logCall() {
System.out.println("Hello from interceptor");
}
}
Dakle, aspekt je obična Javina klasa anotirana odgovarajućim AspectJ
anotacijama. Ovaj aspekt presreće poziv metoda
int operationPlus(int a, int b)
klase MyService i ispisuje jednostavnu poruku.
Da bi se ovaj proram uspešno izvšio potrebno je (osim korišćenja parametra
-javaagent prilikom startovanja programa) u projektu dodati fajl aop.xml u
direktorijum META-INF u kome je naveden svaki aspekt koji je korišćen.
META-INF/aop.xml:
<aspectj>
<aspects>
<aspect name="p2.MyLoggingAspect"/>
</aspects>
</aspectj>
Posle izvršavanja programa vidimo da je poziv medoda operationPlus(10, 20)
presretnut i da je ispisana odgovarajuća poruka.
Posle ovog najjednostavnijeg primera, pogledajmo nešto realnije i korisnije
primere, koji ilustruju presretanje metoda na osnovu bilo kojih elemenata koji
određuju metod kao što su imena paketa, klase ili metoda, tipovi parametra ili
povratne vrednosti.
Da bismo presreli sve pozive metoda operationPlus bez obzira na tip i broj
parametara koristimo:
@Before("execution(int p1.MyService.operationPlus(..))")
Za sve metode koji vraćaju int čiji naziv počinje sa operation, bez obzira na
parametre:
@Before("execution(int p1.MyService.operation*(..))")
Za sve metode, svih klasa u paketu (ne i u potpaketu - za potpaket morali bismo
dodati još jedanput .*):
@Before("execution(* p1.*.*(..))")
Dakle za zamenu se koristi znak '*' u svim slučajevima osim kod parametara
metoda gde se koristi '..'
Evo još jednog primera koji je naročito interesantan, jer prikazuje mogućnost
pronalaženja metoda koji su anotirani nekom anotacijom - u ovom primeru
koristimo anotaciju @WebMethod, a može se naravno koristiti i naša sopstvena
anotacija (što je naročito korisno). U ovom primeru pronalaze se sve metode
anotirane sa @WebMethod u bilo kojoj klasi u paketu p1:
@Before("execution(@javax.jws.WebMethod * p1.*.*(..))")
Dodatni primeri se mogu naći u AspectJ dokumentaciji:
http://www.eclipse.org/aspectj/doc/released/progguide/language-joinPoints.html
Pristup argumentima
U prethodnom primeru videli smo da presretač logCall() uvek ispisuje isti tekst,
bez obzira na to koji metod je presretnut.
Pristup argumentima metode vrši se na sledeći način:
@Before("execution(int p1.MyService.operationPlus(int, int)) && args(a) &&
args(b) ")
public void logCall(int a, int b) {
System.out.format("Hello from interceptor %d, %d", a, b);
}
Ako bismo želeli univerzalni pristup svim parametrima, zatim imenu metoda i još
nekim vrednostima, možemo koristiti AspectJ klasu JoinPoint.
@Before("execution(* p1.MyService.*(..))")
public void logCall(JoinPoint jp) {
System.out.println("Method name " + jp.getSignature().getName());
for (Object arg: jp.getArgs()) {
System.out.println(arg);
}
}
Osim sa @Before moguće je presresti i poziv metode posle njenog poziva,
korišćenjem @After, ili postići kombinovani efekat korišćenjem @Around, što je
naročito interesantno, jer je u tom slučaju moguće uticati na to da li će se
poziv uopšte desiti, a moguće je i menjati vrednost argumenata ili vratiti
željenu povratnu vrednost:
@Around("execution(* p1.MyService.*(..))")
public Object logCall(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("Method name " + pjp.getSignature().getName());
for (Object arg: pjp.getArgs()) {
System.out.println(arg);
}
Object result = pjp.proceed();
System.out.println("Return: " + result);
return result;
}
Ovaj primer je najkorisniji za logovanje poziva bilo koje metode (jedino izgleda
da nije moguće pristupiti nazivima argumenata, jer se oni ne nalaze uvek u
bajtkodu, tj. u .class fajlu (da li ste ikad primetili da vam IDE, ukoliko ga
niste podesili da gleda JDK umesto JRE-a, za argumente Javinih funkcija, npr.
String.format(), posle ctrl+space stavi arg1, arg2, itd umesto njihovih pravih
naziva). Korisna stvar koja se ovde može dodati je merenje vremena izvršavanja.
Kao što smo naveli, aspekt može na različite načine da utiče na postojeće klase.
U prethodnim primerima aspekt je dodavao kod metodama, međutim osim dodavanja
koda postojećim metodama, aspekt može da dodaje potpuno nove metode klasama,
nova polja, može da učini da klasa implementira nove interfese. AspectJ
implementira aspekte putem manipulacije bajtkodom i tu su mogućnosti jako
velike. Presretanje poziva je samo jedan mali primer šta je sve moguće uraditi
pomoću aspekata, iako se u ne malom broju slučajeva korišćenje aspekata svodi na
presretanje poziva.
Višestruko nasleđivanje
Na kraju, navešćemo još jedan interesantan primer upotrebe aspekata, a to je
promena bajtkoda klase tako da ona implementira željeni interfejs upotrebom
AspectJ anotacije @DeclareMixin, čime se može postići efekat višestrukog
nasleđivanja u Javi.
package p3;
public interface Flyable {
void fly();
}
public class Person {
public String name, surname;
}
Imamo posojeću klasu klasu Person i interfejs Flyable. Recimo da imamo i
implementaciju inerfejsa Flyable:
public class ExpertFlyable implements Flyable {
public void fly() { System.out.println("Flying like expert"); }
}
Želimo da učinimo da klasa Person implementira interfejs Flyable:
@Aspect
public class MakeFlyableAspect {
@DeclareMixin("p3.Person")
public Flyable flyableMixin() {
return new ExpertFlyable ();
}
}
public class Main {
public static void main(String[] args) {
Person p = new Person();
((Flyable)p).fly();
}}
Ne zaboravimo da dodamo aspekt u aop.xml:
META-INF/aop.xml:
<aspectj>
<aspects>
<aspect name="p2.MyLoggingAspect"/>
<aspect name="p3.MakeFlyableAspect"/>
</aspects>
</aspectj>br />
Posle startovanja ovog primera vidimo da klasa Person zaista implementira
interfejs Flyable.
Napravićemo još jednu izmenu koja će prethodni primer učiniti vrlo
upotrebljivim. Želimo da postignemo da sve klase koje su označene određenom
anotacijom implementiraju interfejs Flyable. Napravićemo našu anotaciju
@MakeFlyable :
public @interface MakeFlyable {}
Aspekt ćemo izmeniti tako da prepoznaje bilo koju klasu, u bilo kojem paketu ili
potpaketu, koja je anotirana sa @MakeFlyable:
..
@DeclareMixin("@p3.MakeFlyable *.*.*")
..
@MakeFlyable
public class Person {
public String name, surname;
}
Posle startovanja, prikazuje se isti rezultat kao u prethodnom primeru. Možemo
zamisliti postojanje više ovakvih interfejsa (npr. Flyable, Knowledgable), i
anotiranjem klase Person postići da ona implementira svaki od njih, možda i da
na osnovu agrumenata anotacije pruži različitu implementaciju.
Na kraju i par možda naprednijih tema o internom radu aspekata, za koje nije neophodno
razumevanje da bi se
oni koristili kao što je opisano u prethodnim
primerima.
Ukratko o instrumentaciji bajtkoda
AspectJ implementira aspekte vršeći manipulaciju bajtkodom klasa. Ova
manipulacija se može vršiti na dva načina: u trenutku kompajliranja (primenom
posebnog AspectJ kompajlera) ili tokom izvršavanja programa u trenuktu
učitavanja klase (zahvaljujući mogućnosti da se presretne proces učitavanja
klasa koji pruža Java preko argumenta -javaagent). Ovaj članak je opisao drugi
način, jer se tada AspectJ lakše koristi, i njegova upotreba ne zahteva
korišćenje posebnog kompajlera. Na uštrb jednostavnijeg korišćenja imamo nešto
lošije performanse prilikom učitavanja, tj. prvog korišćenja klase (ali ne i
tokom kasnijeg izvršavanja), jer se u tom trenutku vrši instrumentacija bajtkoda
klase. U slučajevima kad se želi izbeći ovaj mali overhead, može se koristiti
poseban AspectJ kompajler, koji proizvodi već instrumentizovane klase. Prednost
ovog načina može da bude i u tome što, iako je proces buildovanja nešto
složeniji (mada u slučaju korišćenja mavena kao build alata postoji vrlo korisni
aspectj-maven-plugin), instaliranje u različitim okruženjima i serverima je
jednostavnije, jer ne zahteva izmenu načina startovanja servera (sa -javaagent)
AspectJ i Spring
UU ovom članku akcenat je bio na korišćenju biblioteke AspectJ u nezavisno od
nekog drugog frejmorka. AspectJ se može koristiti
u kombinaciji sa Springom. Prvo malo istorijata - Spring je još od svoje prve
verzije imao podršku za aspekte, zvanu Spring AOP, nezavisno od biblioteke
AspectJ. Čak je i dobar deo svojih funkcionalnosti implementirao pomoću tih
aspekata. Spring AOP ne pruža toliki spektar mogućnosti kao AspectJ i praktično
samo podržava presretanje poziva, što je ipak dovoljno za mnoge potrebe, a
naročito za njegove interne potrebe za implementaciju upravljanja transakcijama
i sigurnosti ....Ono što ga razlikuje od AspectJ-a je način na koji implementira
aspekte: za razliku od AspectJ-a koji vrši manipulaciju bajtkodom postojećih klasa
(bilo u trenutku kompajliranja ili učitavanja klase), Spring AOP uopšte ne dira
postojeću klasu, već generiše proxy klasu koja wrapuje postojeću klasu i
prosleđuje joj pozive. Iz ovog načina realizacije jasno je zašto spring AOP može
da podrži samo presretanje poziva, a ne i neke druge mogućnosti, koje zahtevaju
izmenu postojeće klase.
Kako ovo praktično utiče na korisnike Spring AOP-a?br />
Prvo, performanse su nešto lošije u odnosu na AspectJ jer se prilikom svakog
poziva prolazi kroz proxy. Korišćenje proxyja se može bukvalno videti u
debageru, za razliku od AspectJ-a koji izmeni klasu pre izvršavanja i ne
ostavlja traga u debageru.
Drugo,
olakšan je deployment, jer ne zahteva poseban kompajler ili korišćenje argumenta
-javaagent.
Od verzije 2 stvari postaju malo zamršenije: omogućeno je da se za definiciju
Spring AOP aspekata koriste AspectJ anotacije, tačnije podskup AspectJ anotacija
koji se odnosi na presretanje poziva. Dakle AspectJ se ovde koristi samo za
parsiranje anotacija i proveru da li data klasa ili metoda potpada pod aspekt. I
dalje je način implementacije aspekata isti, dakle korišćenjem proksija, kao i
podrška samo za presretanje poziva. Ne koristi se AspectJ manipulacija
bajtkodom, već samo njegov parser.
Ovakav način rada je podrazumevani i u poslednjim verzijama Springa.
Dodatnu mogućnost, koja nije podrazumevano uključena, predstavlja potpuno
korišćenje AspectJ-a umesto tradicionalnog Spring proxy zasnovanog mehanizma.
Razlozi za upotrebu ovog režima rada su korišćenje svih mogućnosti AspectJ-a,
kao i poboljšanje performansi u odnosu na proxy zasnovane aspekte.
Zaključak
Kao što ste videli, uvođenje aspekata u vaš projekat ne zahteva velike izmene u
njegovoj strukturi.
Aspekti mogu biti korisni i ako se ne koriste u produkcionoj verziji koda, već
samo tokom razvoja, za debagovanje ili merenje performansi.
Nadam se da ste dobili ideje kako da upotrebite aspekte u sopstvenim projektima.
Eclipse projekat: AspectJTest.zip
(ne zaboravite da stavite -javaagent:lib/aspectjweaver.jar prilikom pokretanja
programa)