1. Aspektusok statikus szövése
Az aspektusorientált programozás
életképességének előfeltétele, hogy
ez a technika ne jelentsen jelentős
sebességcsökkenést vagy
memóriaigény-növekedést. Ezen feltétel
teljesülése attól függ, az aspektusok
szövésének mekkora részét tudjuk
megoldani fordítási időben és a hozzáadott
kód mennyiben növeli meg a
kódméretet/futási időt. A következőkben
néhány egyszerű példán keresztül
bemutatom, hogyan működik az AspectJ 1.1.1-es változata.
Lássuk a következő egyszerű programot és aspektust!
Hello3.java:
import
java.util.Vector;
public class
Hello3 {
public
void printObject( Object obj ) {
System.out.println( "printObject: "+obj.toString() );
}
public
static void main( String args[] ) {
Hello3 h = new Hello3();
h.printObject( new Object() );
h.printObject( new Vector() );
}
}
CS1.java:
public aspect CS1 {
pointcut p1() : execution( void printObject( Object ) );
before(): p1() {
System.out.println( "Before" );
}
after(): p1() {
System.out.println( "After" );
}
}
Az aspektus tanácsai a printObject minden
végrehajtása előtt és után
végrehajtódnak. Észrevehetjük, hogy ez az
aspektus könnyen implementálható lenne
kézzel, ha a printObject törzsébe
beszúrnánk a tanácsokat. Ez azért van, mert
a pontszűrő feltétel nélkül illeszkedik a program
bizonyos pontjaira. Ha az AspectJ ilyen helyeket talál, a
tanácsok feltétel nélküli
hívását fordítja. Íme az AspectJ
által generált osztályokból visszafejtett
programok (megjegyzés: az AspectJ sokszor generál olyan
programot, amit nem lehet Jáva nyelvre visszafejteni, mert a
beszúrt részek nem struktúráltak. Ebben a
szövegben az AspectJ kimenetével egyenértékű
Jáva kódot mutatok be)
Hello3.java:
import
java.util.Vector;
public class
Hello3
{
public void printObject(Object obj)
{
try
{
CS1.aspectOf().ajc$before$CS1$52();
System.out.println("printObject: " + obj.toString());
}
catch(Throwable throwable)
{
CS1.aspectOf().ajc$after$CS1$8e();
throw throwable;
}
CS1.aspectOf().ajc$after$CS1$8e();
}
public static void main(String args[])
{
Hello3 h = new Hello3();
h.printObject(new Object());
h.printObject(new Vector());
}
}
CS1.java:
import
org.aspectj.lang.NoAspectBoundException;
public class
CS1
{
public static final CS1 ajc$perSingletonInstance;
static {
ajc$postClinit();
}
private static void ajc$postClinit() {
ajc$perSingletonInstance = new CS1();
}
public void ajc$before$CS1$52() {
System.out.println("Before");
}
public void ajc$after$CS1$8e() {
System.out.println("After");
}
public static CS1 aspectOf() throws NoAspectBoundException {
if( ajc$perSingletonInstance == null )
throw new NoAspectBundException();
return ajc$perSingletonInstance;
}
public static boolean hasAspect() {
return ajc$perSingletonInstance != null;
}
}
Megfigyelhetjük, hogy az AspectJ helyesen ismerte fel, hogy
a pontszűrő feltétel nélkül illeszkedik bizonyos
programpontokra és oda a tanácsok feltétel
nélküli hívását fordította. A
pontszűrő kifejezés teljesen eltűnt, a lefordított
programban nincs nyoma, csak a fordító használta
fel a Magukat a tanácsokat az aspect-ből generált
Jáva osztályba tette néhány
adminisztráló metódussal együtt. Az AspectJ a
tanácsokat mindig az aspektusból képzett
osztályba kerülnek, habár a Jáva
virtuális gép JIT fordítója gyakran inline
metódusokká fordítja őket. Az
adminisztráló metódusok leginkább azzal
foglalkoznak, hogy garantálják, hogy az
aspektusosztályból pontosan egy egyed keletkezzen,
tehát az aspektus singleton legyen.
Tekintve, hogy az aspektusosztály programozó
számára látható adatot nem tartalmaz,
általában nem okoz gondot, hogy singleton.
Speciális esetekre (pl. synchronized blokkokat tartalmazó
tanácsok) meg lehet mondani, milyen instanciálási
stratégiát kövessen az AspectJ. Az aspektus
deklarálásakor a következő lehetőségek
közül lehet választani:
- issingleton - az alapértelmezett viselkedés
- perthis( pontszűrő ) -
A pontszűrő aktivizálódásakor aktuálisan
futó objektumhoz ("this") saját aspektusegyed tartozik
- pertarget( pontszűrő )
- A pontszűrő aktivizálódásakor target-tel
kiválasztott objektumhoz saját aspektusegyed tartozik
- percflow( pontszűrő )
vagy percflowbelow( pontszűrő )
- Minden cflow feltételhez saját aspektusegyed tartozik.
Előző aspektusunkat kissé módosítottuk, hogy a
perthis() hatását láthassuk.
CS1_perthis.java:
public aspect
CS1_perthis perthis( p1() ) {
pointcut p1() : execution( void printObject( Object ) );
before(): p1() {
System.out.println( "Before" );
}
after(): p1() {
System.out.println( "After" );
}
}
Ha ezt alkalmazzuk a Hello3.java-ra, a futási eredmény
nem változik, de a lefordított kód igen.
Hello3.java:
import
java.util.Vector;
public class
Hello3 implements CS1_perthis.ajcMightHaveAspect
{
public void printObject(Object obj)
{
CS1_perthis.ajc$perObjectBind(this);
try
{
if(CS1_perthis.hasAspect(this))
CS1_perthis.aspectOf(this).ajc$before$CS1_perthis$6a();
System.out.println("printObject: " + obj.toString());
}
catch(Throwable throwable)
{
if(CS1_perthis.hasAspect(this))
CS1_perthis.aspectOf(this).ajc$after$CS1_perthis$a6();
throw throwable;
}
if(CS1_perthis.hasAspect(this))
CS1_perthis.aspectOf(this).ajc$after$CS1_perthis$a6();
}
public static void main(String args[])
{
Hello3 h = new Hello3();
h.printObject(new Object());
h.printObject(new Vector());
}
public CS1_perthis ajc$CS1_perthis$perObjectGet() {
return ajc$CS1_perthis$perObjectField;
}
public void ajc$CS1_perthis$perObjectSet(CS1_perthis cs1_perthis) {
ajc$CS1_perthis$perObjectField = cs1_perthis;
}
private transient CS1_perthis ajc$CS1_perthis$perObjectField;
}
Az aspektusosztály kódját most nem mutatom
meg, az eléggé bonyolult módon karbantart egy
leképezést a perObjectBind-dal kötött
objektumok és aspektusegyedek között. Tanulság,
hogy tudnunk kell az aspektusosztályok singleton
jellegéről és arról is, hogy ennek
megváltoztatása lehetséges, de
költséges.
2. Reziduális feltételek
Előző példánkban a pontszűrő kifejezés teljesen
"elpárolgott", mert a fordítás során
egyértelműen azonosíthatóak voltak a
programpontok, amikre a szűrő vonatkozott. A helyzet nem mindig ilyen
szerencsés. Lássunk egy újabb aspektust.
CS2.java:
import
java.util.Vector;
public aspect
CS2 {
pointcut p1( Vector obj ) : execution( void printObject( Object ) )
&& args( obj );
before( Vector obj ): p1( obj ) {
System.out.println( "Before with Vector" );
}
after(
Vector obj ): p1( obj ) {
System.out.println( "After with Vector" );
}
}
Ez az aspektus csak akkor aktivizálódik, ha a
printObject-et Vector típusú paraméterrel
hívtuk meg. Minthogy a fordítás során nem
tudható, mikor kap a metódus ilyen paraméter, a
pontszűrő egy része nem tűnhet el a kódból. Az
AspectJ a program elemzése során megtalálja azokat
a helyeket, ahol a pontszűrő végrehajtódhat, ezeket a
pontszűrő árnyékának nevezi. A pontszűrő
feltételei közül azokat, amelyek
teljesülését fordítási időben nem
tudja ellenőrizni, reziduális feltételnek hívjuk.
A reziduális feltételnek megfelelő if()
utasítást a kódba szúrják és
ennek teljesülése esetén hajtják végre
a tanácsokat.
Hello3.java:
import
java.util.Vector;
public class
Hello3 {
public void printObject(Object obj) {
Object obj1 = obj;
try
{
if(obj1 instanceof Vector)
CS2.aspectOf().ajc$before$CS2$87((Vector)obj1);
System.out.println("printObject: " + obj.toString());
}
catch(Throwable throwable)
{
if(obj1 instanceof Vector)
CS2.aspectOf().ajc$after$CS2$e0((Vector)obj1);
throw throwable;
}
if(obj1 instanceof Vector)
CS2.aspectOf().ajc$after$CS2$e0((Vector)obj1);
}
public static void main(String args[])
{
Hello3 h = new Hello3();
h.printObject(new Object());
h.printObject(new Vector());
}
}
Mint látható, a pontszűrő execution()
részére nincs szükség, ennek
teljesülését fordítási időben kezelni
tudta a fordító. A paraméter Vector
típusának ellenőrzése azonban reziduális
feltétel maradt, így az ennek megfelelő if() a
kódba került.
3. Dinamikus pontszűrők, cflow()
A cflow() érdekes problémát okoz, ugyanis ennek
kiértékelése függ attól, milyen
úton jutott a vezérlés a programponthoz,
így ennek feloldását a fordító
semmilyen módon nem tudja elvégezni. A naiv
megoldás ellenőrző hívásokat iktatna be minden
helyre, ahol a cflow()-val együtt szereplő feltétel
teljesülne és ellenőrizné a hívási
vermet. Ez rendkívül időigényes feladat lenne. Ha
azonban észrevesszük, hogy a cflow() belsejében levő
pontszűrő által kiszúrt programpontokra
elhelyezhetünk egy számlálót, ami
számlálja, hányszor haladt ott át a
vezérlés, majd a programpont elhagyása után
ezt a számlálót csökkenthetjük, a
probléma egyszerűen a számláló
értékének az ellenőrzésére
redukálódik. A cflow()
lefordításához tehát két dolgot kell
tenni.
- A cflow-hoz tartozó számlálót kezelő
kódot kell tenni arra a helyre, ahol a cflow() belsejében
levő pontszűrőnek árnyéka van
- A pontszűrő maradék (nem cflow) részeinek
árnyékára a reziduális kifejezésbe
be kell venni a számláló
ellenőrzését.
Az AspectJ majdnem így csinálja, csak egy kicsit
bonyolít a dolgon. Számláló helyett
Object[] egyedekből álló vermet épít.
Valahányszor a vezérlés áthalad a cflow
belsejéhez tartozó pontszűrőn, egy új Object[]
egyedet tesz a cflow-hoz tartozó veremre. Amikor a
reziduális kifejezést ellenőrzi, akkor megnézi,
üres-e a verem. Ha van benne valami, a cflow() teljesült. Az
Object[] egyedekre azért van szükség mert esetleg a
cflow()-ban levő pontszűrő valamilyen paramétert akar a
tanácsba juttatni (pl. args() ) és ezeket
paramétereket tárolni kell az esetleges after()
tanács részére.
Lássunk most egy programot és egy cflow-ot
tartalmazó aspektust:
Hello4.java:
public class
Hello4 {
public
void printMessage( String s ) {
System.out.println( s );
}
public
void say1() {
printMessage( "say1" );
say2();
}
public
void say2() {
printMessage( "say2" );
}
public
static void main( String args[] ) {
Hello4 h = new Hello4();
h.say1();
h.say2();
}
}
CS4.java:
public aspect CS4 {
pointcut p1() : call( void printMessage( String ) ) && cflow(
call( void say1() ) );
before(): p1() {
System.out.println( "Before" );
}
after(): p1() {
System.out.println( "After" );
}
}
Emlékeztetőül: a példánkban a cflow azt
mondja, hogy a feltétel teljesüléséhez a
call( void say1() ) pontszűrő hívási
láncában kell lennie az utasításnak, hogy
rá a feltétel cflow része teljesüljön.
Ez azt jelenti, hogy ha a say2() metódushoz a say1()-en
keresztül jutunk el, akkor a say2() benne van a
hívási láncban, de ha direktben hívjuk meg
a say1() kikerülésével, akkor nem. Ennek megfelelően
a nyomtatási kép a következő:
Before
say1
After
Before
say2
After
say2
Lássuk, milyen kódot fordít az AspectJ a
Hello4.java-ból a CS4 aspektussal (az
aspektushívásokhoz tartozó
kivételkezeléseket nem tűntettem fel):
public class
Hello4 {
public
void printMessage( String s ) {
System.out.println( s );
}
public
void say1() {
if( CS4.ajc$cflowStack$0.isValid() )
CS4.aspectOf().ajc$before$CS4$6e();
printMessage( "say1" );
if( ajc$cflowStack$1.isValid() )
CS4.aspectOf().ajc$after$CS4$aa();
say2();
}
public
void say2() {
if( CS4.ajc$cflowStack$0.isValid() )
CS4.aspectOf().ajc$before$CS4$6e();
printMessage( "say2" );
if( ajc$cflowStack$1.isValid() )
CS4.aspectOf().ajc$after$CS4$aa();
}
public
static void main( String args[] ) {
Hello4 h = new Hello4();
CS4.ajc$cflowStack$1.push( new Object[0] );
CS4.ajc$cflowStack$0.push( new Object[0] );
h.say1();
CS4.ajc$cflowStack$0.pop();
CS4.ajc$cflowStack$1.pop();
h.say2();
}
}
Az ajc$cflowStack$1 és ajc$cflowStack$0
változókkal hivatkozott objektumok képezik a
vermeket, amibe a cflow() állapotát mentjük. Az
AspectJ nem volt elég okos hozzá, hogy
rájöjjön, a before() és az after() pontszűrője
ugyanaz, ezért generált két vermet.
Tanulság, hogy a cflow() jelentős többlet-futásidőt
okozhat.
4. Osztályok módosítása
Az AspectJ könnyedén kezeli osztályok
módosítását, hiszen ez teljesen
fordításidőben zajló feladat. Lássunk egy
kis demonstrációt:
CS5.java:
public aspect CS5 {
public
int Hello3.variable = 0;
public
void Hello3.someCode() {
System.out.println( "someCode" );
}
}
Ezt a Hello3.java
ellen alkalmazva a következő adódik:
import java.util.Vector;
public class Hello3
{
public Hello3() {
CS5.ajc$interFieldInit$CS5$Hello3$variable(this);
}
public void printObject(Object obj) {
System.out.println("printObject: " + obj.toString());
}
public static void main(String args[])
{
Hello3 h = new Hello3();
h.printObject(new Object());
h.printObject(new Vector());
}
public void someCode(){
CS5.ajc$interMethod$CS5$Hello3$someCode(this);
}
public int variable;
}
És az
aspektusosztály:
CS5.java:
import org.aspectj.lang.NoAspectBoundException;
public class CS5 {
public static void
ajc$interFieldInit$CS5$Hello3$variable(Hello3 ajc$this_) {
ajc$this_.variable = 0;
}
public static int
ajc$interFieldGetDispatch$CS5$Hello3$variable(Hello3 arg0) {
return arg0.variable;
}
public static void
ajc$interFieldSetDispatch$CS5$Hello3$variable(Hello3 arg0, int arg1) {
arg0.variable = arg1;
}
public static void
ajc$interMethod$CS5$Hello3$someCode(Hello3 ajc$this_) {
System.out.println("someCode");
}
public static void
ajc$interMethodDispatch1$CS5$Hello3$someCode(Hello3 arg0 ) {
arg0.someCode();
}
public static CS5 aspectOf() {
if( ajc$perSingletonInstance == null )
throw new NoAspectBundException();
return;
}
public static boolean hasAspect() {
return
ajc$perSingletonInstance != null;
}
private static void ajc$postClinit() {
ajc$perSingletonInstance =
new CS5();
}
public static final CS5 ajc$perSingletonInstance;
static
{
ajc$postClinit();
}
}
Az AspectJ a beiktatott metódust ill. a
változó inicializálását
továbbra is az aspektusosztály kódjában
végzi el. Ez eléggé bonyolult kapcsolatot hoz
létre a Hello3 és az aspektusosztály
között, de a mérések szerint a
sebességcsökkenés csupán néhány
százalék. Ez valószínűleg a fejlett JIT
fordítóknak köszönhető.
5. Konklúziók
Az AspectJ meglehetősen hatékony abban, hogy az aspektusokkal
kapcsolatos munkát fordítási időben
elvégezze. Csodák azonban nincsenek és
futásidőben is marad elvégezni való. A
mérések alapján az AspectJ által okozott
sebességcsökkenés általában 20% alatt
marad. Vannak azonban aspektusok, amelyek jelentős, akár
20-30-szoros sebességcsökkenést is okozhatnak. Ezek
alapvetően két csoportba oszthatók:
- Pontszűrők, amelyek a kelleténél több helyen
alkalmazódnak
- cflow() bizonyos esetei
Az AspectJ fordítón kétségkívül
sok javítanivaló van, de az esetek
többségében nem okoz
sebességcsökkenést az aspektusok használata.
Ugyanúgy ahogy az OOP programozásnál, az AspectJ
programozásnál is vannak csapdák, amibe
könnyű beleesni.
Ajánlott irodalom:
- H. Masuhara, G. Kiczales and C. Dutchyn: Compilation Semantics of
Aspect-Oriented Programs, Foundations
of Aspect-Oriented Languages (FOAL) 2002, Enschede, The Netherlands,
April, 2002
- B. Dufour, C. Goard, L. Hendren, C. Verbrugge, O. de Moor and
Ganesh Sittampalan: Measuring the Dynamic Behaviour of AspectJ
Programs, Sable Technical Report,
No. 2004-2