Un'introduzione alla Java Debug Interface (JDI)

1. Panoramica

Potremmo chiederci quanto IDE ampiamente riconosciuti come IntelliJ IDEA ed Eclipse implementino funzionalità di debug. Questi strumenti fanno molto affidamento sulla Java Platform Debugger Architecture (JPDA).

In questo articolo introduttivo, discuteremo della Java Debug Interface API (JDI) disponibile sotto JPDA.

Allo stesso tempo, scriveremo un programma di debugger personalizzato passo dopo passo, familiarizzando con le pratiche interfacce JDI.

2. Introduzione a JPDA

Java Platform Debugger Architecture (JPDA) è un insieme di interfacce e protocolli ben progettati utilizzati per eseguire il debug di Java.

Fornisce tre interfacce appositamente progettate, per implementare debugger personalizzati per un ambiente di sviluppo nei sistemi desktop.

Per iniziare, la Java Virtual Machine Tool Interface (JVMTI) ci aiuta a interagire e controllare l'esecuzione delle applicazioni in esecuzione nella JVM.

Poi, c'è il Java Debug Wire Protocol (JDWP) che definisce il protocollo utilizzato tra l'applicazione sotto test (debuggee) e il debugger.

Infine, la Java Debug Interface (JDI) viene utilizzata per implementare l'applicazione debugger.

3. Che cos'è JDI ?

Java Debug Interface API è un insieme di interfacce fornite da Java, per implementare il frontend del debugger. JDI è il livello più alto di JPDA .

Un debugger creato con JDI può eseguire il debug di applicazioni in esecuzione in qualsiasi JVM che supporti JPDA. Allo stesso tempo, possiamo agganciarlo a qualsiasi livello di debug.

Fornisce la possibilità di accedere alla VM e al suo stato insieme all'accesso alle variabili del debuggee. Allo stesso tempo, permette di impostare i breakpoint, stepping, watchpoint e gestire i thread.

4. Configurazione

Avremo bisogno di due programmi separati - un debuggee e un debugger - per comprendere le implementazioni di JDI.

Per prima cosa, scriveremo un programma di esempio come debuggee.

Creiamo una classe JDIExampleDebuggee con alcune variabili String e istruzioni println :

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); } }

Quindi, scriveremo un programma di debug.

Creiamo una classe JDIExampleDebugger con proprietà per contenere il programma di debug ( debugClass ) e numeri di riga per i punti di interruzione ( breakPointLines ):

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

4.1. LaunchingConnector

All'inizio, un debugger richiede un connettore per stabilire una connessione con la macchina virtuale (VM) di destinazione.

Quindi, dovremo impostare il debuggee come argomento principale del connettore . Alla fine, il connettore dovrebbe avviare la VM per il debug.

Per fare ciò, JDI fornisce una classe Bootstrap che fornisce un'istanza di LaunchingConnector . Il LaunchingConnector fornisce una mappa degli argomenti predefiniti, in cui possiamo impostare l' argomento principale .

Pertanto, aggiungiamo il metodo connectAndLaunchVM alla classe JDIDebuggerExample :

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

Ora aggiungeremo il metodo principale alla classe JDIDebuggerExample per eseguire il debug di 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(); } }

Compiliamo entrambe le nostre classi, JDIExampleDebuggee (debuggee) e JDIExampleDebugger (debugger):

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

Discutiamo in dettaglio il comando javac usato qui.

L' opzione -g genera tutte le informazioni di debug senza le quali, potremmo vedere AbsentInformationException .

E -cp aggiungerà tools.jar nel classpath per compilare le classi.

Tutte le librerie JDI sono disponibili in tools.jar di JDK. Pertanto, assicurati di aggiungere tools.jar nel classpath sia alla compilazione che all'esecuzione.

Ecco fatto, ora siamo pronti per eseguire il nostro debugger personalizzato JDIExampleDebugger:

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

Notare la ":." con tools.jar. Questo aggiungerà tools.jar al classpath per il tempo di esecuzione corrente (usa ";." Su Windows).

4.2. Bootstrap e ClassPrepareRequest

L'esecuzione del programma di debug qui non darà risultati poiché non abbiamo preparato la classe per il debug e impostato i punti di interruzione.

La classe VirtualMachine ha il metodo eventRequestManager per creare varie richieste come ClassPrepareRequest , BreakpointRequest e StepEventRequest.

Quindi, aggiungiamo il metodo enableClassPrepareRequest alla classe JDIExampleDebugger .

Questo filtrerà la classe JDIExampleDebuggee e abiliterà ClassPrepareRequest:

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

4.3. ClassPrepareEvent e BreakpointRequest

Una volta abilitato ClassPrepareRequest per la classe JDIExampleDebuggee , la coda degli eventi della VM inizierà ad avere istanze di 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.