Diese und die nächsten beiden Coffee-Shop-Ausgaben widmen sich dem Thema Qualitätssicherung von Java-Software. Klassische Debugger eröffnen den Reigen. Sie sind unter Java nicht zwingend notwendig, aber doch oft nützlich.
Ein hartnäckiger Bug verbirgt sich in einem Programm und behindert die Arbeit oder macht sie gar unmöglich: ein Fall für einen Debugger. War die Nutzung des Debuggers in den ersten Java-Versionen noch ein mühsames Geschäft, so gibt es jetzt gute und vor allem frei verfügbare Lösungen.
Der Nutzen von Debuggern ist jedoch umstritten. Linus Torvalds beispielsweise weigert sich beharrlich, einen Kernel-Debugger in den Mainline-Kernel-Tree aufzunehmen. Sein Argument, dass man dem Code ansehen muss, ob er richtig ist, und daher ein Debugger nur zu schlechtem Programmieren verleitet, hat durchaus etwas für sich. Allerdings lässt sich die Welt nur selten in Schwarzweiß malen. Manchmal hilft ein Debugger schon dabei, das berühmte Brett vor dem Kopf zu erkennen.
Speziell unter Java spricht jedoch auf den ersten Blick noch ein anderer Effekt gegen den Einsatz eines Debuggers: Java-Methoden sollten grundsätzlich kurz und übersichtlich sein. Das Eigentliche, was dann im Programm passiert, ist oftmals verteilt auf unterschiedliche Klassen, die über Mechanismen wie beispielsweise Vererbung oder Aggregation miteinander verknüpft sind. Selbst wenn jede Methode für sich Bug-frei ist, kann die Logik des Zusammenspiels irgendwo trotzdem einen Haken haben. In solchen Fällen ist ein Debugger nur von beschränktem Nutzen.
Weitere Argumente gegen den Einsatz eines Debuggers liefern die schönen Stack-Traces, die von nicht abgefangenen Exceptions (sehr oft »NullPointer«-Exceptions) ausgelöst werden. Solch ein Trace führt meist direkt zur fehlerhaften Programmzeile. Und als letztes Argument sei die Sprache selbst angeführt: Java ist nicht so mächtig wie C++, dafür aber auch weniger fehleranfällig – ohne Pointer-Arithmetik keine Fehler mit den Pointern.
Bereits als Fazit gleich am Anfang dieses Artikels steht also die – sicherlich persönlich gefärbte – Meinung, dass ein Debugger zwar in die Software-Kategorie “Nice to have” fällt, aber erfahrungsgemäß für einen erfolgreichen Software-Entwicklungsprozess unter Java keineswegs zwingend erforderlich ist.
Heimspiele
Wer mit einer Java-IDE entwickelt, für den ist das Debugger-Problem sowieso schon gelöst. Alle verbreiteten IDEs wie Borlands JBuilder, IBMs Visual Age oder SUNs Forte bringen einen Debugger mit. Der Einsatz dieser Werkzeuge soll hier kein Thema sein, obwohl es Entwickler gibt, die nur zum Zweck des Debuggens den Code in solche IDEs laden. Dieser Artikel beschäftigt sich mit Stand-alone-Debuggern, die zusammen mit dem eigenen Lieblingseditor verwendet werden können. Dazu wird zuerst die Architektur erläutert. Anschließend werfen wir einen Blick auf die Referenzimplementation, auf JSWAT und auf JDE.
Debugging unter Java
Java wäre nicht Java, wenn es nicht einheitliche Interfaces für alles, also auch fürs Debuggen gäbe. Es ist eine der großen Stärken der Sprache, durch Vorgabe von Schnittstellen es einerseits den Herstellern zu überlassen, wie sie die Funktionalitäten implementieren, andererseits aber die Interoperabilität zu sichern. Die Java Plattform Debugging Architecture, kurz JPDA genannt (siehe[1]), besteht aus drei Komponenten:
- dem Java Virtual Machine Debug Interface (JVMDI),
- dem Java Debug Wire Protocol (JDWP)
- und dem Java Debug Interface (JDI).
Das JVMDI beschreibt die Services, die eine virtuelle Maschine (VM) bieten muss. Dazu zählen neben der Abfrage des Stack-Frames Aktionen wie das Setzen von Breakpoints oder die Notifikation über Events, etwa wenn ein Breakpoint erreicht wird. Das JVMDI ist durch ein so genanntes Back-End implementiert. Das ist nativer Code, da hier Java das zu debuggende Programm beeinflussen könnte.
Das Back-End kommuniziert mit dem User-Interface, auch Front-End genannt, über das JDWP. Dabei ist nicht der Transportmechanismus selbst festgelegt, sondern nur die Bedeutung der ausgetauschten Informationen. Typischerweise werden aber Sockets verwendet, was auch das Debuggen einer VM auf einer Server-Maschine erlaubt. Ein VM-Hersteller muss also nicht zwingend das JVMDI implementieren, sondern sollte nur sicherstellen, dass das Back-End das JDWP beherrscht. Das JDWP ist paketorientiert und verbindungslos. Analog zum Dialog zwischen Browser und Webserver werden also Kommando- und Antwort-Pakete ausgetauscht.
Auf der Seite des Front-Ends standardisiert das letzte der drei Interfaces, das JDI, den Zugriff auf die Informationen des Transportprotokolls. Dieses Interface ist streng genommen auch nicht notwendig, da ein direkter Zugriff auf das Transportprotokoll möglich wäre, selbst aus einer anderen Programmiersprache heraus. Dieser Fall ist in Abbildung 1 auf der rechten Seite dargestellt.
Vorbereitungen
Bevor es in die Detailbetrachtungen der einzelnen Debugger geht, gibt’s erst eine Reihe von Tipps, die für alle Tools gelten. Wer noch eine alte Version (vor JDK 1.2.2) auf seinem Rechner hat, der sollte spätestens jetzt ein Update durchführen. Es gab zwar bereits einen Debugger für die Kommandozeile, der folgte aber nicht der oben beschriebenen Architektur. Neuere Versionen bieten zwei virtuelle Maschinen als Optionen: die Classic-Variante und die so genannte Hot Spot Engine. Letztere trat mit dem Versprechen an, durch einen verbesserten Just-in-time-Compiler das Laufzeitverhalten zu verbessern
Unabhängig davon, ob das Geschwindigkeitsversprechen der Hot-Spot-VM eingehalten wurde: Für das Debuggen ist sie nicht geeignet. Sowohl Debugger als auch Debuggee sollten aus diesem Grund immer mit der Option »java -classic « gestartet werden.
Ein weiterer wichtiger Punkt betrifft den Compiler. Aufgrund seiner hohen Geschwindigkeit drängt sich im normalen Entwicklungsbetrieb der von IBM entwickelte und in C++ implementierte Compiler Jikes geradezu auf. Für Debug-Sessions ist es allerdings besser, den langsamen »javac« aus dem JDK – mit der Option »-g« – zu verwenden.
JDB: Für Puristen
Im JDK ist der JDB enthalten, ein ausdrücklich Beispiel-Implementation genannter Debugger. Es handelt sich um eine Kommandozeilenanwendung, die allerdings eher etwas für masochistische Spiele als für die praktische Arbeit ist. Unter Linux habe ich das Tool weder in der Blackdown, noch unter der Sun-Implementation zum Laufen gebracht: Es stürzt entweder sofort mit einer »NullPointer«-Exception ab (beim Versuch an den Debuggee anzudocken) oder es hängt sich auf.
Unabhängig davon macht das Fehlen einer Readline-Unterstützung das Tool praktisch unbrauchbar, da somit kein Tippfehler korrigiert werden kann oder schon abgesetzte Befehle einfach zu wiederholen wären. Zusätzlich wird noch der Versuch eines Debuggers mit grafischer Oberfläche mitgeliefert (explizit als Demo bezeichnet). Aber auch das ist nicht mehr als eine Prinzip-Demonstration des High-Level-JDI.
JSwat: State of the Art
Zum Glück braucht sich niemand lange mit solch einer unzulänglichen Implementation aufzuhalten. Unter der GPL gibt es das Programm JSwat, das schon im August 1999 in einer ersten Alphaversion verfügbar war und inzwischen bei der Ausgabe 1.4.5 angekommen ist. Ein Besuch der Homepage unter[2] lohnt sich aber, da das Produkt trotz des jetzt schon großen Funktionsumfangs ständig weiterentwickelt wird. Die aktuelle Version wird beim Erscheinung dieses Artikels also wahrscheinlich schon weiter fortgeschritten sein.
Die Installation ist auch ein Kinderspiel. Das Archiv wird einfach irgendwohin entpackt, das war’s. Die gesamte Funktionalität ist in einer etwa 530 KByte großen Jar-Datei enthalten. Dazu kommt ein Verzeichnis mit einer ausreichenden Dokumentation, einschließlich Tutorial und FAQ. Im Web gibt es ein weiteres Tutorial[3].
Beim Start des Debuggers liegt ein kleiner Stolperstein im Weg. Die Versuchung ist groß, das Programm direkt aus der Jar-Datei zu starten. Allerdings benötigt JSwat unbedingt die JDPA-Dateien aus »tools.jar« im Classpath, doch Java-Programme, die direkt aus dem Jar-Archiv gestartet werden, ignorieren jeden extern gesetzten Classpath. Deshalb ist es am sinnvollsten, den Debugger mit dem Befehl aus Listing 1 zu starten.
Listing 1: Starten von JSwat
1: CLASSPATH=/usr/lib/java/lib/tools.jar:/usr/local/jswat/ jswat-20011027.jar 2: java -classic com.bluemarsh.jswat.Main &
Starten der Anwendung
Der dort aufgeführte Befehl startet einen leeren Debugger (auf einen Screenshot verzichte ich, man stelle sich einfach die Abbildung 4 mit leeren Fenstern vor). Der Debuggee lässt sich jetzt entweder über das »Debug«-Menü starten. (Abbildung 2); alternativ kann eine bereits laufende Java-Anwendung übernommen werden (Abbildung 3). Dafür muss diese wie in Listing 2 aufgeführt gestartet werden. Gerade wer grafische Anwendungen debugged, fährt eventuell mit einem Debugging über das Netz besser. JSwat ist nicht gerade ein Leichtgewicht, um zwei grafische Java-Anwendungen zu verkraften, braucht es sehr viel Hauptspeicher.
Wer sich für die erste Alternative entscheidet, sollte vor dem Start der Anwendung aus JSwat heraus den Classpath richtig setzen. Das kann über das Menü geschehen (»Options -> Set Classpath«) oder vor dem Start, indem neben »tools.jar« und »jswat.jar« auch der für die Anwendung relevante Classpath gesetzt wird.
Eine weitere wichtige Einstellung ist der Pfad zu den Quelldateien, er wird auch über das »Options«-Menü gesetzt. Zum Glück speichert JSwat alle Einstellungen im Verzeichnis »~/.jswat«, so dass dies nur einmal geschehen muss.
Listing 2: Starten einer Anwendung fürs Debuggging
1: CLASSPATH=... java -classic -Xdebug -Xnoagent -Djava.compiler=NONE 2: -Xrunjdwp:transport=dt_socket,address=7000, server=y,suspend=n 3: de.bablokb.websync.main.WebSync &
Das Hauptfenster
Wer schon mit Debuggern gearbeitet hat, wird sich sofort wohl fühlen (und von einem 30-Zoll-Monitor träumen). Das Hauptfenster ist standardmäßig viergeteilt. Oben rechts der Sourcecode, links davon über einzelne Tabs erhältliche Informationen über Threads, Klassen und so weiter, unten links unter anderem der Programm-Output sowie die Breakpoints und unten rechts der Stack-Frame und eine Liste aller definierten Methoden.
Diese sowie die Klassensicht erlauben eine einfache Navigation im Quellcode. Einfach auf die entsprechende Klasse doppelklicken und anschließend die gewünschte Methode selektieren. Breakpoints werden direkt im Quellcode (über die rechte Maustaste) oder über einen entsprechenden Dialog erzeugt. Auch die Eigenschaften können den Bedürfnissen leicht angepasst werden (siehe Abbildung 5).
Die Abbildung 6 zeigt JSwat in Aktion mit der Sicht auf eine Reihe weiterer Fenster. Die aktuelle Zeile ist im Quellcode-Fenster sichtbar, darunter der aktuelle Stack-Frame. Debugger-typische Aktionen (Step-in, Step-over, Step-out) sind über die Icons in der Buttonleiste oben und über Funktionstasten verfügbar. Im Single-Step-Modus fällt es zwar nicht so auf, aber will man zum Beispiel bis zum nächsten Breakpoint weiterlaufen, dann ist Geduld angesagt. Ein Java-Programm unter Debugger-Kontrolle läuft sehr viel langsamer. Insbesondere der Aufbau von Dialogen mit vielen Controls ist recht mühsam.
JSwat ist aber nicht nur ein leistungsfähiger Debugger, sondern auch eine Demonstration des GUI-Toolkits Swing. Verschiebbare Panes, Tab-Panes, Tree-Views, Listboxen, kurz: Fast alles, was Swing zu bieten hat, wird auch sinnvoll eingesetzt. Trotzdem (man könnte sagen in Selbstverleugnung) bietet JSwat auch einen Konsolenmodus, allerdings ohne Sicht auf den Quellcode. Darauf werden aber nur wenige zurückgreifen, eventuell auch nur wegen der oben bereits erwähnten Speicherprobleme. Die entsprechenden Befehle können aber auch gewissermaßen als Shortcut im GUI eingegeben werden, wie in Abbildung 4 zu sehen ist: Ganz unten links verbirgt sich eine Kommandozeile.

Abbildung 6: JSwat in Aktion. Typische Aufgaben sind über die Buttonleiste oder Funktionstasten verfügbar.
Für Emacs-Freaks: JDE
Wer nur in Java entwickelt, wird bereits mit einer reinen Java-IDE zufrieden sein. Wer aber verschiedenste Programmier- und Skriptsprachen verwendet, wird sich nicht jeweils in einen neuen Editor einarbeiten wollen. Dann schlägt die Stunde der eierlegenden Wollmilchsau der Editoren: Je nach Zweck verwendet Emacs dazu einen anderen so genannten Modus. Der JDE-Modus macht aus dem Emacs eine Java-IDE.
Der Download der nötigen Dateien (einschließlich eines guten Users-Guide) erfolgt von der JDE-Homepage[4]. Die Installationsanweisung ist allerdings nicht Teil des Download-Pakets, sondern nur online verfügbar.
JDE ist mehr als ein Debugger-Front-End. Es ist eine komplette IDE, mit allen Funktionalitäten, die nötig sind, um dem Programmierer das Leben leichter zu machen. Die Abbildung 7 zeigt die Möglichkeiten des JDE-Menüs. Der Quellcode im Fenster wurde automatisch über entsprechende Templates erzeugt. Neue Benutzer seien vorgewarnt. JDE ist kein guter Einstieg in Emacs. Das liegt daran, dass JDE eine Reihe von Lisp-Packages benötigt, die normalerweise nicht Teil der Emacs-Distribution sind. Deren Download und Einbindung sind zwar kein prinzipielles Problem, doch selbst mit einiger Erfahrung ist der Vorgang nicht völlig trivial.
Das JDE-Paket habe ich ausführlich in meinem allerersten Coffee-Shop vor nun schon drei Jahren beschrieben[5], deshalb hier nur eine Anmerkung zur Debug-Architektur. Die damals beschriebene JDE-Version nutzte den einst aktuellen »jdb« als Kommandozeilen-Schnittstelle, JDPA gab es noch nicht. Inzwischen sieht die Architektur anders aus. Emacs verwendet nicht direkt das Transportprotokoll, sondern kommuniziert über Standard-Streams mit einem Java-Programm (JDEbug), das im Hintergrund werkelt und über das JDI auf das Back-End zugreift. Diese Architektur steht also gewissermaßen zwischen den beiden in Abbildung 1 dargestellten Möglichkeiten.
Klein, aber fein
Ein weiteres Paket für die Fehlersuche möchte ich noch kurz erwähnen, das zwar genau genommen kein Debugger ist, aber die JDPA verwendet und aus meiner Sicht typisch ist für die Unix-Philosophie: kleine Programme für klar abgegrenzte Aufgaben als Bausteine für größere Aufgaben. Auf der Suche nach einem kleinen, doch leistungsstarken Editor mit Emacs-Tastenbelegung für eine Notfalldiskette bin ich auf E3 gestoßen (dieser Editor bietet Emacs-, vi- und Wordstar-Belegungen in ganzen 8 KByte, statisch!). Der Autor, Albrecht Kleine, hat auch den bekannten Just-in-time-Compiler Tya geschrieben und nun noch das Tool JLouiss, das die JDPA verwendet, um einen Trace aller aufgerufenen Methoden auszugeben. Alle diese Programme sind von[6] downloadbar und stehen unter der GPL.
In der Klasse »java.lang.Runtime« gibt es zwar die Methode »traceMethodCalls()«, doch diese darf von der VM ignoriert werden (“The virtual machine may ignore this request if it does not support this feature”). Unter Linux habe ich bisher noch keine VM gefunden, die dieses Feature unterstützt. JLouiss umgeht das Problem.
Kompilation und Installation sind kein Problem. Im Makefile sollte der richtige Pfad zur Java-Installation überprüft werden, danach genügt ein »make all install«, um JLouiss zu installieren. Konfiguriert wird es über eine Konfigurationsdatei, wie sie in Listing 3 zu sehen ist. Im Wesentlichen muss man über reguläre Ausdrücke selektieren, welche Klassen geloggt werden und wie viele Informationen dazu. Wer hier zu großzügig ist, füllt seine Platte in kürzester Zeit mit unnötigen Informationen auf. Die einzelnen Parameter sind ausführlich in einer Beispiel-Konfigurationsdatei erklärt, aber schon das abgedruckte Beispiel zeigt, dass die Namen der Parameter selbsterklärend sind.
Der Output dazu ist auszugsweise in Listing 4 zu sehen. Wozu soll das alles gut sein? Wie am Anfang erwähnt, sind Java-Methoden of sehr kurz und die eigentlichen Fehler entstehen durch das fehlerhafte Zusammenspiel. Gerade wenn so etwas wie Polymorphismus ins Spiel kommt, sind sie schwer aufzuspüren. Ein Trace, in welcher Reihenfolge und mit welchen Parametern die Methoden aufgerufen werden, ist dann sehr hilfreich.
Listing 3: Konfigurationsdatei für JLouiss
22: logfile=websync.log 37: 38: classes=de.bablokb.websync.* 39: #not_classes=java/lang/.* 40: #methods=main 41: #not_methods=toString 42: #exceptions=.* 43: #not_exceptions=Demo 44: 59: showEntryCounter=1 60: showThreadId=1 61: showModifiers=1 62: showSignature=1 63: showResultType=1 64: showResultValue=1 65: showSrcline=0 66: flushFlag=0
Listing 4: JLouiss-Output
01: <info> : jLouiss 0.8 (c) 2001 Albrecht Kleine
02: <info> : compiled for : JDK 1.22,1.3 , sn style
03: <info> : configuration : websync.cfg
04: <info> : logging classes : de.bablokb.websync.*
05: <info> : except : <none>
06: <info> : logging methods : <all>
07: <info> : except : <none>
08: <info> : logging exceptions: <no>
09: <info> : except : <none>
10: <info> : log source line : N
11: <info> : output format : 1 line per CALL, 1 line per RETURN as follows:
12: <info> : sequence thread-ID# CALL_/_RET class.method, parameter and result info
13: <info> : ======== ========== ========== ============================
14:
15: <entry>: 00000002 0x42840110 static de.bablokb.websync.main. WebSync.main(java.lang.String[]) = (0x4287de68)
16: <entry>: 00000004 0x42840110 0x4287df98 de.bablokb.websync.main.WebSync.<init>()
17: <exit> : 0x42840110 RET void de.bablokb.websync.main.WebSync.<init>
18: <entry>: 00000006 0x42840110 0x4287df98 de.bablokb.websync.main. WebSync.parseCommandLine(java.lang.String[]) = (0x4287de68)
19: <entry>: 00000008 0x42840110 static de.bablokb.websync.main.WebSync.U class$(java.lang.String) = ("de.bablokb.websync.main.WebSyn ..")
20: <exit> : 0x42840110 RET object de.bablokb.websync.main. WebSync.class$ = 0x4690c4c0 TYPE=java.lang.Class
21: <exit> : 0x42ac67f0 RET void de.bablokb.websync.main.WebSync.parseCommandLine
22: <entry>: 0000000a 0x42ac67f0 0x42ae22b8 de.bablokb.websync.main.WebSync.run()
finally{}
Tools fürs Debuggen gibt es also ausreichend. Betrachtet man aber den Aufwand, der für die spezielle Kompilation und den Programmaufruf notwendig ist, stellt sich schon die Frage, ob nicht ein einfaches »println()« an der verdächtigen Stelle genauso schnell hilft.
Die beiden kommenden Folgen des Coffee-Shops werden das Thema Qualitätssicherung bei Software von zwei weiteren Seiten beleuchten. Der nächste Beitrag befasst sich mit dem Unit-Testing, die Folge darauf mit dem Logging. In der Zwischenzeit ist die beste Strategie sicherlich, einfach fehlerfreien Code zu schreiben. ;-) (uwo)
Infos |
|
[1] JPDA: [http://java.sun.com/products/jpda/] [2] JSwat: [http://www.bluemarsh.com/java/jswat/] [3] JSwat-Tutorial: [http://heather.cs.ucdavis.edu/~matloff/jswat.html] [4] JDE-Homepage: [http://jdee.sunsite.dk] [5] Coffee-Shop: “Emacs als Java-IDE”, Linux-Magazin 2/99, S. 79ff. (auch online verfügbar) [6] Homepage von JLouiss, Tya und E3: [http://www.sax.de/~adlibit/] |
Der Autor |
|
Bernhard Bablok arbeitet bei der AGIS mbH (Allianz Gesellschaft für Informatik Service mbH) als Systemprogrammierer im Bereich Systems Management. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um Objektorientierung. Er ist unter [coffee-shop@bablokb.de] erreichbar. |











