1. 概述

你有没有好奇过 IntelliJ IDEA 和 Eclipse 这类主流 IDE 是如何实现调试功能的?它们的背后都依赖于 Java Platform Debugger Architecture(JPDA) 这套调试架构。

本文将带你深入理解 JPDA 中的 Java Debug Interface(JDI)API,并手把手实现一个简单的自定义调试器,帮助你掌握 JDI 的核心接口。

✅ 重点目标:

  • 理解 JDI 在 JPDA 中的角色
  • 实现一个能设置断点、查看变量、单步执行的简易调试器

2. JPDA 简介

JPDA(Java Platform Debugger Architecture)是 Java 提供的一套用于调试的标准化接口与协议集合。它由三个层次组成,分别对应调试的不同阶段:

  1. JVMTI(Java Virtual Machine Tool Interface)

    • JVM 层的接口,允许工具直接监控和控制 JVM 的运行状态
    • 比如:获取线程、内存、类加载信息等
  2. JDWP(Java Debug Wire Protocol)

    • 定义了调试器(debugger)与被调试程序(debuggee)之间的通信协议
    • 通常通过 socket 或共享内存传输调试指令和数据
  3. JDI(Java Debug Interface)

    • 最高层接口,用于开发调试器前端
    • 基于 JVMTI 和 JDWP 构建,提供面向对象的 Java API
    • 我们日常用的 IDE 调试功能,底层就是通过 JDI 实现的

🔍 简单理解:JDI 是给 Java 程序员用的“调试 API”,而 JVMTI 是给 JVM 开发者用的“底层钩子”。


3. 什么是 JDI?

JDI 是 JPDA 的最高层接口,专为实现调试器前端而设计。它的核心能力包括:

✅ 支持跨 JVM 调试

  • 只要目标 JVM 支持 JPDA,JDI 调试器就能连接并调试

✅ 提供完整的调试控制能力

  • 设置断点(breakpoints)
  • 单步执行(stepping)
  • 查看变量值(locals, fields)
  • 监控线程状态
  • 捕获异常和类加载事件

✅ 面向对象的 API 设计

  • 所有操作都通过 VirtualMachineEventStackFrame 等对象完成

⚠️ 注意:JDI 本身不负责启动 JVM 或建立连接,它依赖 Connector 机制来完成初始化。


4. 环境准备

要实践 JDI,我们需要两个程序:

  • debuggee:被调试的目标程序
  • debugger:我们自己写的调试器

4.1 被调试程序(Debuggee)

public class JDIExampleDebuggee {
    public static void main(String[] args) {
        String jpda = "Java Platform Debugger Architecture";
        System.out.println("Hi Everyone, Welcome to " + jpda); // 在此处设断点

        String jdi = "Java Debug Interface"; // 此处也设断点,并单步执行
        String text = "Today, we'll dive into " + jdi;
        System.out.println(text);
    }
}

4.2 调试器类骨架

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

    // getter 和 setter 省略
}

4.3 启动连接器(LaunchingConnector)

调试的第一步是启动目标 JVM 并建立连接。JDI 提供了 LaunchingConnector 来完成这个任务。

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 方法中调用:

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

4.4 编译与运行

⚠️ 关键点:必须包含 tools.jar 和调试信息

# 编译(Mac/Linux)
javac -g -cp "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar" com/baeldung/jdi/*.java

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

-g:生成调试信息,否则会抛 AbsentInformationException
-cp ...tools.jar:JDI 的所有类都在 tools.jar 中,必须加入 classpath


4.5 类准备事件(ClassPrepareRequest)

目前程序运行但没有调试行为,因为我们还没设置断点。断点必须在类加载后才能设置。

使用 ClassPrepareRequest 监听类加载事件:

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

4.6 断点设置(BreakpointRequest)

JDIExampleDebuggee 类加载完成后,会触发 ClassPrepareEvent,此时可以设置断点:

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.7 变量查看(StackFrame)

断点触发后,通过 StackFrame 获取当前作用域的变量值:

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. 调试主循环

现在把所有环节串起来,实现完整的调试流程:

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

输出结果

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.

✅ 成功!我们在第 6 行和第 9 行捕获了变量值。


5.1 单步执行(StepRequest)

调试不止于断点,还需要单步执行。在断点处创建 StepRequest

public void enableStepRequest(VirtualMachine vm, BreakpointEvent event) {
    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();    
    }
}

STEP_LINE:按行单步
STEP_OVER:跳过方法调用(不进入)


5.2 单步事件处理(StepEvent)

在主循环中添加对 StepEvent 的处理:

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

单步执行输出

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.

✅ 完美!我们实现了从第 9 行开始的逐行单步,并实时查看变量变化。


6. 输出被调试程序的打印内容

你会发现 System.out.println 的内容没有出现在调试器输出中。这是因为它们属于 debuggee 的独立输出流。

我们可以在 finally 块中读取:

finally {
    if (vm != null && vm.process() != null) {
        try (InputStreamReader reader = new InputStreamReader(vm.process().getInputStream());
             OutputStreamWriter writer = new OutputStreamWriter(System.out)) {
            char[] buf = new char[512];
            int len;
            while ((len = reader.read(buf)) != -1) {
                writer.write(buf, 0, len);
            }
            writer.flush();
        }
    }
}

最终输出(包含 println)

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

✅ 踩坑提示:vm.process() 仅在 LaunchingConnector 启动时可用,远程调试时不适用。


7. 总结

本文带你从零实现了一个基于 JDI 的简易调试器,核心要点回顾:

JDI 是 JPDA 的高层 API,适合开发调试工具前端
调试流程 = 连接 + 事件监听 + 断点 + 变量查看 + 单步执行
必须包含 tools.jar-g 编译选项,否则无法获取调试信息
事件驱动模型:通过 EventQueue 处理 ClassPrepareEventBreakpointEventStepEvent

虽然这只是 JDI 的冰山一角,但已足够理解主流 IDE 调试功能的底层原理。如果你想深入,建议阅读 JDI 官方文档 或查看 GitHub 示例代码

🔧 接下来你可以尝试:

  • 支持方法调用(STEP_INTO
  • 添加异常断点
  • 实现变量修改(setValue
  • 远程调试(AttachingConnector

JDI 的能力远不止于此,掌握它,你就能打造自己的调试利器。


原始标题:An Intro to the Java Debug Interface (JDI) | Baeldung