Az AspectJ programozási nyelv

Példaprogramok

Fejlesztői Aspektusok

Ez a rész olyan példákat mutat, amelyek egy Java program fejlesztése során hasznosak. Ezek az aspektusok megkönnyítik a program debug-olását, tesztelését, és optimalizálását. Az aspektusok által megvalósított funkciók az egyszerű nyomkövetéstől egészen a program belső konzisztenciájának teszteléséig terjednek. Az AspectJ használatával lehetővé válik az ilyen jellegű feladatok modularizálása, és így ezek a funkciók könnyedén ki/be kapcsolhatók az igényeknek megfelelően.

Nyomkövetés, naplózás, és mérés

Ez az első példa bemutatja, hogy lehet növelni a program belső munkavégzésének "láthatóságát". Ez egy egyszerű nyomkövető aspect, ami üzenetet ír ki, meghatározott metódushívások alkalmával. A mi alakzatszerkesztő példánkban, egy ilyen aspect nyomon követi a pontok rajzolását.

aspect SimpleTracing { pointcut tracedCall(): call(void FigureElement.draw(GraphicsContext)); before(): tracedCall() { System.out.println("Entering: " + thisJoinPoint); } }

ez a kód használ egy speciális változót a thisJoinPoint-ot. Minden advice törzsében ennek a változónak az értéke egy olyan objektum, amely leírja az aktuális csatlakozási pontot. Ennek a kódrészletnek a hatására a következő sor íródik ki, valahányszor a FigureElement osztály egy objektumán meghívódik a draw metódus:

Entering: call(void FigureElement.draw(GraphicsContext))

Egy másik tracing megvalósítás:

public class Trace { public static int TRACELEVEL = 0; public static void initStream(PrintStream s) {...} public static void traceEntry(String str) {...} public static void traceExit(String str) {...} } aspect TraceMyClasses { pointcut myClass(): within(TwoDShape) || within(Circle) || within(Square); //a megfigyelt osztályaim pointcut myConstructor(): myClass() && execution(new(..)); //konstruktorok figyelése pointcut myMethod(): myClass() && execution(* *(..)); before (): myConstructor() { Trace.traceEntry("" + thisJoinPointStaticPart.getSignature()); } after(): myConstructor() { Trace.traceExit("" + thisJoinPointStaticPart.getSignature()); } before (): myMethod() { Trace.traceEntry("" + thisJoinPointStaticPart.getSignature()); } after(): myMethod() { Trace.traceExit("" + thisJoinPointStaticPart.getSignature()); } }

És egy harmadik, professzionálisabb tracing megvalósítás:

abstract aspect Trace { public static int TRACELEVEL = 0; protected static PrintStream stream = null; protected static int callDepth = 0; public static void initStream(PrintStream s) { stream = s; } protected static void traceEntry(String str, Object o) { if (TRACELEVEL == 0) return; if (TRACELEVEL == 2) callDepth++; printEntering(str + ": " + o.toString()); } protected static void traceExit(String str, Object o) { if (TRACELEVEL == 0) return; printExiting(str + ": " + o.toString()); if (TRACELEVEL == 2) callDepth--; } private static void printEntering(String str) { printIndent(); stream.println("Entering " + str); } private static void printExiting(String str) { printIndent(); stream.println("Exiting " + str); } private static void printIndent() { for (int i = 0; i < callDepth; i++) stream.print(" "); } abstract pointcut myClass(Object obj); pointcut myConstructor(Object obj): myClass(obj) && execution(new(..)); pointcut myMethod(Object obj): myClass(obj) && execution(* *(..)) && !execution(String toString()); before(Object obj): myConstructor(obj) { traceEntry("" + thisJoinPointStaticPart.getSignature(), obj); } after(Object obj): myConstructor(obj) { traceExit("" + thisJoinPointStaticPart.getSignature(), obj); } before(Object obj): myMethod(obj) { traceEntry("" + thisJoinPointStaticPart.getSignature(), obj); } after(Object obj): myMethod(obj) { traceExit("" + thisJoinPointStaticPart.getSignature(), obj); } }

Hogy megérthessük, miért előnyös ezt AspectJ-ben megvalósítani, vegyük azt az esetet, ha változik azon metódusok köre, amiket nyomonkövetni akarunk. Ehhez mindössze a tracedCall pointcut definícióján kell változtatnunk, az egyes metódusok kódja változatlan marad.

Hibakeresés során a programozók gyakran jelentős idő/munka ráfordításával tudják csak kialakítani a nyomkövetési pontok megfelelő halmazát, egy adott problématípushoz. Amikor a hibakeresés lezárult, vagy annak tűnik, elég nagy veszteség ezeket elveszíteni a nyomkövető utasítások kitörlésével. A másik alternatíva, hogy megjegyzésként megmaradnak, ez azonban megnehezíti a programkód olvasását, különösen, ha különböző problématípusokhoz használt utasítások keverednek.

AspectJ segítségével könnyen lehet egyrészt megőrizni, a nyomkövető pontok megtervezésével eltöltött munkát, másrészt egyszerűen eltávolíthatók a programból, ha nincs már szükség rájuk. Ez megtehető, ha minden problématípushoz írunk egy aspect-et, és a fordítási listából egyszerűen eltávolítjuk, ha nem szükséges.

Mérés és naplózás

A második példánk bemutatja, hogyan lehet specializált mérések végezni. Annak ellenére, hogy számos kifinomult mérési segédprogram létezik, melyek sokféle információ gyűjtésére, és ezek különböző megjelenítésére alkalmasak, előfordulhat, hogy valami egészen specializált viselkedését akarjuk naplózni, és/vagy mérni a programunknak. Az ilyen esetekben gyakran lehet ezt egy az alábbiakhoz hasonló egyszerű aspektusokkal kezelni.

Példaként, a következő aspect megszámlálja a Line rotate metódusainak hívását, és a Point azon set* metódushívásait, amik a rotate hívásokon belül történnek.

aspect SetsInRotateCounting { int rotateCount = 0; int setCount = 0; before(): call(void Line.rotate(double)) { rotateCount++; } before(): call(void Point.set*(int)) && cflow(call(void Line.rotate(double))) { setCount++; } }

Ez az AspectJ nagy erőssége a szokványos mérő ill. naplózó segédprogramokkal szemben, hogy rendkívül összetett méréseket tesz lehetővé.

Elő- és utófeltételek

Sok programozó használja a "Design by Contract" programozási stílust (Bertand Meyer Object-Oriented Software Construction, 2/e). Ebben a stílusban explicit kódrészlet teszteli az elő, és utófeltételeket metódusok hívásakor.

Az AspectJ-ben lehetőség van ezen elő, és utófeltételek tesztelésére moduláris formában. Például:

aspect PointBoundsChecking { pointcut setX(int x): (call(void FigureElement.setXY(int, int)) && args(x, *)) || (call(void Point.setX(int)) && args(x)); pointcut setY(int y): (call(void FigureElement.setXY(int, int)) && args(*, y)) || (call(void Point.setY(int)) && args(y)); before(int x): setX(x) { if ( x < MIN_X || x > MAX_X ) throw new IllegalArgumentException("x is out of bounds."); } before(int y): setY(y) { if ( y < MIN_Y || y > MAX_Y ) throw new IllegalArgumentException("y is out of bounds."); } }

A fenti programrész, egy egyszerű értékhatár ellenőrzést valósít meg. Érdemes megfigyelni, hogy a setX pointcut kijelöl minden módot, ahogy az x koordinátája a pontnak megváltozhat, vagyis a setX metódust, és a setXY metódus "felét". Ez egy példa egyben arra is, milyen "finomhangolt" kereszthivatkozásokat tesz lehetővé a nyelv.

A nyomkövetéshez hasonlóan az elő, utófeltételek tesztelése is könnyedén ki/be kapcsolható, egy egyszerű újrafordítással.

Szerződés kikényszerítés (Contract Enforcement)

A tulajdonság alapú kereszthivatkozás lehetővé tesz még kifinomultabb szerződés kikényszerítést. Ezek közül az egyik igen hatékony módszer, azon metódushívások kiszűrése, amelyeknek egy korrekt program esetében nem szabadna létezniük. Például a következő aspect kikényszeríti azt a megszorítást, hogy az alakzat elemek regisztrációjába, csak a gyártó metódusok meghívásával kerülhessen be új elem. Ezzel biztosítva, hogy egyetlen alakzat elem sem kerül kétszer regisztrálásra.

static aspect RegistrationProtection { pointcut register(): call(void Registry.register(FigureElement)); pointcut canRegister(): withincode(static * FigureElement.make*(..)); before(): register() && !canRegister() { throw new IllegalAccessException("Illegal call " + thisJoinPoint); } }

Az aspect használja a withincode primitív pointcut-ot, aminek segítségével ki tudja választani a gyártó metódusok (make*) törzsében előforduló hívásokat.

Végleges aspektusok

Ebben a fejezetben olyan aspektusokat fogunk bemutatni, amik a program szerves részét képezik, sokkal inkább új funkcionalitás megvalósítása a céljuk és nem elsősorban a program belső szerkezetének megjelenítése. Most is a név alapú aspektusokkal kezdjük, és aztán rátérünk a tulajdonság alapú aspektusokra. Általában a név alapú aspektusok csak néhány metódust érintenek. Éppen ezért lehetnek a következő lépés az AspectJ bevezetésében egy projektbe. Különösen, hogy bár általában kicsik és egyszerűek, gyakran jelentős hatásuk van, a program érthetőségére és karbantarthatóságára.

Változásfigyelés

Az eső példa bemutat egy egyszerű funkcionalitás megvalósítását. A kódrészlet a képernyőfrissítést segíti. Az aspektus szerep egy "dirty" jelzés kezelése, amely azt jelzi, hogy az utolsó frissítés óta mozgattak-e valamit a képernyőn.

Ennek a megvalósítása aspektusként egyértelmű. A testAndClear metódust hívja a képernyőkezelő kód, hogy megállapítsa elmozdult-e valami a képernyőn. Ez visszaadja a "dirty" állapotát, és beállítja hamisra. A move pointcut kiszűri az összes metódushívást, amivel egy objektum helyzete változhat. Az advice, ami minden hívás után hajtódik végre, pedig beállítja a "dirty"-t igazra.

aspect MoveTracking { private static boolean dirty = false; public static boolean testAndClear() { boolean result = dirty; dirty = false; return result; } pointcut move(): call(void FigureElement.setXY(int, int)) || call(void Line.setP1(Point)) || call(void Line.setP2(Point)) || call(void Point.setX(int)) || call(void Point.setY(int)); after() returning: move() { dirty = true; } }

Ez az egyszerű példa is jól megmutatja az AspectJ használatának előnyeit. Nézzük meg, hogyan lehetett volna ezt Java-ban megvalósítani: Létrehoznánk egy segéd osztályt, ami a dirty -ből, a testAndClear metódusból, és egy setFlag metódusból állna. Mindegyik metódus, ami egy elemet mozgat, tartalmazna egy setFlag hívást.

Nézzük meg az AspectJ által kínált megoldás előnyeit:

A funkcionalitás teljesen egységbezárt. A move pointcut egyértelműen meghatározza az érintett metódusokat, így a programozót nem csak a setFlag hívásoktól szabadítja meg, de láthatóvá teszi a program szerkezetét.

Egyszerűbb a továbbfejlesztés. Tegyük fel, hogy nem csak azt szeretnénk tudni, hogy valami mozdult-e a képernyőn, de azt is, hogy melyik elemek mozdultak. Ez a változtatás csak ezt az egy aspektust érintené. A pointcut-nak ki kellene választani a mozgatott elemet is, és az advice kódjában pedig feljegyeznénk azt.

A funkcionalitás egyszerűen ki/be kapcsolható. Csakúgy, mint a fejlesztői aspektusok esetében már láthattuk.

Stabilabb az implementáció. Ha például a programozó létrehoz a Line egy leszármazottját, amiben felülírja az eredeti metódusokat, az aspektusban kifejezett funkcionalitás továbbra is megmarad, míg a hagyományos esetben szükséges a setFlag metódus meghívása.

Állapot átadás

Az állapot átadás modularitást átvágó jellege jelentős forrása lehet a Java programok komplexitásának. Képzelünk el egy olyan funkcionalitás bevezetését, amellyel bármelyik éppen létrejövő alakzat színét is meghatározhatjuk. Ilyenkor tipikusan szükség van rá, hogy a hívótól kapott színt átadjuk minden híváskor, egészen az alakzat gyártó rutinokig. Minden programozó tisztában van a kényelmetlenségekkel, ami az új paraméter bevezetését jelenti ezeknél a metódusoknál, amelyek csupán az állapot átadását szolgálják.

AspectJ-t használva ez a fajta állapot átadás modulárisan megvalósítható. A következő kódrészlet egy hívás utáni advice-t tartalmaz, ami csak akkor fut le, ha a Figure gyártó metódusait a ColorControllingClient metódusainak végrehajtása közben hívták.

aspect ColorControl { pointcut CCClientCflow(ColorControllingClient client): cflow(call(* * (..)) && target(client)); pointcut make(): call(FigureElement Figure.make*(..)); after (ColorControllingClient c) returning (FigureElement fe): make() && CCClientCflow(c) { fe.setColor(c.colorFor(fe)); } }

Ez az aspektus csak néhány metódust érint, de vegyük észre, hogy a nem aspektus orientált megoldás sokkal több metódus érintett volna, az összes metódust, ami a hívótól a gyártóig vezető hívási láncban szerepel. Ez az előny általában is jellemzi a tulajdonság alapú aspektusok használatát, hogy bár az aspektus egyszerű, és csak kevés metódust érint, a kód komplexitását sokkal nagyobb mértékben csökkenti.

Egységes viselkedés kialakítása

Ez a példa bemutatja, hogyan alakíthatunk ki egységes viselkedést az eljárások nagy halmazán, a tulajdonság alapú aspektusok használatával. Ez az aspektus biztosítja, hogy minden publikus metódus a com.xerox package-ben naplóz minden kiváltott hibát (Throwable, ami nem Exception). A publicMethodCall pointcut kiválasztja a package összes publikus metódus hívását, és az advice akkor fut le, ha egy hívás kivétel dobásával tér vissza a hívóhoz. Az advice naplózza a kivételt, majd a kivétel kezelése folytatódhat.

aspect PublicErrorLogging { Log log = new Log(); pointcut publicMethodCall(): call(public * com.xerox.*.*(..)); after() throwing (Error e): publicMethodCall() { log.write(e); } }

Előfordulhat, hogy így egy kivétel kétszer kerül naplózásra. Ez akkor fordul elő, ha a com.xerox package-en belülről is meghívódnak publikus metódusok. A cflow primitív pointcut használatával ezek egyszerűen kiszűrhetők:

after() throwing (Error e): publicMethodCall() && !cflow(publicMethodCall()) { log.write(e); }

A következő aspektus az AspectJ compiler kódjából származik. Az aspektus kb. 35 metódust érint a JavaParser osztályban. Az egyes eljárások kezelik a különböző parszolandó elemeket. (parseMethodDec, parseThrows, parseExpr stb)

aspect ContextFilling { pointcut parse(JavaParser jp): call(* JavaParser.parse*(..)) && target(jp) && !call(Stmt parseVarDec(boolean)); // var decs // are tricky around(JavaParser jp) returns ASTObject: parse(jp) { Token beginToken = jp.peekToken(); ASTObject ret = proceed(jp); if (ret != null) jp.addContext(ret, beginToken); return ret; } }

Szinkronizálás

Párhuzamos környezetben, a feladattól függően gondoskodni kell, hogy az egyes szálak (vagy folyamatok, vagy programok) ellenőrzött módon érjék el a közös adatterületet (például egy számla objektumot, bármely közös erőforrást). Egy feladat leírásában például szerepelhet, hogy egy számlán végrehajtható műveleteknek teljesíteniük kell az ACID-t (Atomicity, Consistency, Isolation, Durability).

Kritikus szakasznak az olyan párhuzamosan végrehajtás alatt álló program szakaszokat értjük, amelyek vizsgálhatják, vagy módosíthatják a közös adatterület elemét. A kritikus szakaszoktól megköveteljük: véges ideig tartson, véges időn belül be lehessen lépni, kritikus szakaszon kívüli hiba (szál leállás) ne befolyásolja a kritikus szakasz „használatát”.

Kölcsönös kizárást kell biztosítani a kritikus szakaszokra, azaz biztosítani kell hogy egyidejűleg csak egy szál (vagy folyamat, vagy program) lehessen a kritikus szakaszban.

Megjegyzem, hogy a különböző adatbázisok is közös adatterületnek számítanak. Itt az adatbázis kezelő rendszernek szolgáltatásait felhasználva az adatbázis tervezőjének kell gondoskodni a kritikus szakaszokról (ACID). Az adatbázis elérését biztosító API rákényszeríti a programozót, hogy helyesen használja az adatbázist.

Java nyelvben a párhozamosság megvalósítására a Thread osztályt használják, amellyel párhuzamosan futtatható szálakat futtathatunk. Két mód van párhuzamosan futtatható programrész definiálására: vagy Thread osztályból származtatnunk, vagy megvalósítjuk a Runnable interfészt. Mindkét esetben a párhuzamosan futtatandó programrészt a run() metódusba kell tenni. Ha a Thread osztályból származtattunk, akkor a származtatott osztály objektumának start() metódusának meghívásával indíthatjuk el a párhuzamos végrehajtást. Ha a Runnable interfészt valósítottuk meg, akkor megvalósító osztály objektumát paraméterként át kell adni a Thread osztály konstruktorának, majd az így létrejött objektum start() metódusával indíthatjuk el a párhuzamos végrehajtást. A programban tetszőleges számú szálat indíthatunk, megadhatjuk prioritásukat, felfüggeszhetjük, újraindíthatjuk, megállíthatjuk őket. Szinkronizálásukra a join kulcsszót használhatjuk. A szálakat szálcsoportokba (ThreadGroup) rendezhetjük. A kommunikáció osztott változókon keresztül történik. Kölcsönös kizárásra a synchronized kulcsszó szolgál, amivel egy blokkot vagy egy metódust védhetünk meg a párhuzamos eléréstől.

A következők kölcsönös kizárást, és író olvasó problémákat megoldó és szemléltető források letölthetőek.

Kölcsönös kizárás aspektusok
Kölcsönös kizárás aspektus
// számla osztály, osztottan használt számla objektumok osztálya public class AccountM { private int amount; public AccountM(int amount) { this.amount = amount; } // szinkronizálandó metódus! public void deposit(int depositAmount, int id) { sleep(); this.amount += depositAmount; sleep(); } // ... // sleep definíciója, hogy egy metódus lefutása hosszabb legyen } // kölcsönös kizárást biztosító aspektus public aspect MutexAspect { // AccountM osztályú objektumokra vonatkozó deposit metóduson történő // objektum szintű szinkronizálást megvalósító advice Object around(AccountM a) : call(* AccountM.deposit(int, int)) && target(a) { synchronized(a){ return proceed(a); } } }
Újrafelhasználható kölcsönös kizárás aspektus
Első verzió
// számla osztály, osztottan használt számla objektumok osztálya public class AccountMR1 { private int amount; public AccountMR1 (int amount) { this.amount = amount; } // szinkronizálandó metódus! public void deposit(int depositAmount, int id) { sleep(); this.amount += depositAmount; sleep(); } // ... // sleep definíciója, hogy egy metódus lefutása hosszabb legyen } // kölcsönös kizárást biztosító absztrakt, általános aspektus public abstract aspect ReusableMutexAspect1 { // leszármazottakban definiálandó pointcut: // hol kell szinkronizálni abstract pointcut callToSync(Object o); // objektum szintű szinkronizálást megvalósító advice Object around(Object o): callToSync(o){ synchronized (o) { return proceed(o); } } } // kölcsönös kizárást biztosító absztrakt aspektus kiterjesztése, // konkrétan az AccountMR1 osztályú objektumok szinkronizálására, // hasonlóan bármely osztályra bevezethető szinkronizálás public aspect MutexAspectForAccountMR1 extends ReusableMutexAspect1 { // az ősben (ReusableMutexAspect1) deklarált absztrakt pointcut definiálása: // hol akarunk szinkronizálni az AccountMR1 osztályban pointcut callToSync(Object o): call(* AccountMR1.deposit(int, int)) && target(o); }
Második verzió
// számla osztály, osztottan használt számla objektumok osztálya public class AccountMR2 { private int amount; public AccountMR2(int amount) { this.amount = amount; } // szinkronizálandó metódus! public void deposit(int depositAmount, int id) { sleep(); this.amount += depositAmount; sleep(); } // ... // sleep definíciója, hogy egy metódus lefutása hosszabb legyen } // kölcsönös kizárást biztosító absztrakt, általános aspektus public abstract aspect ReusableMutexAspect2 { // szinkronizálandó osztálynak meg kell valósítania ezt az interfészt, // hogy az osztály maga döntse el, mivel akar szinkronizálni, // melyik objektum legyen a szemafor a szinkronizálandó részeknél interface Sync { Object getSemaphore(); } // leszármazottakban definiálandó pointcut: // hol kell szinkronizálni abstract pointcut callToSync(Sync s); // objektum által megadott szemafor szintű szinkronizálást megvalósító advice Object around(Sync s): callToSync(s){ synchronized (s.getSemaphore()) { return proceed(s); } } } // kölcsönös kizárást biztosító absztrakt aspektus kiterjesztése, // konkrétan az AccountMR2 osztályú objektumok szinkronizálására, // hasonlóan bármely osztályra bevezethető szinkronizálás public aspect MutexAspect2ForAccountMR2 extends ReusableMutexAspect2 { // aspektus osztály szintű változója private static Object s = new Object(); // ez mondja meg, hogy az AccountMR2 megvalósítja a Sync interfészt declare parents: AccountMR2 implements Sync; // AccountMR2 itt valósítja meg a Sync interfészt, // amelyben leírja, hogy mivel akar szinkronizál, // melyik objektum legyen a szemafor a szinkronizálandó részeknél public Object AccountMR2.getSemaphore() { // Itt most az s-t használtunk szemafornak, melynek az eredménye az, // hogy a szinkronizálandó részre mindig csak egy szál léphet be. // Használhattuk volna a this kulcsszót (most az AccountMR2 osztályon belül vagyunk!) // melynek az eredménye hogy, a szinkronizálás objektum szintű lesz, // azaz az egyes AccountMR2 osztályú objektumok elérhetjük párhuzamosan, // de egy AccountMR2 osztályú objektumra egyidejűleg csak egy szál léphet be // szinkronizálandó részekre return s; } // az ősben (ReusableMutexAspect2) deklarált absztrakt pointcut definiálása: // hol akarunk szinkronizálni az AccountMR2 osztályban pointcut callToSync(Sync a) : call(* AccountMR2.deposit(int, int)) && target(a); }
Író olvasó aspektusok
Író olvasó aspektus
// számla osztály, osztottan használt számla objektumok osztálya public class AccountRW { private int amount; public AccountRW (int amount) { this.amount = amount; } // szinkronizálandó író metódus! public void deposit(int depositAmount, int id) { sleep(); this.amount += depositAmount; sleep(); } // szinkronizálandó olvasó metódus! public int getBalance(int id) { sleep(); int tempAmount = amount; sleep(); return tempAmount; } // ... // sleep definíciója, hogy egy metódus lefutása hosszabb legyen } // az író olvasó problémát megoldó aspektus public aspect ReaderWriterAspect { // AccountRW osztályba bevezetjük a readers változót // 0 kezdeti érékkel: olvasók száma private int AccountRW.readers = 0; // AccountRW osztályba bevezetjük a writing változót // false kezdeti érékkel: írja-e valaki private boolean AccountRW.writing = false; // reader pointcut-tal definiáljuk, hogy melyek az olvasó műveletek pointcut reader(AccountRW a): target(a) && call(int AccountRW.getBalance(int)); // reader pointcut-tal definiáljuk, hogy melyek az író műveletek pointcut writer(AccountRW a): target(a) && ( call(void AccountRW.deposit(int, int)) ); // olvasás előtt végrehajtandó programrész before(AccountRW a): reader(a) { boolean canGo = false; while (!canGo) { synchronized (a) { canGo = !a.writing; if (canGo) { a.readers++; } else { try { a.wait(); } catch (Exception e) {} } } } } // olvasás után végrehajtandó programrész after(AccountRW a): reader(a) { synchronized (a) { a.readers--; try { a.notifyAll(); } catch (Exception e) {} } } // írás előtt végrehajtandó programrész before(AccountRW a): writer(a) { boolean canGo = false; while (!canGo) { synchronized (a) { canGo = a.readers == 0 && !a.writing; if (canGo) { a.writing = true; } else { try { a.wait(); } catch (Exception e) {} } } } } // írás után végrehajtandó programrész after(AccountRW a): writer(a) { synchronized (a) { a.writing = false; try { a.notifyAll(); } catch (Exception e) {} } } }
Újrafelhasználható író olvasó aspektus
// számla osztály, osztottan használt számla objektumok osztálya public class AccountRWR { private int amount; public AccountRWR (int amount) { this.amount = amount; } // szinkronizálandó író metódus! public void deposit(int depositAmount, int id) { sleep(); this.amount += depositAmount; sleep(); } // szinkronizálandó olvasó metódus! public int getBalance(int id) { sleep(); int tempAmount = amount; sleep(); return tempAmount; } // ... // sleep definíciója, hogy egy metódus lefutása hosszabb legyen } //az író olvasó problémát megoldó absztrakt, általános aspektus public abstract aspect ReusableReaderWriterAspect { // szinkronizálandó osztálynak meg kell valósítania ezt az interfészt, hogy // az osztály maga döntse el, mivel akar szinkronizálni, melyik objektum // legyen a szemafor a szinkronizálandó részeknél interface Sync { Object getSemaphore(); } // Sync interfészbe bevezetjük a readers változót // 0 kezdeti érékkel: olvasók száma private int Sync.readers = 0; // Sync interfészbe bevezetjük a writing változót // false kezdeti érékkel: írja-e valaki private boolean Sync.writing = false; // leszármazottakban definiálandó pointcut: hol kell szinkronizálni olvasást abstract pointcut reader(Sync s); // leszármazottakban definiálandó pointcut: hol kell szinkronizálni írást abstract pointcut writer(Sync s); // objektum által megadott szemafor szintű szinkronizálást megvalósító // olvasás előtt végrehajtandó programrész, advice before(Sync s): reader(s) { boolean canGo = false; while (!canGo) { synchronized (s.getSemaphore()) { canGo = !s.writing; if (canGo) { s.readers++; } else { try { s.wait(); } catch (Exception e) {} } } } } // objektum által megadott szemafor szintű szinkronizálást megvalósító // olvasás után végrehajtandó programrész, advice after(Sync s): reader(s) { synchronized (s.getSemaphore()) { s.readers--; try { s.notifyAll(); } catch (Exception e) {} } } // objektum által megadott szemafor szintű szinkronizálást megvalósító // írás előtt végrehajtandó programrész, advice before(Sync s): writer(s) { boolean canGo = false; while (!canGo) { synchronized (s.getSemaphore()) { canGo = s.readers == 0 && !s.writing; if (canGo) { s.writing = true; } else { try { s.wait(); } catch (Exception e) {} } } } } // objektum által megadott szemafor szintű szinkronizálást megvalósító // írás után végrehajtandó programrész, advice after(Sync s): writer(s) { synchronized (s.getSemaphore()) { s.writing = false; try { s.notifyAll(); } catch (Exception e) {} } } } // az író olvasó problémát megoldó absztrakt aspektus kiterjesztése, // konkrétan az AccountRWR osztályú objektumok szinkronizálására, // hasonlóan bármely osztályra bevezethető szinkronizálás public aspect ReaderWriterAspectForAccountRWR extends ReusableReaderWriterAspect { // aspektus osztály szintű változója private static Object s = new Object(); // ez mondja meg, hogy az AccountRWR megvalósítja a Sync interfészt, így az // AccountRWR objektumok mindegyike tartalmaz egy-egy saját reader és // writing változót. declare parents: AccountRWR implements Sync; // AccountRWR itt valósítja meg a Sync interfészt, amelyben leírja, // hogy mivel akar szinkronizál, // melyik objektum legyen a szemafor a szinkronizálandó részeknél public Object AccountRWR.getSemaphore() { // AccountRWR megvalósítja a Sync interfészt, így az AccountRWR // objektumok mindegyike tartalmaz egy-egy saját reader és writing // változót. Itt most az s-t használtunk szemafornak, hogy a // szinkronizálást megvalósító programrészben az ős a szinkronizáláshoz // az s-t használja szemafornak. Ennek az eredménye az, hogy minden // readers, writing vizsgálat és beállítás izoláltan hajtódik végre az // összes AccountRWR objektumra nézve (az író olvasó probléma // megoldásában). Ha csak azt szeretnénk, hogy objektumonként legyenek // izoláltak az readers, writing vizsgálatok és beállítások, akkor // használjuk a this kulcsszót (most az AccountRWR osztályon belül // vagyunk!), ami gyorsabb végrehajtáshoz vezet. return s; } // az ősben (ReusableReaderWriterAspect) deklarált absztrakt pointcut definiálása: // hol akarunk szinkronizálni az AccountRWR osztályban olvasásra pointcut reader(Sync a): target(a) && call(int AccountRWR.getBalance(int)); // az ősben (ReusableReaderWriterAspect) deklarált absztrakt pointcut definiálása: // hol akarunk szinkronizálni az AccountRWR osztályban írásra pointcut writer(Sync a): target(a) && ( call(void AccountRWR.deposit(int, int)) ); }

Statikus aspektusok: Inter-type declaration

Eddig az aspektus orientáltság dinamikus eszközeivel ismerkedtünk meg, melyek a program végrehajtását befolyásolják. AspectJ azonban eszközöket kínál a program statikus szerkezetének módosítására is. Ezt az úgynevezett inter-type declaration segítségével tehetjük meg.

Egy inter-type declaration egy aspektus tagja, de egy másik típus (osztály) tagját módosítja. Egy inter-type declaration

  1. hozzáadhat metódusokat egy már létező osztályhoz
  2. hozzáadhat mezőket egy létező osztályhoz
  3. kiterjeszthet egy osztály egy másikkal
  4. megvalósíthat egy inteface-t egy létező osztályban
  5. kezelt kivételeket kezeletlenekké tehet

Tegyük fel, hogy meg akarjuk változtatni a Point osztályt, hogy lehetséges legyen a klónozása. Inter-type declaration használatával ez könnyen megoldhatjuk. Az osztály maga meg sem változik, csak az azt használó kódrészlet. Az alábbi példában a CloneablePoint aspektus három dolgot tesz:

  1. Deklarálja, hogy a Point osztály megvalósítja a Cloneable inteface-t,
  2. Deklarálja, hogy az Object clone() szignatúrájú metódusok által kiváltott kivételek kezeletlenek,
  3. Tartalmaz egy metódust, ami felülírja a Point osztály clone metódusát, amit az Object osztálytól örökölt

class Point { private int x, y; Point(int x, int y) { this.x = x; this.y = y; } int getX() { return this.x; } int getY() { return this.y; } void setX(int x) { this.x = x; } void setY(int y) { this.y = y; } public static void main(String[] args) { Point p = new Point(3,4); Point q = (Point) p.clone(); } } aspect CloneablePoint { declare parents: Point implements Cloneable; declare soft: CloneNotSupportedException: execution(Object clone()); Object Point.clone() { return super.clone(); } }

Az inter-type declaration egy igen erőteljes eszköz az egységbezárás és modularitás szempontjából, mivel nem csak a program komponenseinek viselkedését befolyásolja, hanem a köztük lévő kapcsolatot is.

Logolás megvalósítása másképp

Ez a példaprogram egy összetett program logolását valósítja meg. Az eredeti program egy http-n keresztül érkező kéréseket szolgál ki, amely kérések könyveket tároló adatbázisra vonatkoznak. Van egy központi szerver, amely elosztja a beérkező kéréseket a tároló szervereknek, amelyek ehhez a központi szerverekhez kapcsolódnak. Azt, hogy melyik tároló szerver fogja a kérést kiszolgálni a központi szerver Managere dönti el, RoundRobin algoritmust használva. Látható, hogy a logolást a központi szerveren kell megvalósítani. A központi szervernek van egy CRescource osztálya (a CRUD műveleteket fogadja, ezért a CRescource elnevezés), ezen osztály függvényeire kell a logolást megvalósítani.
Nézzük belőle egy részletet:

@GET @Path("/{id}") @Produces("application/json") public Book getBook(@PathParam("id") int id) { manager.incSzamlalo(); return manager.getNowRescource().path(Integer.toString(id)) .type(MediaType.APPLICATION_JSON).get(new GenericType(){}); } @GET @Path("/isbn/{isbn}") @Produces("application/json") public Book getBookbyId(@PathParam("isbn") String isbn) { manager.incSzamlalo(); return manager.getNowRescource().path("/isbn/"+ isbn) .type(MediaType.APPLICATION_JSON).get(new GenericType(){}); }

Láthatjuk, hogy egy GET kérést kétféleképpen kaphatunk, lekérhetjük egy könyv adatait id és isbn szám alapján is. Mivel a logoláshoz szükségünk van arra is, hogy melyik adatra történt a lekérés, így ezt a két függvényt nem tudjuk egy pointcut-ba összefogni, mivel a bemenő paraméterek típusai különböznek.

pointcut getid (int id) : execution(* resources.CRescource.getBook(int)) && args(id) ; pointcut getisbn (String isbn) : execution(* resources.CRescource.getBookbyId(String)) && args(isbn);

A Logoláshoz szükségünk van arra az információra is, hogy sikeresen végrehajtottuk-e a kérést, avagy exception-t kaptunk. Ezért két külön advice-ot hozunk létre ennek megfelelően, arra az esetre, hogy normál lefutás történt-e, avagy kivételt kaptunk.
A WriteToFile végzi el a fájlba írást.
Ezek után nézzük meg a programkódot:

after (int id) returning: getid( id) { WriteToFile(" GET BY ID: " + id); } after (int id) throwing: getid( id) { WriteToFile(" GET BY ID: " + id + " NOT EXISTS"); } after(String isbn) returning: getisbn(isbn) { WriteToFile(" GET BY ISBN: " + isbn ); } after(String isbn) throwing: getisbn(isbn) { WriteToFile(" GET BY ISBN: " + isbn + " NOT EXISTS"); }