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 提供的一套用于调试的标准化接口与协议集合。它由三个层次组成,分别对应调试的不同阶段:
JVMTI(Java Virtual Machine Tool Interface)
- JVM 层的接口,允许工具直接监控和控制 JVM 的运行状态
- 比如:获取线程、内存、类加载信息等
JDWP(Java Debug Wire Protocol)
- 定义了调试器(debugger)与被调试程序(debuggee)之间的通信协议
- 通常通过 socket 或共享内存传输调试指令和数据
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 设计
- 所有操作都通过
VirtualMachine
、Event
、StackFrame
等对象完成
⚠️ 注意: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
处理 ClassPrepareEvent
、BreakpointEvent
、StepEvent
虽然这只是 JDI 的冰山一角,但已足够理解主流 IDE 调试功能的底层原理。如果你想深入,建议阅读 JDI 官方文档 或查看 GitHub 示例代码。
🔧 接下来你可以尝试:
- 支持方法调用(
STEP_INTO
)- 添加异常断点
- 实现变量修改(
setValue
)- 远程调试(
AttachingConnector
)
JDI 的能力远不止于此,掌握它,你就能打造自己的调试利器。