Übersicht

Das Debuggen einer entfernten Java-Anwendung kann in mehr als einem Fall nützlich sein.

In diesem Tutorial werden wir herausfinden, wie man das mit den Werkzeugen des JDK macht.

Die Anwendung

Lassen Sie uns damit beginnen, eine Anwendung zu schreiben. Wir lassen sie an einem entfernten Ort laufen und debuggen sie lokal mit Hilfe dieses Artikels:

public class OurApplication { private static String staticString = "Static String"; private String instanceString; public static void main(String args) { for (int i = 0; i < 1_000_000_000; i++) { OurApplication app = new OurApplication(i); System.out.println(app.instanceString); } } public OurApplication(int index) { this.instanceString = buildInstanceString(index); } public String buildInstanceString(int number) { return number + ". Instance String !"; }}

JDWP: Das Java Debug Wire Protocol

Das Java Debug Wire Protocol ist ein in Java verwendetes Protokoll für die Kommunikation zwischen einem Debuggee und einem Debugger. Der Debuggee ist die zu debuggende Anwendung, während der Debugger eine Anwendung oder ein Prozess ist, der sich mit der zu debuggenden Anwendung verbindet.

Beide Anwendungen laufen entweder auf demselben Rechner oder auf verschiedenen Rechnern. Wir werden uns auf letzteres konzentrieren.

3.1. Die Optionen von JDWP

Wir werden JDWP in den JVM-Befehlszeilenargumenten verwenden, wenn wir die Debuggee-Anwendung starten.

Ihr Aufruf erfordert eine Liste von Optionen:

  • transport ist die einzige vollständig erforderliche Option. Sie legt fest, welcher Transportmechanismus verwendet werden soll. dt_shmem funktioniert nur unter Windows und wenn beide Prozesse auf demselben Rechner laufen, während dt_socket mit allen Plattformen kompatibel ist und es den Prozessen ermöglicht, auf verschiedenen Rechnern zu laufen
  • server ist keine obligatorische Option. Wenn dieses Flag aktiviert ist, definiert es die Art und Weise, wie der Prozess mit dem Debugger verbunden wird. Entweder wird der Prozess über die in der Option address definierte Adresse offengelegt. Andernfalls gibt JDWP eine Standardadresse an
  • suspend legt fest, ob die JVM auf einen Debugger warten soll oder nicht
  • address ist die Option, die die Adresse, im Allgemeinen ein Port, des Debuggers enthält. Sie kann auch eine Adresse darstellen, die als Zeichenkette übersetzt wird (wie javadebug, wenn wir server=y verwenden, ohne eine Adresse unter Windows anzugeben)

3.2. Launch Command

Beginnen wir mit dem Starten der Remote-Anwendung. Wir werden alle zuvor aufgelisteten Optionen zur Verfügung stellen:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000 OurApplication

Bis Java 5 musste das JVM-Argument runjdwp zusammen mit der anderen Option debug verwendet werden:

java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000

Diese Art der Verwendung von JDWP wird immer noch unterstützt, wird aber in zukünftigen Versionen wegfallen. Wir werden die Verwendung der neueren Notation bevorzugen, wenn möglich.

3.3. Seit Java 9

Endlich hat sich eine der Optionen von JDWP mit der Veröffentlichung von Version 9 von Java geändert. Es handelt sich um eine recht kleine Änderung, da sie nur eine Option betrifft, aber sie wird einen Unterschied machen, wenn wir versuchen, eine entfernte Anwendung zu debuggen.

Diese Änderung wirkt sich darauf aus, wie sich die Adresse für entfernte Anwendungen verhält. Die ältere Schreibweise address=8000 gilt nur für localhost. Um das alte Verhalten zu erreichen, verwenden wir ein Sternchen mit einem Doppelpunkt als Präfix für die Adresse (z.B. address=*:8000).

Nach der Dokumentation ist dies nicht sicher und es wird empfohlen, die IP-Adresse des Debuggers anzugeben, wann immer dies möglich ist:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:8000

JDB: Der Java Debugger

JDB, der Java Debugger, ist ein Werkzeug, das im JDK enthalten ist, um einen bequemen Debugger-Client von der Kommandozeile aus bereitzustellen.

Um JDB zu starten, werden wir den Attach-Modus verwenden. Dieser Modus fügt JDB an eine laufende JVM an. Es gibt noch andere Modi, wie z.B. listen oder run, die aber vor allem beim Debuggen einer lokal laufenden Anwendung nützlich sind:

jdb -attach 127.0.0.1:8000> Initializing jdb ...

4.1. Haltepunkte

Lassen Sie uns fortfahren, indem wir einige Haltepunkte in der in Abschnitt 1 vorgestellten Anwendung setzen.

Wir setzen einen Haltepunkt im Konstruktor:

> stop in OurApplication.<init>

Wir setzen einen weiteren in der statischen Methode main, indem wir den vollqualifizierten Namen der String-Klasse verwenden:

> stop in OurApplication.main(java.lang.String)

Schließlich setzen wir den letzten in der Instanzmethode buildInstanceString:

> stop in OurApplication.buildInstanceString(int)

Wir sollten nun feststellen, dass die Serveranwendung angehalten und das Folgende in unserer Debugger-Konsole ausgegeben wird:

> Breakpoint hit: "thread=main", OurApplication.<init>(), line=11 bci=0

Lassen Sie uns nun einen Haltepunkt in einer bestimmten Zeile hinzufügen, nämlich in der, in der die Variable app.instanceString gedruckt wird:

> stop at OurApplication:7

Wir bemerken, dass at nach stop anstelle von in verwendet wird, wenn der Haltepunkt auf einer bestimmten Zeile definiert ist.

4.2. Navigieren und Auswerten

Nun, da wir unsere Haltepunkte gesetzt haben, lassen Sie uns cont verwenden, um die Ausführung unseres Threads fortzusetzen, bis wir den Haltepunkt in Zeile 7 erreichen.

In der Konsole sollte Folgendes angezeigt werden:

> Breakpoint hit: "thread=main", OurApplication.main(), line=7 bci=17

Zur Erinnerung: Wir haben in der Zeile angehalten, die das folgende Codestück enthält:

System.out.println(app.instanceString);

Das Anhalten in dieser Zeile hätte auch durch Anhalten in der Hauptmethode und zweimaliges Eintippen von step erfolgen können. step führt die aktuelle Codezeile aus und hält den Debugger direkt in der nächsten Zeile an.

Nachdem wir angehalten haben, wertet der Debugger unseren staticString, den instanceString der App, die lokale Variable i aus und schaut sich schließlich an, wie man andere Ausdrücke auswertet.

Lassen Sie uns staticField auf der Konsole ausgeben:

> eval OurApplication.staticStringOurApplication.staticString = "Static String"

Wir setzen den Namen der Klasse explizit vor das statische Feld.

Lassen Sie uns nun das Instanzfeld von app ausgeben:

> eval app.instanceStringapp.instanceString = "68741. Instance String !"

Als nächstes sehen wir uns die Variable i an:

> print ii = 68741

Im Gegensatz zu den anderen Variablen ist es bei lokalen Variablen nicht erforderlich, eine Klasse oder eine Instanz anzugeben. Wir können auch sehen, dass print genau dasselbe Verhalten wie eval hat: beide werten einen Ausdruck oder eine Variable aus.

Wir werten eine neue Instanz von OurApplication aus, für die wir eine ganze Zahl als Konstruktorparameter übergeben haben:

> print new OurApplication(10).instanceStringnew OurApplication(10).instanceString = "10. Instance String !"

Nachdem wir alle Variablen ausgewertet haben, die wir brauchten, wollen wir die zuvor gesetzten Haltepunkte löschen und den Thread mit seiner Verarbeitung fortfahren lassen. Um dies zu erreichen, verwenden wir den Befehl clear, gefolgt von der Kennung des Haltepunkts.

Die Kennung ist genau die gleiche wie die, die zuvor mit dem Befehl stop verwendet wurde:

> clear OurApplication:7Removed: breakpoint OurApplication:7

Um zu überprüfen, ob der Haltepunkt korrekt entfernt wurde, verwenden wir clear ohne Argumente. Dies zeigt die Liste der existierenden Haltepunkte an, ohne den gerade gelöschten:

> clearBreakpoints set: breakpoint OurApplication.<init> breakpoint OurApplication.buildInstanceString(int) breakpoint OurApplication.main(java.lang.String)

Abschluss

I:n diesem kurzen Artikel haben wir entdeckt, wie man JDWP zusammen mit JDB, beides JDK-Tools, benutzen kann.