Java - von am Thursday, January 10, 2008 11:54 - 0 Kommentare

Fünf Wege der Fehlersuche in Java

Bitte beachten: dieser Artikel wurde mit freundlicher Genehmigung von Zviki Cohen aus seinem Blog übernommen und ins Deutsche übersetzt.

Oft steht man vor dem Problem, sich in fremden Sourcecode einarbeiten zu müssen. Meistens ist dieser auch noch spärlich dokumentiert und nicht selten hat man nur einen Teil davon vorliegen und muss den Rest dekompilieren. Diesem anstrengenden Prozess ist jeder ausgesetzt, der fremdem Code über den Weg läuft, sei es durch Wartungsarbeiten oder durch Benutzung projektfremder Frameworks und Libraries von Drittanbietern. Stellen Sie sich eine Situation vor, in der Sie das Interface eines Event-Handlers implementieren müssen und unsicher sind, welches Event wann ausgelöst wird und welche Argumente Sie von diesem erhalten.

Meiner Erfahrung nach ist es einfacher, den Code zur Laufzeit nachzuverfolgen, insbesondere wenn man vor nicht-trivialen Problemstellungen steht. Beispiel: Von einem Interface existieren mehrere Implementierungen und wir wollen wissen, welches zur Laufzeit verwendet wird.

In diesem Artikel möchte ich die (mir bekannten) Wege aufzeigen, Java-Code zur Laufzeit nachzuverfolgen. Die Tipps und Beispiele setzen voraus, daß Sie wissen, wie Eclipse verwendet wird, aber die meisten davon können auch in anderen IDEs nachvollzogen werden. Ich werde manche der genannten Techniken in späteren Artikeln aufgreifen und detaillierter darstellen.

1. Das Basisverfahren: Breakpoints und Schritt-für-Schritt-Ausführung

Beginnen wir mit der einfachsten Möglichkeit: setzen Sie Breakpoints und verfolgen Sie jeden einzelnen Schritt der Anwendung nach.

Vorteile:

  • Sehr einfach aufzusetzen und in der Verwendung. Alles was Sie benötigen ist ein Debugger.
  • Ermöglicht den lokalen Zugriff auf ALLE Informationen, inkl. Methodenattributen, lokalen Variablen etc..
  • Modifiziert nicht den Code.
  • Selektiv: Sie können selbst wählen, wo die Breakpoints stehen.

Nachteile:

  • Schritt-für-Schritt durch den Code zu gehen dauert sehr lang. Desweiteren erfordert dieses Vorgehen eine hohe Konzentration. Für jeden Schritt müssen Sie selbst entscheiden, ob der Debugger in, über oder aus dem jeweiligen Schritt springen soll. Das macht dieses Vorgehen unpraktikabel.
  • Wenn Sie durch Event-Handler debuggen müssen Sie Breakpoints an jeder Methode setzen, da Sie vielleicht nicht wissen, welche Methode wann aufgerufen wird.
  • Sie haben keine Chance, Breakpoints innerhalb einer Methode zu setzen, wenn Sie nicht den Sourcecode vorliegen haben.

Dieses Verfahren lohnt sich wenn…
… Sie eine schnelle und einfache Möglichkeit suchen, Sie den gesamten Sourcecode vorliegen haben, wissen, wo Sie die Anwendung unterbrechen müssen oder ausführliche Anwendungsinformationen (Argumente, lokale Variablen etc.) zu einem bestimmten Zeitpunkt während der Laufzeit der Anwendung benötigen.

Mein Tipp: Auch wenn Sie nicht alle Sourcen haben können Sie die Anwendung unterbrechen wenn eine Methode oder Klasse aufgerufen wird, indem Sie Breakpoints an der Methode, bzw. beim Laden der Klasse, setzen.

  • Classloader Breakpoints können leicht gesetzt werden: einfach aus Eclipse’s “Run” Menü “Add Class Load Breakpoint…” wählen
  • Um Breakpoints in Methoden zu setzen müssen Sie die Klasse zuerst dekompilieren, z.B. mit dem JadClipse plug-in. Nach der Dekompilierung wählen Sie die Methode und machen einen Rechtsklick auf die graue Spalte links vom Editor, wo Sie dann per “Toggle Breakpoint” den Breakpoint ein- und ausschalten können. Bereiten Sie sich darauf vor, daß Sie Breakpoints in dekompilierten Klassen setzen, diese aber nicht funktionieren. Das liegt daran, daß Decompiler den Code nicht immer korrekt herstellen können.

2. Die Grundlagen: Debugnachrichten

Wir fahren fort mit der Ausgabe von Debugnachrichten. Der einfachste Weg ist es, System.out.println statements zu verwenden, um Nachrichten auf die Standardausgabe zu schreiben. Fortgeschrittener ist es, einen Loggingmechanismus zu verwenden, wie beispielsweise das Logging im JDK, dem Apache Commons Logging oder den Klassiker Log4j.

Vorteile:

  • Kann sehr selektiv eingesetzt werden.
  • Falls nicht zuviele Debugausgaben vorgenommen werden ist die Ausführung sehr schnell.
  • Sie können exakt die Informationen ausgeben an denen Sie interessiert sind.

Nachteile:

  • Erfordert Modifikationen am Code. Das ist inpraktikabel wenn Sie die Sourcen nicht besitzen.
  • Das Setup kostet Zeit und wenn Sie nicht wissen wonach Sie suchen müssen, werden Sie zuviele Debugausgaben vornehmen.
  • Macht den Code unleserlich und führt zu unnötigem Overhead, wenn Sie die Debugausgaben nach dem Debugging nicht wieder entfernen.

Dieses Verfahren lohnt sich wenn…
… Sie den Sourcecode vorliegen haben und eine Ahnung davon haben, wonach Sie suchen müssen. Dieses Verfahren eignet sich für das Debugging von Event-Handlern und seine hohe Ausführungsgeschwindigkeit macht sich dann auch während der Nachverfolgung komplexer Abläufe positiv bemerkbar.

Mein Tipp: Erstellen Sie eine generische Methode die die momentan ausgeführte Methode ausgibt. Der beste, mir bekannte, Weg ist, den Stacktrace auszugeben. Der letzte Eintrag auf dem Aufrufstack ist die generische Methode, die die Debugausgabe vornimmt. Der Eintrag davor ist die aufrufende Methode, also die Methode die den Entwickler interessiert. Den Stacktrace bekommen Sie über:

  • Seit dem JDK 5.0: Benutzen Sie Thread.currentThread().getStackTrace()
  • Davor (JDK 1.4): Erzeugen Sie eine neue Instanz einer Exception und holen Sie sich den Aufrufstack über dessen getStackTrace()-Methode. Die Exception muss hier nicht “geworfen” werden.

3. Der Aufsteiger: Dynamic Proxy

Eine Verbesserung gegenüber einfachen Debugausgaben ist der Dynamic Proxy. Dieses spezielle Feature der Java Reflection “sitzt vor” der betroffenen Klasse und fängt alle Aufrufe mittels eines Interfaces ab. Wenn Sie einen Aufruf erst einmal abgefangen haben, können Sie Debugausgaben vornehmen und alle gewünschten Informationen ausgeben.

Vorteile:

  • Der Code muss nicht mit Debugausgaben “verseucht” werden. Alle Debugausgaben können an einer zentralen Stelle vorgenommen werden, unabhängig vom eigentlichen Code.
  • Können einfach entfernt werden sobald das Debugging abgeschlossen werden. Hat damit auch keine Auswirkungen auf die Performance der Anwendung.

Nachteile:

  • Funktioniert nur durch Interfaces und ist damit nutzlos für Methoden, die nicht als “public” oder nicht im Interface definiert sind.
  • Der bestehende Sourcecode muss modifiziert werden, was Zugriff auf diesen voraussetzt.
  • Nicht-selektive Nachrichten werden ausgegeben.

Dieses Verfahren lohnt sich wenn…
… Sie eine sehr gute Lösung für Event-Handler suchen. Sie können einen Dummy-Event mit einem generischen Proxy innerhalb von Sekunden aufsetzen und die Abfolge von Events nachverfolgen. Das ist die einfachste und schnellste Methode, wenn es um die Nachverfolgung von Events geht.

Mein Tipp: Auf Sun’s Website finden Sie Code für einen generischen Dynamic Proxy. Das Beispiel ist ganz gut, allerdings implementiert es nur das unmittelbar implementierte Interface der durch den Proxy gewrappten Klasse. Es implementiert keine seiner weiteren implementierten Interfaces. Das kann leicht behoben werden, indem die Liste der implementierten Interfaces traversiert und alle Interfaces aufgenommen werden. In einem späteren Artikel werde ich eine bessere Version beschreiben.

4. Die rohe Gewalt: Run-time Profiler

Profiler sind mächtige Tools, die alle Aufrufe innerhalb des Systems durch spezielle “Hooks” nachverfolgen. Mit dieser Methode schiessen Sie allerdings mit Kanonen auf Spatzen.

Vorteile:

  • Keine Modifikationen am Code notwendig.
  • Der gesamte Ablauf der Anwendung kann nachverfolgt werden, auch wenn der Sourcecode nicht vorhanden ist.
  • Die meisten Profilingtools ermöglichen es, weniger interessante Daten auszufiltern, beispielsweise Aufrufe der Systembibliotheken, Getter- und Setter-Methoden etc..

Nachteile:

  • Die meisten guten Profiler sind teuer.
  • Die meisten Profiler bringen einen hohen Einarbeitungsaufwand mit sich. Auch das meistern der Tools ist nicht immer trivial.
  • Profiling reduziert die Ausführungsgeschwindigkeit ganz erheblich. Es ist nicht praktikabel, eine Anwendung länger laufen zu lassen während ein Profiler Informationen sammelt.
  • Die meisten Profiler zielen darauf ab, Performancemessungen durchzuführen und vernachlässigen die Anzeige des Ausführungspfads, sprich: welche Methoden wann aufgerufen wurden.
  • Die meisten Profiler zeigen Methodenaufrufe an, allerdings nicht die dazugehörigen Argumente.

Dieses Verfahren lohnt sich wenn…
… Sie ein Gesamtbild für eine sehr spezifische Operation benötigen, Sie also einen sehr kurzen Anwendungsfall untersuchen möchten.

Mein Tipp: Gute Profiler kosten um die US$ 500,00 pro Entwicklerlizenz. Die kostenlosen Profiler bieten hingegen nur rudimentäre Funktionen an. Meiner Meinung nach ist das Beste Open-Source-Tool für diesen Job die Eclipse Testing and Performance Tools Platform (TPTP). Ich habe sehr gute Erfahrungen mit diesem Tool gemacht und kann es nur empfehlen. Die TPTP stellt beispielsweise den Ablauf des Trace anhand eines Sequenzdiagramms dar, was das Verständnis von Ausführungspfaden erheblich vereinfacht. TPTP gehörte zu meinen Favoriten bis ich auf den Mac gewechselt bin und mit Inkompatibilitäten zu kämpfen hatte (vielleicht behebe ich diese einmal, wenn ich die Zeit dafür finde).

5. Das neue Zeitalter: Aspekte

Die Aspektorientierte Programmierung (AOP) ist ein nicht-triviales Verfahren. Ohne in die Details von Aspekten zu gehen schauen wir uns an, was unter dem Strich bleibt: es ist eine schnelle und einfache Methode, den Ablauf des Codes “abzufangen”. Sie können selektiv “Hooks” um Methoden, Konstruktoren, Zugriff auf Felder etc. herum setzen, ohne den Originalcode zu modifizieren. In diesen “Hooks” können Sie dann Debugausgaben vornehmen.

Vorteile:

  • Schnell und einfach aufzusetzen.
  • Selektiv einsetzbar: Sie können nur die Klassen oder Methoden tracen die Sie benötigen.
  • Stellt alle Informationen bereit, inklusive der Argumente.
  • Keine Modifikation des Originalcodes notwendig. Leicht entfernbar, sobald das Debugging abgeschlossen ist.
  • Gute Performance. Kann auch während einer längeren Ausführung eingesetzt werden.

Nachteile:

  • Setzt grundlegende Kenntnisse von AOP voraus, insbesondere wenn komplexere Filter aufgesetzt werden sollen.
  • Setzt voraus, daß der Code rekompiliert werden muss. Das kann ein Problem darstellen, wenn Code innerhalb eines JARs nachverfolgt werden soll.

Dieses Verfahren lohnt sich wenn…
… Sie den Ablauf einer Anwendung nachverfolgen möchten, dessen Code Sie rekompilieren können.

Mein Tipp: Benutzen Sie es. AOP ist immer noch weit davon entfernt, ein Standardverfahren zu sein und es ist unsicher, wann es zu einem wird. Ohne seine Qualitäten und Features zu diskutieren kann man getrost davon ausgehen, daß die meisten Entwickler innerhalb von einer Stunde mit einem AOP-basierten Logging loslegen können – die exakten Schritte dafür werde ich in einem späteren Artikel darstellen. Denen die Eclipse verwenden empfehle ich die Aspect Java Development Tools (AJDT).

Schlussfolgerung

Welche Methode sollten Sie verwenden? Ich unterscheide hier zwei verschiedene Fälle: die komplette Ausführung einer kurzen Codesequenz nachverfolgen und das nachverfolgen von Events zu/von einer definierten Klasse oder Anwendungsschicht.

Nachverfolgen der kompletten Ausführung:

  1. Ein Profiler stellt den einfachsten Weg dar. Meiner Meinung nach sollte jeder Entwickler in der Lage sein, einen Profiler zu verwenden und einen in seinem Arsenal bereitliegen haben.
  2. Die nächste Wahl ist AOP. Das ist nur die zweite Wahl, weil das Aufsetzen eines Profilers einfacher ist wenn es um JARs geht. Wie auch immer, AOP gibt uns mehr Kontrolle, beispielsweise in der Ausgabe von Methodenargumenten.
  3. Nehmen Sie Debugausgaben vor.

Nachverfolgen von Events:

  1. Benutzen Sie AOP. Fangen Sie selektiv nur die für Sie interessanten Aufrufe ab.
  2. Benutzen Sie einen Dynamic Proxy. Das ähnelt dem Verwenden von Debugausgaben, aber auf eine elegantere Art und Weise. AOP mag anfangs komplex erscheinen, das aber nur zu anfangs. Auf lange Sicht würde ich AOP verwenden.
  3. Nehmen Sie Debugausgaben vor.
  4. Benutzen Sie Breakpoints.

Wie Sie sehen bin ich, in diesem Kontext, kein großer Fan von Breakpoints. Breakpoints eignen sich für das Debugging von Code, aber nicht, um die Ausführung nachzuverfolgen. Dafür gibt es weitaus effizientere Wege.

Update: einige Leser haben mich darauf hingewiesen, daß AspectJ auch für JARs verwendet werden kann. Ich werde dem in einem späteren Artikel nachgehen.

Weiterführende Literatur

Laut dem bekannten Dr. Dobb’s Journal findet sich die beste Beschreibung der Dynamic Proxies in Java Reflection in Action.

Eine sehr gute Einführung in das Standardframework für die Aspektorientierte Programmierung findet sich in Aspektorientierte Programmierung mit AspectJ 5. Einsteigen in AspectJ und AOP..

Be Sociable, Share!


Kommentare

Kommentieren

Weitere Empfehlungen: