JavaSvet - otvorena java zajednica

 
glavna stranica arr2javasvet  english version arr2java.net

Korišćenje aspekata

swagl05
Sadržaj:
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)