An Intro to the Java Debug Interface (JDI) – Java调试接口(JDI)的介绍

最后修改: 2019年 8月 26日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

We may wonder how widely recognized IDEs like IntelliJ IDEA and Eclipse implement debugging features. These tools rely heavily on the Java Platform Debugger Architecture (JPDA).

我们可能会想知道像IntelliJ IDEA和Eclipse这样被广泛认可的IDE是如何实现调试功能的。这些工具在很大程度上依赖于Java平台调试器架构(JPDA)。

In this introductory article, we’ll discuss the Java Debug Interface API (JDI) available under JPDA.

在这篇介绍性文章中,我们将讨论JPDA下可用的Java调试接口API(JDI)。

At the same time, we’ll write a custom debugger program step-by-step, familiarizing ourselves with handy JDI interfaces.

同时,我们将逐步编写一个自定义调试器程序,熟悉方便的JDI接口。

2. Introduction to JPDA

2.JPDA简介

Java Platform Debugger Architecture (JPDA) is a set of well-designed interfaces and protocols used to debug Java.

Java平台调试器架构(JPDA)是一套精心设计的接口和协议,用于调试Java。

It provides three specially designed interfaces, to implement custom debuggers for a development environment in desktop systems.

它提供了三个专门设计的接口,以实现桌面系统中开发环境的自定义调试器。

To begin, the Java Virtual Machine Tool Interface (JVMTI) helps us interact and control the execution of applications running in the JVM.

首先,Java虚拟机工具接口(JVMTI)帮助我们交互和控制在JVM中运行的应用程序的执行。

Then, there’s the Java Debug Wire Protocol (JDWP) which defines the protocol used between the application under test (debuggee) and the debugger.

然后,还有Java调试线协议(JDWP),它定义了被测程序(debuggee)和调试器之间使用的协议。

At last, the Java Debug Interface (JDI) is used to implement the debugger application.

最后,Java调试接口(JDI)被用来实现调试器的应用。

3. What Is JDI?

3.什么是JDI

Java Debug Interface API is a set of interfaces provided by Java, to implement the frontend of the debugger. JDI is the highest-layer of the JPDA.

Java Debug Interface API是由Java提供的一组接口,用于实现调试器的前端。JDI是JPDA的最高层

A debugger built with JDI can debug applications running in any JVM which supports JPDA. At the same time, we can hook it into any layer of debugging.

用JDI构建的调试器可以调试运行在任何支持JPDA的JVM中的应用程序。同时,我们可以把它挂到任何一层的调试中。

It provides the ability to access the VM and its state along with access to variables of the debuggee. At the same time, it allows to set the breakpoints, stepping, watchpoints and handle threads.

它提供了访问虚拟机及其状态的能力,以及访问调试器的变量的能力。同时,它允许设置断点、步进、观察点和处理线程。

4. Setup

4.设置

We’ll require two separate programs – a debuggee and a debugger – to understand JDI’s implementations.

我们需要两个独立的程序–调试程序和调试器–来了解JDI的实现。

First, we’ll write a sample program as the debuggee.

首先,我们要写一个样本程序作为调试器。

Let’s create a JDIExampleDebuggee class with a few String variables and println statements:

让我们创建一个JDIExampleDebuggee类,其中有几个String变量和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);
    }
}

Then, we’ll write a debugger program.

然后,我们将编写一个调试器程序。

Let’s create a JDIExampleDebugger class with properties to hold the debugging program (debugClass) and line numbers for breakpoints (breakPointLines):

让我们创建一个JDIExampleDebugger类,用属性来保存调试程序(debugClass)和断点的行号(breakPointLines)。

public class JDIExampleDebugger {
    private Class debugClass; 
    private int[] breakPointLines;

    // getters and setters
}

4.1. LaunchingConnector

4.1.LaunchingConnector

At first, a debugger requires a connector to establish a connection with the target Virtual Machine (VM).

起初,调试器需要一个连接器来与目标虚拟机(VM)建立连接。

Then, we’ll need to set the debuggee as the connector’s main argument. At last, the connector should launch the VM for debugging.

然后,我们需要将debuggee设置为连接器的main参数。最后,连接器应该启动虚拟机进行调试。

To do so, JDI provides a Bootstrap class which gives an instance of the LaunchingConnector. The LaunchingConnector provides a map of the default arguments, in which we can set the main argument.

为此,JDI提供了一个Bootstrap类,它给出了一个LaunchingConnector的实例。LaunchingConnector提供了一个默认参数的映射,我们可以在其中设置main参数。

Therefore, let’s add the connectAndLaunchVM method to the JDIDebuggerExample class:

因此,让我们将connectAndLaunchVM方法添加到JDIDebuggerExample类。

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

Now, we’ll add the main method to the JDIDebuggerExample class to debug the JDIExampleDebuggee:

现在,我们将把main方法添加到JDIDebuggerExample类中,以调试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();
    }
}

Let’s compile both of our classes, JDIExampleDebuggee (debuggee) and JDIExampleDebugger (debugger):

让我们编译我们的两个类,JDIExampleDebuggee(debuggee)和JDIExampleDebugger(debugger)。

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

Let’s discuss the javac command used here, in detail.

让我们详细讨论一下这里使用的javac命令。

The -g option generates all the debugging information without which, we may see AbsentInformationException.

-g选项生成所有的调试信息没有它,我们可能看到AbsentInformationException

And -cp will add the tools.jar in the classpath to compile the classes.

-cp将在classpath中添加tools.jar来编译这些类。

All JDI libraries are available under tools.jar of the JDK. Therefore, make sure to add the tools.jar in the classpath at both compilation and execution.

所有的JDI库都在JDK的tools.jar下可用。因此,请确保在编译和执行时将tools.jar加入classpath中。

That’s it, now we are ready to execute our custom debugger JDIExampleDebugger:

就这样,现在我们准备执行我们的自定义调试器 JDIExampleDebugger:

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

Note the “:.” with tools.jar. This will append tools.jar to the classpath for current run time (use “;.” on windows).

注意tools.jar.的”:.”,这将把tools.jar追加到当前运行时间的classpath(在windows下使用”;.”)。

4.2. Bootstrap and ClassPrepareRequest

4.2.BootstrapClassPrepareRequest

Executing the debugger program here will give no results since we haven’t prepared the class for debugging and set the breakpoints.

在这里执行调试器程序不会有任何结果,因为我们还没有为调试准备好类并设置断点。

The VirtualMachine class has the eventRequestManager method to create various requests like ClassPrepareRequest, BreakpointRequest, and StepEventRequest.

VirtualMachine类有eventRequestManager方法来创建各种请求,如ClassPrepareRequestBreakpointRequest,和StepEventRequest。

So, let’s add the enableClassPrepareRequest method to the JDIExampleDebugger class.

因此,让我们将enableClassPrepareRequest方法添加到JDIExampleDebugger类。

This will filter the JDIExampleDebuggee class and enables the ClassPrepareRequest:

这将过滤JDIExampleDebuggee类并启用ClassPrepareRequest:

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

4.3. ClassPrepareEvent and BreakpointRequest

4.3.ClassPrepareEventBreakpointRequest

Once, ClassPrepareRequest for the JDIExampleDebuggee class is enabled, the event queue of the VM will start having instances of the ClassPrepareEvent.

一旦,ClassPrepareRequestJDIExampleDebuggee类启用,VM的事件队列将开始有ClassPrepareEvent的实例。

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

使用ClassPrepareEvent,我们可以获得设置断点的位置并创建一个BreakPointRequest

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

要做到这一点,让我们把setBreakPoints方法添加到JDIExampleDebugger类。

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

4.4.BreakPointEventStackFrame

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

到目前为止,我们已经为调试准备了类,并设置了断点。现在,我们需要捕捉BreakPointEvent并显示变量。

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

JDI提供了 StackFrame类,以获得调试器的所有可见变量的列表。

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

因此,让我们把displayVariables方法添加到JDIExampleDebugger类。

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

5. Debug Target

5.调试目标

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

在这一步,我们只需要更新main方法的JDIExampleDebugger即可开始调试。

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

因此,我们将使用已经讨论过的方法,如enableClassPrepareRequestsetBreakPointsdisplayVariables:

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.

现在首先,让我们用已经讨论过的javac命令再次编译JDIDebuggerExample类。

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).

万岁!我们已经成功调试了JDIExampleDebuggee类。同时,我们已经显示了断点位置(第6行和第9行)的变量值。

Therefore, our custom debugger is ready.

因此,我们的自定义调试器已经准备好了。

5.1. StepRequest

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.

在创建StepRequest的实例时,我们必须提供步骤的大小和深度。我们将分别定义STEP_LINESTEP_OVER

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

让我们写一个方法来启用步骤请求。

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

为了简单起见,我们将从最后一个断点(第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:

现在,我们可以更新JDIExampleDebuggermain方法,当它是一个BreakPointEvent时启用步骤请求。

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

5.2. StepEvent

5.2.StepEvent

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

BreakPointEvent类似,我们也可以在StepEvent显示变量。

Let’s update the main method accordingly:

让我们相应地更新main方法。

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.

如果我们比较一下输出,就会发现调试器从第9行开始介入,并在所有后续步骤中显示变量。

6. Read Execution Output

6.读取执行输出

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

我们可能注意到,println类的JDIExampleDebuggee语句还没有成为调试器输出的一部分。

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:

因此,让我们把它添加到我们的main方法的finally子句中。

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:

现在,执行调试器程序也会将println类中的JDIExampleDebuggee语句添加到调试输出中。

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

7. Conclusion

7.结语

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

在这篇文章中,我们已经探讨了Java平台调试器架构(JPDA)下可用的Java调试接口(JDI)API。

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.

在这一过程中,我们利用JDI提供的方便的接口建立了一个自定义的调试器。同时,我们还为调试器增加了步进功能。

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

由于这只是对JDI的介绍,建议看看JDI API下的其他接口的实现。

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

像往常一样,所有的代码实现都可以在GitHub上找到