Java调试接口(JDI)简介

2023/06/15

1. 概述

我们可能想知道像IntelliJ IDEA和Eclipse这样被广泛认可的IDE是如何实现调试功能的。这些工具严重依赖于Java Platform Debugger Architecture(JPDA)。

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

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

2. JPDA简介

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

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

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

然后是Java Debug Wire Protocol(JDWP),它定义了被测应用程序(调试对象)和调试器之间使用的协议。

最后,使用Java调试接口(JDI)来实现调试器应用程序。

3. 什么是JDI?

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

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

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

4. 设置

我们需要两个独立的程序-一个被调试程序和一个调试器,来理解JDI的实现。

首先,我们将编写一个示例程序作为调试对象。

让我们创建一个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);
    }
}

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

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

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

    // getters and setters
}

4.1 LaunchingConnector

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

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

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

因此,让我们将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);
}

现在,我们将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();
    }
}

让我们编译这两个类,JDIExampleDebuggee(被调试程序)和JDIExampleDebugger(调试器):

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

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

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

而-cp将在类路径中添加tools.jar来编译类

所有JDI库都在JDK的tools.jar中提供,因此,请务必在编译和执行时将tools.jar添加到类路径中。

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

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

注意tools.jar后面的“:.”,这会将tools.jar添加到当前运行时的类路径中(在Windows上使用“;.”)。

4.2 Bootstrap和ClassPrepareRequest

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

VirtualMachine类具有eventRequestManager方法来创建各种请求,如ClassPrepareRequest、BreakpointRequest和StepEventRequest。

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

这将过滤JDIExampleDebuggee类并启用ClassPrepareRequest:

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

4.3 ClassPrepareEvent和BreakpointRequest

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

使用ClassPrepareEvent,我们可以获取设置断点的位置并创建BreakPointRequest。

为此,让我们将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和StackFrame

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

JDI提供了StackFrame类,用来获取被调试程序所有可见变量的列表。

因此,让我们将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. 调试目标

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

因此,我们将使用已经讨论过的方法,如enableClassPrepareRequest、setBreakPoints和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();
}

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

最后,我们将执行调试器程序以及所有更改以查看输出:

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

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

因此,我们的自定义调试器已准备就绪。

5.1 StepRequest

调试还需要单步执行代码并在后续步骤中检查变量的状态。因此,我们将在断点处创建一个步骤请求。

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

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

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

现在,我们可以更新JDIExampleDebugger的main方法,以在它是BreakPointEvent时启用步骤请求:

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

5.2 StepEvent

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

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

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

最后,我们将执行调试器以在单步执行代码时查看变量的状态:

Variables at cn.tuyucheng.taketoday.jdi.JDIExampleDebuggee:6 > 
args = instance of java.lang.String[0] (id=93)
Variables at cn.tuyucheng.taketoday.jdi.JDIExampleDebuggee:9 > 
args = instance of java.lang.String[0] (id=93)
jpda = "Java Platform Debugger Architecture"
Variables at cn.tuyucheng.taketoday.jdi.JDIExampleDebuggee:10 > 
args = instance of java.lang.String[0] (id=93)
jpda = "Java Platform Debugger Architecture"
jdi = "Java Debug Interface"
Variables at cn.tuyucheng.taketoday.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 cn.tuyucheng.taketoday.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.

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

6. 读取执行输出

我们可能会注意到JDIExampleDebuggee类的println语句不是调试器输出的一部分。

根据JDI文档,如果我们通过LaunchingConnector启动VM,则其输出和错误流必须由Process对象读取。

因此,让我们将其添加到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();
}

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

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

7. 总结

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

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

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

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Gitalking ...

Post Directory

1. 概述
2. JPDA简介
3. 什么是JDI?
4. 设置
  - 4.1 LaunchingConnector
  - 4.2 Bootstrap和ClassPrepareRequest
  - 4.3 ClassPrepareEvent和BreakpointRequest
  - 4.4 BreakPointEvent和StackFrame
5. 调试目标
  - 5.1 StepRequest
  - 5.2 StepEvent
6. 读取执行输出
7. 总结
扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章