Eine Einführung in die Java Debug Interface (JDI)

1. Übersicht

Wir fragen uns vielleicht, wie weithin anerkannte IDEs wie IntelliJ IDEA und Eclipse Debugging-Funktionen implementieren. Diese Tools basieren stark auf der Java Platform Debugger Architecture (JPDA).

In diesem Einführungsartikel wird die unter JPDA verfügbare Java Debug Interface API (JDI) erläutert.

Gleichzeitig schreiben wir Schritt für Schritt ein benutzerdefiniertes Debugger-Programm , das uns mit den praktischen JDI-Schnittstellen vertraut macht.

2. Einführung in JPDA

Die Java Platform Debugger Architecture (JPDA) besteht aus einer Reihe gut gestalteter Schnittstellen und Protokolle, die zum Debuggen von Java verwendet werden.

Es bietet drei speziell entwickelte Schnittstellen, um benutzerdefinierte Debugger für eine Entwicklungsumgebung in Desktop-Systemen zu implementieren.

Zunächst hilft uns die Java Virtual Machine Tool-Schnittstelle (JVMTI) bei der Interaktion und Steuerung der Ausführung von Anwendungen, die in der JVM ausgeführt werden.

Dann gibt es das Java Debug Wire Protocol (JDWP), das das Protokoll definiert, das zwischen der zu testenden Anwendung (Debuggee) und dem Debugger verwendet wird.

Zuletzt wird das Java Debug Interface (JDI) verwendet, um die Debugger-Anwendung zu implementieren.

3. Was ist JDI ?

Die Java Debug Interface API ist eine Reihe von Schnittstellen, die von Java bereitgestellt werden, um das Frontend des Debuggers zu implementieren. JDI ist die höchste Schicht des JPDA .

Ein mit JDI erstellter Debugger kann Anwendungen debuggen, die in jeder JVM ausgeführt werden, die JPDA unterstützt. Gleichzeitig können wir es in jede Debugging-Ebene einbinden.

Es bietet die Möglichkeit, auf die VM und ihren Status sowie auf Variablen des Debuggers zuzugreifen. Gleichzeitig können Haltepunkte, Schritte, Überwachungspunkte und Threads festgelegt werden.

4. Setup

Wir benötigen zwei separate Programme - einen Debugger und einen Debugger -, um die Implementierungen von JDI zu verstehen.

Zuerst schreiben wir ein Beispielprogramm als Debuggee.

Erstellen wir eine JDIExampleDebuggee- Klasse mit einigen String- Variablen und println- Anweisungen:

public class JDIExampleDebuggee { public static void main(String[] args) { String jpda = "Java Platform Debugger Architecture"; System.out.println("Hi Everyone, Welcome to " + jpda); // add a break point here String jdi = "Java Debug Interface"; // add a break point here and also stepping in here String text = "Today, we'll dive into " + jdi; System.out.println(text); } }

Dann schreiben wir ein Debugger-Programm.

Erstellen wir eine JDIExampleDebugger- Klasse mit Eigenschaften für das Debugging-Programm ( debugClass ) und Zeilennummern für Haltepunkte ( breakPointLines ):

public class JDIExampleDebugger { private Class debugClass; private int[] breakPointLines; // getters and setters }

4.1. Connector starten

Für einen Debugger ist zunächst ein Connector erforderlich, um eine Verbindung mit der virtuellen Zielmaschine (VM) herzustellen.

Dann müssen wir die Debuggee als den Stecker des setzen Hauptargument. Zuletzt sollte der Connector die VM zum Debuggen starten.

Zu diesem Zweck stellt JDI eine Bootstrap- Klasse bereit, die eine Instanz des LaunchingConnector angibt . Der LaunchingConnector bietet eine Karte der Standardargumente, in denen wir das einstellen Hauptargument.

Deshalb lassen Sie uns das Hinzufügen connectAndLaunchVM Methode zum JDIDebuggerExample Klasse:

public VirtualMachine connectAndLaunchVM() throws Exception { LaunchingConnector launchingConnector = Bootstrap.virtualMachineManager() .defaultConnector(); Map arguments = launchingConnector.defaultArguments(); arguments.get("main").setValue(debugClass.getName()); return launchingConnector.launch(arguments); }

Jetzt werden wir das Hinzufügen Hauptverfahren zur JDIDebuggerExample Klasse zu debuggen die JDIExampleDebuggee:

public static void main(String[] args) throws Exception { JDIExampleDebugger debuggerInstance = new JDIExampleDebugger(); debuggerInstance.setDebugClass(JDIExampleDebuggee.class); int[] breakPoints = {6, 9}; debuggerInstance.setBreakPointLines(breakPoints); VirtualMachine vm = null; try { vm = debuggerInstance.connectAndLaunchVM(); vm.resume(); } catch(Exception e) { e.printStackTrace(); } }

Lassen Sie uns unsere beiden Klassen JDIExampleDebuggee (Debuggee) und JDIExampleDebugger (Debugger) kompilieren :

javac -g -cp "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar" com/baeldung/jdi/*.java

Lassen Sie uns den hier verwendeten Befehl javac im Detail diskutieren .

Die Option -g generiert alle Debugging-Informationen, ohne die AbsentInformationException angezeigt wird .

Und -cp fügt die tools.jar im Klassenpfad hinzu, um die Klassen zu kompilieren.

Alle JDI-Bibliotheken sind unter tools.jar des JDK verfügbar . Stellen Sie daher sicher, dass Sie beim Kompilieren und Ausführen die Datei tools.jar im Klassenpfad hinzufügen .

Jetzt können wir unseren benutzerdefinierten Debugger JDIExampleDebugger ausführen :

java -cp "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar:." JDIExampleDebugger

Beachten Sie das ":." mit tools.jar. Dadurch wird tools.jar für die aktuelle Laufzeit an den Klassenpfad angehängt (verwenden Sie unter Windows ";.").

4.2. Bootstrap und ClassPrepareRequest

Das Ausführen des Debugger-Programms hier führt zu keinen Ergebnissen, da wir die Klasse nicht für das Debuggen vorbereitet und die Haltepunkte festgelegt haben.

Die Virtuelle Maschine Klasse hat die eventRequestManager Methode , um verschiedene Anforderungen wie zu schaffen ClassPrepareRequest , BreakpointRequest und StepEventRequest.

Also, lassen Sie uns das Hinzufügen enableClassPrepareRequest Methode zum JDIExampleDebugger Klasse.

Dadurch wird die JDIExampleDebuggee- Klasse gefiltert und die ClassPrepareRequest aktiviert :

public void enableClassPrepareRequest(VirtualMachine vm) { ClassPrepareRequest classPrepareRequest = vm.eventRequestManager().createClassPrepareRequest(); classPrepareRequest.addClassFilter(debugClass.getName()); classPrepareRequest.enable(); }

4.3. ClassPrepareEvent und BreakpointRequest

Einmal ClassPrepareRequest für die JDIExampleDebuggee ist Klasse aktiviert, wird die Ereigniswarteschlange der VM startet Instanzen des mit ClassPrepareEvent .

Using ClassPrepareEvent, we can get the location to set a breakpoint and creates a BreakPointRequest.

To do so, let's add the setBreakPoints method to the JDIExampleDebugger class:

public void setBreakPoints(VirtualMachine vm, ClassPrepareEvent event) throws AbsentInformationException { ClassType classType = (ClassType) event.referenceType(); for(int lineNumber: breakPointLines) { Location location = classType.locationsOfLine(lineNumber).get(0); BreakpointRequest bpReq = vm.eventRequestManager().createBreakpointRequest(location); bpReq.enable(); } }

4.4. BreakPointEvent and StackFrame

So far, we've prepared the class for debugging and set the breakpoints. Now, we need to catch the BreakPointEvent and display the variables.

JDI provides the StackFrame class, to get the list of all the visible variables of the debuggee.

Therefore, let's add the displayVariables method to the JDIExampleDebugger class:

public void displayVariables(LocatableEvent event) throws IncompatibleThreadStateException, AbsentInformationException { StackFrame stackFrame = event.thread().frame(0); if(stackFrame.location().toString().contains(debugClass.getName())) { Map visibleVariables = stackFrame .getValues(stackFrame.visibleVariables()); System.out.println("Variables at " + stackFrame.location().toString() + " > "); for (Map.Entry entry : visibleVariables.entrySet()) { System.out.println(entry.getKey().name() + " = " + entry.getValue()); } } }

5. Debug Target

At this step, all we need is to update the main method of the JDIExampleDebugger to start debugging.

Hence, we'll use the already discussed methods like enableClassPrepareRequest, setBreakPoints, and displayVariables:

try { vm = debuggerInstance.connectAndLaunchVM(); debuggerInstance.enableClassPrepareRequest(vm); EventSet eventSet = null; while ((eventSet = vm.eventQueue().remove()) != null) { for (Event event : eventSet) { if (event instanceof ClassPrepareEvent) { debuggerInstance.setBreakPoints(vm, (ClassPrepareEvent)event); } if (event instanceof BreakpointEvent) { debuggerInstance.displayVariables((BreakpointEvent) event); } vm.resume(); } } } catch (VMDisconnectedException e) { System.out.println("Virtual Machine is disconnected."); } catch (Exception e) { e.printStackTrace(); }

Now firstly, let's compile the JDIDebuggerExample class again with the already discussed javac command.

And last, we'll execute the debugger program along with all the changes to see the output:

Variables at com.baeldung.jdi.JDIExampleDebuggee:6 > args = instance of java.lang.String[0] (id=93) Variables at com.baeldung.jdi.JDIExampleDebuggee:9 > jpda = "Java Platform Debugger Architecture" args = instance of java.lang.String[0] (id=93) Virtual Machine is disconnected.

Hurray! We've successfully debugged the JDIExampleDebuggee class. At the same time, we've displayed the values of the variables at the breakpoint locations (line number 6 and 9).

Therefore, our custom debugger is ready.

5.1. StepRequest

Debugging also requires stepping through the code and checking the state of the variables at subsequent steps. Therefore, we'll create a step request at the breakpoint.

While creating the instance of the StepRequest, we must provide the size and depth of the step. We'll define STEP_LINE and STEP_OVER respectively.

Let's write a method to enable the step request.

For simplicity, we'll start stepping at the last breakpoint (line number 9):

public void enableStepRequest(VirtualMachine vm, BreakpointEvent event) { // enable step request for last break point if (event.location().toString(). contains(debugClass.getName() + ":" + breakPointLines[breakPointLines.length-1])) { StepRequest stepRequest = vm.eventRequestManager() .createStepRequest(event.thread(), StepRequest.STEP_LINE, StepRequest.STEP_OVER); stepRequest.enable(); } }

Now, we can update the main method of the JDIExampleDebugger, to enable the step request when it is a BreakPointEvent:

if (event instanceof BreakpointEvent) { debuggerInstance.enableStepRequest(vm, (BreakpointEvent)event); }

5.2. StepEvent

Similar to the BreakPointEvent, we can also display the variables at the StepEvent.

Let's update the main method accordingly:

if (event instanceof StepEvent) { debuggerInstance.displayVariables((StepEvent) event); }

At last, we'll execute the debugger to see the state of the variables while stepping through the code:

Variables at com.baeldung.jdi.JDIExampleDebuggee:6 > args = instance of java.lang.String[0] (id=93) Variables at com.baeldung.jdi.JDIExampleDebuggee:9 > args = instance of java.lang.String[0] (id=93) jpda = "Java Platform Debugger Architecture" Variables at com.baeldung.jdi.JDIExampleDebuggee:10 > args = instance of java.lang.String[0] (id=93) jpda = "Java Platform Debugger Architecture" jdi = "Java Debug Interface" Variables at com.baeldung.jdi.JDIExampleDebuggee:11 > args = instance of java.lang.String[0] (id=93) jpda = "Java Platform Debugger Architecture" jdi = "Java Debug Interface" text = "Today, we'll dive into Java Debug Interface" Variables at com.baeldung.jdi.JDIExampleDebuggee:12 > args = instance of java.lang.String[0] (id=93) jpda = "Java Platform Debugger Architecture" jdi = "Java Debug Interface" text = "Today, we'll dive into Java Debug Interface" Virtual Machine is disconnected.

If we compare the output, we'll realize that debugger stepped in from line number 9 and displays the variables at all subsequent steps.

6. Read Execution Output

We might notice that println statements of the JDIExampleDebuggee class haven't been part of the debugger output.

As per the JDI documentation, if we launch the VM through LaunchingConnector, its output and error streams must be read by the Process object.

Therefore, let's add it to the finally clause of our main method:

finally { InputStreamReader reader = new InputStreamReader(vm.process().getInputStream()); OutputStreamWriter writer = new OutputStreamWriter(System.out); char[] buf = new char[512]; reader.read(buf); writer.write(buf); writer.flush(); }

Now, executing the debugger program will also add the println statements from the JDIExampleDebuggee class to the debugging output:

Hi Everyone, Welcome to Java Platform Debugger Architecture Today, we'll dive into Java Debug Interface

7. Conclusion

In this article, we've explored the Java Debug Interface (JDI) API available under the Java Platform Debugger Architecture (JPDA).

Along the way, we've built a custom debugger utilizing the handy interfaces provided by JDI. At the same time, we've also added stepping capability to the debugger.

As this was just an introduction to JDI, it is recommended to look at the implementations of other interfaces available under JDI API.

As usual, all the code implementations are available over on GitHub.