Aspektusok belső reprezentációja az AspectJ-ben

Paller Gábor

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:
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.
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:
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: