Az AspectJ programozási nyelv

Aspektus-orientált programozás



A dinamikus csatlakozási pontok modellje (join point model)

Minden aspektus-orientált nyelv tervezésének kritikus pontja a csatlakozási pont modell (join point modell). A csatlakozási pontok teszik lehetővé a kereszthivatkozások helyeinek definiálását. Ezekkel a pontokkal határozzuk meg, hogy programkódban lévő mely sorok vannak elszóródva több modulban, azaz nincsenek fizikailag elszeparálva saját moduljukba.

Ez a fejezet az AspectJ dinamikus csatlakozási pontjait mutatja be, melyek jól definiált pontok a program végrehajtásában. Ezek például metódus vagy konstruktor hívások, kivételkezelő rész lefutása, adattag elérése, megváltoztatása, osztály vagy objektum inicializálása.

Az AspectJ több fajta csatlakozási pontot is definiál, a legegyszerűbb a metódushívási csatlakozási pont. Ez magában foglalja az összes akciót, ami egy metódus hívásakor lejátszódik, az argumentumok kiértékelésétől egészen a normális, ill. abnormális visszatérésig.

Pointcut

A pointcut nyelvi konstrukció, mely kiválogatja az egyes join point-okat, és hozzájuk tartozó értékeket. Az AspectJ nyelvben egy pointcut kijelöl néhány csatlakozási pontot a program végrehajtásában. Például a következő pointcut:

call(void Point.setX(int))

kiválasztja a setX metódus meghívását a Point osztály objektumain. A pointcut-okból kompozíciós eszközökkel újabb pointcut-ok definiálhatók. Ilyen kompozíciós operátorok a vagy:|| az és:&& a negáció:! és a helyes zárójelezés:(,). Például:

call(void Point.setX(int)) || call(void Point.setY(int))

kiválaszt minden setX vagy setY hívást a Point objektumokon. A programozó definiálhat saját pointcut-okat. A szintaxisa a következő:

abstract [modifiers] pointcut Id(Formals); [modifiers] pointcut Id(Formals) : Pointcut;

A modifier lehet public, private vagy semmi. A Java szabályainak megfelelően, ha public, akkor mindenki látja, private esetén csak az osztály, ha semmit sem írunk, akkor pedig a csomag. A Formals paraméter pedig a pointcut formális paramétereit tartalmazza, amiket el tudunk érni, amikor kiváltódik egy pointcut. A pointcut-ok kiválaszthatnak csatlakozási pontokat több különböző osztályból. A következő példa deklarál egy új pointcut-ot:

pointcut move(): call(void FigureElement.setXY(int,int)) || call(void Point.setX(int)) || call(void Point.setY(int)) || call(void Line.setP1(Point)) || call(void Line.setP2(Point));

A deklaráció után a move egy olyan pointcut, ami kiválaszt minden metódushívást, ami egy alkotóelemet mozgat.

Tulajdonság alapú pointcut-ok

Az előző példákban mindenhol a metódus szignatúra explicit megadásával határoztuk meg a pointcut-ot. Ezt név-alapú kereszthivatkozásnak nevezzük. AspectJ lehetőséget ad más tulajdonságok alapján történő kiválasztásra, mint a pontos megnevezés. Ezt tulajdonság alapú kereszthivatkozásnak nevezzük. A legegyszerűbb módja ennek a helyettesítő karakterek használata a metódus szignatúrában, ahol a „*” tetszőleges számú karaktert jelent, akár nullát is, a „..” pedig tetszőleges számú és típusú paramétert. Például:

call(void Figure.make*(..))

kiválaszt minden metódushívást a Figure osztályban ahol a metódus neve "make" szöveggel kezdődik.

call(public * Figure.* (..))

Kiválasztja az összes publikus metódushívást a Figure osztályon. Egy különösen hasznos primitív pointcut a cflow kiválasztja azokat a csatlakozási pontokat, amik egy másik pointcut-on belül lépnek fel. Például:

cflow(move())

kiválasztja az összes csatlakozási pontot a move-ban definiált metódusok hívásától, egészen azok visszatéréséig.

Advice

Az advice egy nyelvi konstrukció, amelyekkel az egyes pointcut-okhoz kódot rendelhetünk, ami lefut, mikor a program végrehajtása az adott pointcut-hoz ér. Tehát a pointcut-okat advice-ok definiálásakor használjuk. Az AspectJ-ben több fajta advice létezik, melyek plusz végrehajtandó kódot definiálnak, melyeknek a csatlakozási pontok elérésekor kell végrehajtódniuk. A Before Advice, akkor hajtódik végre, amikor a végrehajtás eléri a csatlakozási pontot, de mielőtt annak végrehajtása elkezdődne pl.: az argumentumok már kiértékelődtek, de mielőtt a metódustörzs végrehajtódna. Az After Advice az után fut le, miután a végrehajtás a csatlakozási ponton befejeződött pl.: a metódustörzs végrehajtása után, mielőtt a visszatérne hívóhoz a végrehajtás. Ennek két típusa van: a returning a metódus normális lefutása után hajtódik végre, a throwing pedig egy le nem kezelt kivételdobás után. Ha nem adjuk meg a típust, akkor mindkét esetben lefut az advice. Az around advice akkor hajtódik végre, amikor a csatlakozási pontot eléri a végrehajtás, és meghatározhatja, hogy a csatlakozási pont által meghatározott programrész végrehajtódjon-e. Példák:

before() : call( * *.set*(..) ) { System.out.println("about to set"); } after(): move() { System.out.println("A figure element moved."); } after() returning : call(* *.set*(..)) { System.out.println("setting OK"); } after() throwing : call( * *.set*(..) ) { System.out.println("setting failed"); } void around() : call(void Connection.rollback*(..)) { System.out.print("Press Y to really rollback connection: "); try { if (System.in.read() == 'Y') proceed(); } catch (java.io.IOException e) {} }

A pointcut kontextusának használata

A pointcut-okban kiválaszthatók a csatlakozási pontok kontextusának bizonyos elemei is. A kiválasztott értékek felhasználhatók az advice definíciójában is. A következő kódrészletben négy érték kerül kiválasztásra, a meghívott osztály objektuma (target), a metódus két argumentuma (args), illetve a hívó objektum (this), ezeket felhasználva az advice-ban kiírjuk, melyik alakzatelemet hova mozgatták és ki:

pointcut setXY(FigureElement fe, int x, int y, Object o): call(void FigureElement.setXY(int, int)) && target(fe) && args(x, y) && this(o); after(FigureElement fe, int x, int y, Object o): setXY(fe, x, y, o) { System.out.println(fe + " moved to (" + x + ", " + y + "). by " + o); } after() returning (int n) : call(int *.get*(..)) { System.out.println("int getting OK:" + n); } after() throwing (IOException e) : call(int *.get*(..)) { System.err.println("error in int getting:" + e); }

Inter-type declaration

Ez egy nyelvi konstrukció, amely megváltoztatja a program statikus szerkezetét. Bevezethetünk vele egy új adattagot, metódust, vagy megváltoztathatjuk az osztályok közötti kapcsolatokat.

Így az introducton ad módot az AspectJ nyelvben az osztályok, és azok hierarchiájának módosítására. Egy inter-type declaration új tagokat definiálhat, illetve megváltoztathatja az osztályok közötti leszármazási viszonyokat. Az advice-okkal ellentétben, melyek elsősorban dinamikusan fejtik ki hatásukat az inter-type declaration-ök hatása fordításkor statikusan jelentkezik. Az inter-type declaration megváltoztatja az osztályt, majd a megváltozott osztály kerül használatra a program ill. a fordítás többi részében.

Képzeljünk el egy esetet, amikor már meglévő származtatott osztályokat, kell új tulajdonságokkal kiegészíteni. Java-ban ekkor egy új interface-t kell definiálni, amiben meghatározzuk a tulajdonságot, majd minden egyes osztályhoz egyesével hozzáadni az új metódusokat, melyek megvalósítják az interface-t.

Az AspectJ eszközeivel ez jobban kezelhető. Mivel az új tulajdonság több osztályt közösen érint. Az inter-type declaration segítségével, a már meglévő osztályokhoz hozzáadhatjuk a metódusokat, és mezőket, amik az új tulajdonság megvalósításához szükségesek. Ez által gyakorlatilag kiegészíthetünk tetszőleges osztályt a saját függvényeinkkel. Az ilyen függvényeknek a this referenciája nem az aspect -re, hanem az objektumra fog mutatni. A deklarálás ugyanúgy írható le, mintha egy osztály törzsébe lennénk, annyi különbséggel, hogy itt meg kell adni a függvény neve előtt az osztályát is.

Tegyük fel, hogy azt szeretnénk, hogy a Screen objektum nyomon kövesse a Point objektumok változásait. Ezt megvalósíthatjuk, ha bevezetünk egy új mezőt a Point objektumokba az observers mezőt, ahol nyilvántartjuk, kiket kell értesíteni a változásokról. Ehhez két statikus metódussal lehet hozzáadni, illetve kivenni objektumokat. A changes nevű pointcut-ban definiáljuk, mik azok a "változások" amiről értesíteni kell a megfigyelőket. Majd egy advice-ban meghatározzuk, mit teszünk a változások hatására. Megjegyezzük, hogy ehhez sem a Screen sem a Point osztály kódját nem kellett módosítanunk, minden szükséges kód egyetlen aspect-ben szerepel!

aspect PointObserving { private Vector Point.observers = new Vector(); //Point osztályban létrehoztunk egy új adattagot //melyben nyilvántartjuk az értesítendő objektumokat public static void addObserver(Point p, Screen s) { p.observers.add(s); } public static void removeObserver(Point p, Screen s) { p.observers.remove(s); } pointcut changes(Point p): target(p) && call(void Point.set*(int)); after(Point p): changes(p) { Iterator iter = p.observers.iterator(); while ( iter.hasNext() ) { updateObserver(p, (Screen)iter.next()); } } static void updateObserver(Point p, Screen s) { s.display(p); } }

Láthatóság, hatókör

private: csak aspect-en belül érhető el
protected: nincs
csomag szintű: aspect csomagján belül érhető el
public: szokásos

Osztályok közötti kapcsolatot megváltoztatása

Implements kapcsolat megváltoztatása:

declare parents: Point implements Comparable; public int Point.compareTo(Object p){…}

Extends kapcsolat megváltoztatása:

declare parents: Point extends Labelled;

Fordítóprogram ellenőrzésének kiterjesztése

Ezekkel a konstrukciókkal definiálhatunk fordítóprogram által adott új figyelmeztetést (warning) vagy hibát (error). Szerződés kikényszerítésére használhatjuk.

aspect RegistrationProtection { pointcut register() : call(void Registry.register(Figure)); pointcut canRegister() : withincode(* Figure.make*(..)); declare error: register() && !canRegister() : "Illegal call"; }

Ezzel kikényszerítjük, hogy egy Figure regisztrálását csak a Figura osztály make metódusai végezhetik el.

Join point-ok és használatuk részletezése

A join point-ok típusai:

call(MethodPattern) // a megadott mintájú metódus hívásakor váltódik ki. call(ConstructorPattern) // a megadott mintájú konstruktor hívásakor váltódik ki. execution(MethodPattern) // megadott mintájú metódus végrehajtásakor váltódik ki. execution(ConstructorPattern) // a megadott mintájú konstruktor végrehajtásakor váltódik ki. set(FieldPattern) // a megadott mintájú mező értékének megváltoztatásakor váltódik ki. get(FieldPattern) // a megadott mintájú mező értékének kiolvasásakor váltódik ki. initialization(ConstructorPattern) // a megadott mintájú konstruktor futása közben az objektuminicializálás pillanata. preinitialization(ConstructorPattern) // a megadott mintájú konstruktor futása közben a super hívás előtti pillanat. staticinitialization(TypePattern) // a megadott mintájú osztály betöltésekor váltódik ki. handler(TypePattern) // a megadott mintájú kivételosztály dobott objektumának lekezelésekor (catch) váltódik ki. Csak before advice-ban használható. adviceexecution() // advice futásakor váltódik ki.

Különbség van a call és az execution között: a call-t a hívó szemszögéből az execution-t a hívott szemszögéből nézzük. Azaz a this kontextus más (lásd később). További csatlakozási pontok.

Változó olvasás, írás

Egy példányváltozó „olvasásának” (get) vagy „írásának” (set) végrehajtásakor fellépő pointcut. Akkor hasznos, ha a példányváltozó megváltozását akarjuk figyelni. Ezt a nélkül tehetjük meg, hogy az összes őt megváltoztató összes metódust metódushívási (call) joinpoint-ba tennénk:

aspect PointBoundsChecking { before(int x): set(Point.x) && args(x) { if ( x < MIN_X || x > MAX_X ) throw new IllegalArgumentException("x is out of bounds."); }

Ez a példa a Point osztály x változójának megváltoztatásakor ellenőrzi az x helyességét.

Konstruktorok

A konstruktor végrehajtásának teljes folyamata:

  1. konstruktor hívás
  2. this hívás (0 db, 1 db, 2 db…)
  3. super hívás
  4. super lefutása
  5. visszatérés super hívásból
  6. esetleges inicializálók lefutása
  7. esetleges this-ek lefutása és visszatérése (2. alapján)
  8. konstruktor lefutása
  9. konstruktor visszatérése

A program végrehajtásának pontjai alapján:

call(ConstructorPattern) // 1 preinitialization(ConstructorPattern) // 2 initialization(ConstructorPattern) // 6, 7, 8 execution(MethodPattern) // 6, 8

A super-re vonatkozó specialitások:

  1. A super meghívása nem konstuktor hívás, hanem csak konstruktor végrehajtás.
  2. A super-ből történő metódus használat nem metódushívás, hanem csak metódus végrehajtás.

A this-re vonatkozó specialitások:

  1. A this meghívása nem konstruktor hívás, hanem csak konstruktor végrehajtás.

Polimorfizmus

A join point-ok a vizsgált típussal kapcsolatban polimorfikusak. Például az alábbi osztályok és aspect-t definiálása után:

class A { void method(){} } class B extends c { void method (){} } aspect P { after() : call(void B. method ()) { … } }

az alábbi hívás után az after advice lefut:

(new B()).method ()

Statikus környezet

Használatuk mintája:

withincode(MethodPattern) withincode(ConstructorPattern) within(TypePattern)

Statikus környezetet vizsgál. A forráskód alapján dönt el, hogy mely join point-ok kiváltódása tartozik hozzá: azok, amelyeket explicit a forráskódba írtunk.

aspect RegistrationProtection { pointcut register() : call(void Registry.register(Figure)); pointcut canRegister() : withincode(* Figure.make*(..)); before(): register() && !canRegister() { throw new IllegalStateException(…); } }

A fenti aspect ellenőrzi, hogy egy Figure regisztrálását csak egy Figure osztálybeli make metódusa végezheti el. Ez egy szerződés kikényszerítés.

Dinamikus környezet

Használatuk mintája:

cflow(Pointcut) cflowbelow(Pointcut) if(boolean-expr)

Dinamikus környezetet vizsgál. A cflow-val és cflowbelow-val a végrehajtás futását, az if-el futásidejű értékeket vizsgálhatunk.

cflow( call(* Figure.make*(..)) ) && call( void *.set*(..) )

A fenti példa kiválasztja a Figure osztály make prefixű metódusainak lefutásakor az összes setter metódushívást. Egy példa cflowbelow használatára:

before() : move() && (! cflowbelow(move())) { System.out.println("moving something"); }

Az alábbi példában a whiteSteps pointcut csak egy fehér Figure mozgatásakor váltódik ki.

aspect ChessTracer { pointcut whiteSteps( Figure f ): call(void Figure.moveTo(int,int)) && target(f) && if(f.isWhite()); }

Környezet elérés

Használatuk mintája:

this(Type or Id) target(Type or Id) args(Type or Id or “..” or “*”)

A this a jelenlegi aktuális objektumot választja ki, a target pedig a célobjektumot. Az args-al kiválaszthatjuk a metódus paramétereit, get/set join point mezőjét vagy a handler join point kivételét.

args(int,*), args(int,..,String)

Reflektív információk

Ezek az objektumok információt szolgáltatnak az aktuális join point szignatúrájáról, a program végrehajtásának nyomon követésekor hasznosak.

aspect SimpleTracing { pointcut traced(): call(* *.set*(..)); before(): traced() { System.out.println("Entering: " + thisJoinPoint); } }

Ez az aspect kiírja az összes set prefixű metódushívás előtt, annak teljes szignatúráját.

Minták

A mintákban helyettesítő jeleket használhatunk az egyszerűbb használat céljából.

Mező minta (FieldPattern). Típus.mezőnév. A „*” használható. A mező értékét args-al érhetjük el.

Point.x, Point.*, Point.*Id

Típus minta (TypePattern). Teljes vagy részletes osztálynevet is használhatunk, valamint az alábbiakat:

  1. Logikai operátorok „&&”, „||”, „!”, „( )” az összetett típusfeltételhez.
  2. Tömb típus a „[]” suffixxel.
  3. Altípus a „+” suffixxel.
  4. Helyettesítő karakterek a „..” és a „*”.

java.util.* java.lang.*Error java.*.List java..List java..* java..Map.* FigureElement && !(Point || Line) pointcut callToUndefinedMethod() : call(* Point+.*(..)) && !call(* Point.*(..)); // Az utolsó példa a Point osztály valódi alosztályainak metódushívásait írja le.

Metódus és konstruktor minta (MethodPattern, ConstructorPattern). A teljes szignatúrát fel lehet használni a minta leírására. Ezek a módosítók, paraméterek, deklarált kivételek, teljes vagy részletes osztálynév, metódusnév. Használhatóak még a logikai operátorok és a helyettesítő karakterek is.

call( public final void C.foo() throws java.io.IOException ) call( !public !final * *.*(..) throws java.io.IOException ) call( * create*(..,int) throws (Exception && !SecurityException) )

Aspect

Az aspect egy nyelvi konstrukció. Egy aspect egy keresztülvágó vonatkozás megvalósítását zárja egy-egységbe. Ezzel érhetjük el, hogy a keresztülvágó vonatkozások egy modulba, egy aspect-be kerüljenek. Ezt úgy tesszük, hogy az adott keresztülvágó vonatkozáshoz tartozó pointcut-okat, advice-okat, inter-type declaration-okat egy aspect-be helyezzük el, hasonlóan, mint az osztály adatait és metódusait. Az aspect tartalmazhat saját metódusokat, adattagokat és inicializáló blokkokat, sőt öröklődés lehet közöttük. Szintaxisa a következő:

[ privileged ] [ Modifier ] aspect Id [ extends Type ] [ implements TypeList ] [ PerClause ] { Body }

Issingletion aspect

Alapértelmezés szerint az aspect-ek singleton-ok azaz egy példány létezik belőlük a program futása alatt.

aspect A issingleton() { ... }

Privileged aspect

A privileged aspect a mások által nem látható metódusokat, adattagokat is elérik (private).

Extended aspect

Az aspect-ek az osztályhoz hasonlóan kiterjeszthetők. Egy aspektus kiterjeszthet osztályt, interfészt vagy aspektust. De egy osztály nem terjeszthet ki aspektust!

aspect B extends A{ ... }

Abstract aspect

Lehet abstract aspect-et definiálni, melynek szerepe hasonló, mint az absztrakt osztályé. Ilyen aspect-ben megadhatunk például abstract pointcut-ot, melyet a kiterjesztésnél definiálunk.

abstract aspect Auth{ abstract pointcut service(User u); before(User u): service(u){ if (!u.valid()) { throw new IllegalArgumentException("Not valid user"); } } }

PerClause

A PerClause záradék lehet percflow( PointCut ), ami azt jelenti, hogy minden egyes advice futásakor egy új példány fog létrejönni az aspect-ből.

Precedence

Használat:

declare precedence : TypePatternList;

Ezzel a konstrukcióval meg lehet adni az aspect-ek feldolgozásának sorrendjét. Ez fontos, ha egy pointcut kiváltódásakor több aspect is végrehajt advice-ot

declare precedence: *..*Security*, Logging+, *;

Megkötés hogy csak konkrét aspect-eket lehet a listába felsorolni.