• Nem Talált Eredményt

Góbi Attila, Kozsik Tamás, Vezér Boglárka

2. Az AspectJ nyelv

#18 = Utf8 Time

#19 = NameAndType #26:#27 // advance:(I)V

#20 = Class #28 // java/lang/System

#21 = NameAndType #29:#30 // out:Ljava/io/PrintStream;

...

#26 = Utf8 advance

#27 = Utf8 (I)V

#28 = Utf8 java/lang/System

#29 = Utf8 out

#30 = Utf8 Ljava/io/PrintStream;

3. kódlista.Részletek aMainosztály bájtkódjából

Végigkövethetjük például a Time.advance metódus meghívását, mely a#4hivatkozás segítségével történik. A#4egyMethodref, mely a

#2osztályra és#19szignatúrára hivatkozik. A#2szimbólumhoz tartozó osztálynév a#18, amihez aTimeérték tartozik. A#19szignatúra elemeit a #26, valamint a #27 határozza meg. Az előbbi az advance nevet, az utóbbi az(I)Vparaméterlista-specifikációt (intparaméterű,void visszatérési értékű) tárolja.

2. Az AspectJ nyelv

Az aspektus-orientált programozás a programkód modularizálásában kíván segíteni. A paradigma alapvetése, hogy egy hagyományos módszer-rel modulokra bontott szoftverben lesznek olyan funkcionalitások (más szóval vonatkozások, concerns), melyek implementációja szétszóródik több modulra, és összekeveredik más vonatkozások kódjával. Ezeket az úgynevezett keresztbe vágó vonatkozásokat (cross-cutting concerns) tudjuk külön modulokba, aspektusokba kiemelni, a fő modulfelbontásról leválasztani az AOP segítségével. Közismert példák aspektusok haszná-latára a naplózás (logging) és a jogosultság-ellenőrzés (authorization).

Ezek olyan programfunkciók, amelyek a teljes programon végighúzód-hatnak, és hagyományos, például objektum-orientált megközelítéssel nem igazán jól modularizálhatók. A keresztbe vágó vonatkozások az egész programban szétszóródó megvalósítása jelentősen hozzájárul a program komplexitásához, nehezíti a kód megértését és karbantartását.

Az aspektusok segítségével egy modulba gyűjthetjük egy keresztbe

vágó vonatkozás megvalósításához tartozó kódunkat. A szoftvernek to-vábbra is lesz egy elsődleges dekompozíciója, egy alapvető modulokra bontása, amely például Java esetén lehet egy objektumelvű megközelí-tésnek megfelelő, típusdefiníciókból álló struktúra, de ezt most kiegé-szíthetjük másfajta modulokkal, aspektusokkal is. Az aspektusok és az elsődleges modulfelbontásból származó definíciók összefüggő programmá való alakítását aspektusszövésnek (aspect weaving) nevezzük. Ennek során az aspektusokban leírt viselkedést beillesztjük az eredeti modulok minden olyan pontjára (join point), ahol ez szükséges. Ehhez persze az kell, hogy az aspektusokból meg tudjuk nevezni a join pointokat, azaz hivatkozni tudjunk az eredeti modulfelbontással előállt struktúra egyes részeire. A módszernek akkor van leginkább haszna, ha az aspektusok-ban olyan kódrészleteket tudunk összegyűjteni, amelyeket több helyre is be kell szőni, azaz amikor a szövést több join pointon rendeljük el (kvantált hivatkozás join pointokra, quantification).

A Java nyelv egy aspektusorientált kiterjesztése az AspectJ. Az AspectJ csupán pár nyelvi elemmel bővíti ki a Javát, hogy a fenti célokat elérje.

• A pointcut-kifejezések join pointok halmazát nevezik meg, va-lamint hivatkozhatunk segítségükkel a join pointokon elérhető programértékekre. Pointcutoknak (pointcut-deklarációkkal) nevet is adhatunk.

• Az advice egy alprogramszerű programegység, amelyet egy pointcuttal megnevezett join pointok elérésekor hajthatunk végre.

Az AspectJbefore,afterésaroundtípusú advice-t támogat, melyek rendre a hivatkozott join point előtt, után, illetve helyett/körül hívódnak meg.

• Azinter-type declarationsegítségével típusok mezőkkel és metó-dusokkal való kiterjesztését, öröklődési kapcsolatok finomítását, domain-specifikus szemantikus összefüggések fordítási idejű ellen-őrzését írhatjuk elő.

• Azaspect az a modul, amelyben egy keresztbe vágó vonatkozás megvalósításához szükséges pointcutok, advice-ok, inter-type deklarációk és Java elemek bevezetésre kerülnek.

Az aspektusszövés folyamatát egy példán keresztül mutatjuk be.

Tekintsük a már emlegetett Timeosztályt egy részletét.

public class Time { private int hour, min;

public int getHour(){return hour; } public int getMin(){ return min; } public void advance(int minutes){

min += minutes;

hour += min/60;

min %= 60;

}

@Override public String toString(){

return String.format("%1$02d:%2$02d",hour,min);

} }

4. kódlista.ATimeosztály

A Time.advance metódus egy végrehajtása kapcsán kétféle join pointot is azonosíthatunk: a metódus meghívását, illetve a metódustörzs lefutását. Az előbbitcall, az utóbbitexecution join pointnak nevezzük.

Ha rászövünk ezekre a join pointokra, akkor az aspektusszövő az első esetben a hívást tartalmazó kódokat változtatja meg, a második esetben pedig a metódus kódját transzformálja.

Mind az execution, mind a call joint pointra van lehetőség advice-okat szőni, melyek futhatnak a metóduselőtt (before),után (after), illetve a metódus vagy a metódushíváskörül. Ez utóbbi tulajdonképpen azt jelenti, hogy az advice fut le a metódus helyett, és az advice-ban lehető-ségünk van úgy dönteni, hogy a metódushívást ténylegesen végrehajtjuk (aproceed utasítással kezdeményezhetjük a join point végrehajtását a köré szőtt around advice-ban). A következő aspektus, mellyel (a design-by-contract szemléletet követve) szerződéseket fogalmazunk meg aTime osztályhoz, ezeket demonstrálja.

public aspect Contracts {

void around(Time time, int mins):

execution(public void Time.advance(int)) && args(mins) && this(time) {

int oldMinutes = time.getHour() * 60 + time.getMin();

proceed(time,mins);

int newMinutes = time.getHour() * 60 + time.getMin();

if( oldMinutes + mins != newMinutes )

throw new AssertionError("Time.add: postcondition violated!");

}

after(Time time) returning:

call(public * *.*(..)) && target(time) && !within(Contracts) { int min = time.getMin();

if( min < 0 || min >= 60 )

throw new AssertionError("Time: class invariant violated!");

} }

5. kódlista.AspectJ példa

Az első advice (around execution) metódustörzs végrehajtása köré szövődik, a második (after call) pedig metódus hívása utánra. Az első advice aTime.advance metódus törzsét módosítja. A megadott pointcut kifejezés nem csak a szövés helyét adja meg: felfogja a metódusvégrehajtás paramétereit is. A this pointcut segítségével köthetünk nevet az objektumhoz, amire a metódust meghívták, az args segítségével pedig a paraméterlistában szereplő paraméterhez rendelhetünk nevet. A két paramétert az advice paraméterlistájában deklarálva a statikus típushelyességről is gondoskodik az aspektusszövő.

Az eredeti metódustörzs (proceed(time,mins)) végrehajtása előtti és utáni állapotot összehasonlítva az advice eldönti, hogy a metódus megfelel-e az elvárt viselkedésnek, és (nem ellenőrzött) kivételt vált ki, ha aTime.advancenem felel meg a szerződésének.

A második advice nem csak egy metódus meghívására hivatkozik, hanem aTimeosztály minden publikus metódusáéra, a visszatérési érték típusától, valamint formális paraméterek számától és típusától függet-lenül. A join pointok kvantálását a pointcutban a csillag szimbólum és a két pont értelemszerű használatával érhetjük el. A célunk ezzel az advice-szal, hogy minden metódushívás után ellenőrizzük a Time osztály invariánsát, és kivételt váltsunk ki, ha az osztályinvariáns sérül.

A returning záradék az after advice-ban azt fejezi ki, hogy csak a rendben véget ért hívások után szeretnénk az advice végrehajtását, ha a join point kivételt váltott ki, akkor nem.

A metódushívás join pointok esetében a target pointcut kötheti névhez azt az objektumot, amire a metódust meghívták; vegyük észre a hasonlóságot azexecutionjoin point esetén használtthispointcuttal.

Acall esetében is használható athispointcut, de azcall esetben a metódushívást kezdeményező objektumot azonosítja, nem azt, amire a metódust meghívták.

Az advice paraméterlistájában a híváshoz használt objektumot Time típusúként deklaráltuk, ez szűkíti le a vizsgált metódusokat a Time osztály metódusaira. A within pointcuttal a vizsgált join pointokat szűrhetjük. Az itt használt tagadó alakú!withinsegítségével kizárjuk a megnevezett join pointok közül azokat, amelyek aContracts aspektusban vannak. Ez azért fontos, mert ezzel az aspektussal nem szeretnénk szőni az ugyanebben az aspektusban bekövetkező eseményekre (nehogy a szövés végtelen rekurziót eredményezzen).

Nézzük most meg, hogyan módosul aMainosztálymainmetódusa (1. kódlista) a szövés hatására. Hasonlítsuk össze a 2. és a 6. kódlistát!

public static void main(java.lang.String[]);

Code:

0: new #17 // class Time 3: dup

4: invokespecial #19 // Method Time."<init>":()V 7: astore_1

16: invokevirtual #20 // Method Time.advance:(I)V

19: invokestatic #47 // Method Contracts.aspectOf:()LContracts;

22: aload_3

23: invokevirtual #51 // Method

Contracts.ajc$afterReturning$Contracts$2$aa874fa4:(LTime;)V 26: nop

27: getstatic #24 // Field java/lang/System.out:Ljava/io/PrintStream;

30: aload_1

31: invokevirtual #30 // Method

java/io/PrintStream.println:(Ljava/lang/Object;)V 34: return

6. kódlista.MegszőttMain.mainmetódus

ATime.advancemetódus hívása után a szőtt változatban egy osz-tályszintű és egy példánymetódus is meghívódik, mindkettő az aspektus kódját tartalmazóContractstípusdefinícióból. Vessünk egy pillantást az utóbbira, a 7. kódlistán. Világosan látható benne az osztályinvariáns ellenőrzésének és azAssertionErrorkivétel kiváltásának logikája.

public void ajc$afterReturning$Contracts$2$aa874fa4(Time);

Code:

0: aload_1

1: invokevirtual #44 // Method Time.getMin:()I 4: istore_2

5: iload_2 6: iflt 15 9: iload_2 10: bipush 60 12: if_icmplt 25

15: new #50 // class java/lang/AssertionError 18: dup

19: ldc #88 // String Time: class invariant violated!

21: invokespecial #54 // Method

java/lang/AssertionError."<init>":(Ljava/lang/Object;)V 24: athrow

25: return

7. kódlista.Azafteradvice bájtkódja

Hasonlóan érdekes az is, hogy a szövés hatására hogyan változik meg aTimeosztály bájtkódja, hogyan kerül be azadvancemetódusba az around advice által definiált programlogika. Ennek a felderítését azonban most az Olvasóra hagyjuk.

Végezetül megemlítjük, hogy az AspectJ többféle módon is képes szőni. A szövés valójában nem a forráskódok szintjén történik, hanem a forráskódból előállított bájtkód transzformálásával. Az ajc aspek-tusszövő Java és AspectJ forráskódok lefordítására és összeszövésére is használható, de arra is, hogy (Javából, Scalából vagy más nyelvből) előállított bájtkódot módosítson az aspektusok kódja alapján. Egy másik lehetőség az, hogy a bájtkódokat ne fordítási időben módosítsuk, hanem akkor, amikor futáskor betöltődnek a Java virtuális gépbe. A betöltési idejű szövés az AspectJajparancsával történhet.