Saturday, 11. July 2015
SW-Archäologie mit AspectJ (3)
2. Abstecher in die Welt von AOP

3. Unterstützung durch AOP

Nach diesem Ausflug in die wunderbare Welt der Aspekte, die uns einen kleinen Einblick in die Denkweise ermöglichte, wenden wir uns wieder unserem Fundstück aus der Steinzeit des Java-Zeitalters zu. Der größte Manko bei Altanwendungen (und nicht nur dort) sind die Testfälle. Meistens fehlen sie oder sind genauso veraltet wie die Dokumentation. Ist die Anwendung noch in Betrieb, kann man versuchen, daraus Testfälle für die weitere Entwicklung abzuleiten. Und genau hier kann AOP helfen, fehlende Log-Informationen zu ergänzen oder die Kommunikation mit der Umgebung aufzuzeichnen.

3.1. Schnittstellen beobachten

Die interessanten Stellen sind vor allem die Schnittstellen zur Außenwelt. Bei J2EE-Applikationen sind dies meist andere Systeme wie Legacy-Anwendungen oder Datenbanken, die über das Netzwerk angebunden werden. Hier reicht es oft aus, die Anwendung ohne Netzwerkverbindung zu starten und zu beobachten, wo überall Exceptions auftreten:

java.net.SocketException: java.net.ConnectException: Connection refused
    at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:156)
    at com.mysql.jdbc.MysqlIO.(MysqlIO.java:283)
    at com.mysql.jdbc.Connection.createNewIO(Connection.java:2541)
    at com.mysql.jdbc.Connection.(Connection.java:1474)
    at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:264)
    at java.sql.DriverManager.getConnection(DriverManager.java:525)
    at java.sql.DriverManager.getConnection(DriverManager.java:193)
    at bank.Archiv.init(Archiv.java:25)
    …
Aus dieser Exception kann man erkennen, dass die Anwendung auf eine MySQL-Datenbank zugreift, aber kein Connection-Objekt vom DriverManager bekam. Mit diesem Wissen kann man sich alle Connection-Aufrufe genauer unter die Lupe nehmen:

after() returning(Object ret) : 
         call(* java.sql.Connection.*(..)) {
    log.debug(getAsString(thisJoinPoint) + " = " + ret);
}
Mit dieser Anweisung wird am Ende jedes Connection-Aufrufs der Aufruf selbst mitsamt seinen Parametern (z.B. SELECT-Statements) und Rückgabewert ausgegeben (über die getAsString()-Methode, hier nicht abgebildet). Ähnlich kann man bei Netz- bzw. Socket-Verbindungen verfahren: man erweitert die Stellen (im AOP-Jargon „Joinpoints“ genannt), über die Daten ausgetauscht werden.

Handelt es sich um eine interne Bibliothek oder Framework, das es zu betrachten gilt, sind vor allem die öffentlichen Schnittstellen von Interesse. Hier kann man mit Aspektorientierten Sprachmitteln all die Methodenaufrufe ermitteln, die tatsächlich von außerhalb aufgerufen werden – denn oft sind nicht alle Methoden, die als „public'“ deklariert sind, für den Aufruf von außen vorgesehen.

public pointcut executePublic() :
    (execution(public * bank..*.*(..))
        || execution(public bank..*.new(..)))
    && !within(EnvironmentAspect);

public pointcut executeFramework() :
    execution(* bank..*.*(..)) || execution(bank..*.new(..));

public pointcut calledFromOutside() :
    executePublic() && !cflowbelow(executeFramework());

before() : calledFromOutside() {
    Signature sig = thisJoinPoint.getSignature();
    String caller = 
        getCaller(Thread.currentThread().getStackTrace(), sig);
    log.info(caller + " calls " + sig);
}
Hier werden alle Methoden eines Bank-Frameworks (bank-Package) überwacht. Nur wenn der Aufruf nicht von innerhalb kam (!cflowbelow), wird vor der Ausführung der Methode oder Konstruktor der Aufrufer anhand des Stacktraces ermittelt (Methode getCaller(), hier nicht aufgelistet).

...
jsp.index_jsp._jspService(index_jsp.java:54) calls bank.Konto(int)
jsp.index_jsp._jspService(index_jsp.java:58) calls void bank.Konto.einzahlen(double)
jsp.index_jsp._jspService(index_jsp.java:60) calls double bank.Konto.abfragen()
...
Dies ist die Ausgabe, die den Aufruf aus einer JSP zeigt. Lässt man die Anwendung lang genug laufen, erhält man so alle Methoden, die von außerhalb aufgerufen werden.
3.1.1 Daten aufnehmen
Mit Java ist es relativ einfach möglich, Objekte abzuspeichern und wieder einzulesen. Damit lässt sich ein einfacher Objekt-Recorder bauen, um damit die Schnittstellen-Daten aufzunehmen:

after() returning(Object ret) : sqlCall() {
    objLogger.log(thisJoinPoint);
    objLogger.log(ret);
}
Hinter thisJoinPoint verbirgt sich der Kontext der Aufrufstelle, die der AspectJ-Compiler (eine AO-Sprache, die auf Java aufbaut, s.a. [1]) als Objekt bereitstellt.
3.1.2 Aufnahmedaten einspielen
Wenn man die notwendigen Schnittstellen-Daten gesammelt hat, kann man anhand dieser Daten einen (einfachen) Simulator bauen. Man kennt die Punkte, über die diese Daten hereingekommen sind, man muss jetzt lediglich an diesen Punkte die aufgezeichneten Daten wieder einspielen:

Object around() : sqlCall() {
    Object logged = logAnalyzer.getReturnValue(thisJoinPoint);
    return logged;
}
Hat man so die Anwendung von der Aussenwelt isoliert und durch Daten aus früheren Läufen simuliert, kann man das dynamische Verhalten der Anwendung genauer untersuchen. So kann man weitere Aspekte hinzufügen, die automatisch Sequenz-Diagramme erzeugen oder wichtige Programmzweige visualisieren (z.B. mit Hilfe der UMLGraph-Bibliothek). Damit erhält man neben der statischen Sicht durch Klassen-Diagrammen (die sich notfalls mittels Reverse-Engineering wiedergewinnen lassen) auch eine Visualisierung des dynamischen Verhaltens.

3.2. Code-Änderungen

Nach diesen Vorbereitungen kann mit den ersten Code-Änderungen (Refactorings) begonnen werden. Soll der Original-Code (noch) unverändert bleiben (was bei unbekannter Abdeckungen von vorhandenen Testfällen sinnvoll sein kann), liefert die Aspektorientierung die Möglichkeit, den Code getrennt vom Original abzulegen. Man kann sogar verschiedene Varianten einer Komponente vorhalten und diese während der Laufzeit austauschen. Auf diese Weise kann man experimentell verschiedene Varianten und Code-Manipulationen ausprobieren, um deren Verhalten auf die Gesamt-Anwendung studieren zu können.

4. Ausblick

... comment