Java Debug Interface(JDI)介绍
An Intro to the Java Debug Interface (JDI) | Baeldung
1. 概述
我们可能会想像IntelliJ IDEA和Eclipse这样大的IDE是如何实现调试特征的。这些工具极大依赖于Java平台调试架构(JPDA)。
在本文中,我们将讨论Java Debug Interface API(JDI),这种可以在JPDA向下获得的接口。
同时,我们会一步步写一个自定义的调试程序,让我们熟悉JDI接口。
2. 介绍JPDA
Java平台调试架构(JPDA)是一组设计得非常好得接口和协议集合,其目的是用于调试Java。
它提供了3种特别设计的接口,用于实现自定义的调试器,该调试器用于在桌面系统中的开发环境。
首先,Java虚拟机工具接口(JVMTI)帮助我们交互和控制应用程序的执行,这些应用程序都运行在JVM上。
然后,Java调试线协议(Java Debug Wire Protocol(JDWP))定义了在测试(debugggee)和调试器(debugger)之下运行的程序的协议。
最后,Java调试接口(JDI)用于实现调试器应用程序。
3. 什么是JDI?
Java调试器接口API是一组由Java提供的接口,其目的是实现调试器的前端。JDK是JPDA的最高层。
一个由JDI构成的调试器能够调试运行在任何JVM上的应用程序,只要这个JVM支持JPDA。同时,我们能够钩在调试的任何一层。
它提供了访问虚拟机的能力,并且它能访问调试器的变量。同时,它允许设置断点,单步调试,观察点和控制线程。
4. 安装
我们分别需要2个程序,一个debuggee和一个debugger,用于理解JDI的实现。
首先,我们要写一个简单的例子作为debuggee。
让我们创建一个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
首先,一个debugger需要一个连接器取建立一个与目标虚拟机之间的连接。
然后,我们需要设置一个debuggee作为连接器的主参数。最后,连接器会运行VM进行debug。
为了做这个,JDI提供了一个Bootstrap类,该类提供了一个LaunchingConnector实例。这个LaunchingConnector提供了一个默认参数的映射,在这里面,我们能够设置主参数。
因此,让我们添加一个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方法到JDIDebubggerExample用于调试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(debuggee)和JDIExampleDebugger(debugger):
javac -g -cp "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar"
com/baeldung/jdi/*.java
让我们讨论一下javac的细节。
没有这个选项的话,我们可能看到AbsentInformationException。
并且 -cp会添加tools.jar在classpath中去编译类。
**所有的JDI库都能在JDK的tools.jar下获得。**因此,要确保编译和执行的时候都要添加tools.jar在classpath下。
现在我们准备运行我们自定义的调试器JDIExampleDebugger:
java -cp "/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/lib/tools.jar:."
JDIExampleDebugger
注意tools.jar后面的":."。对于当前运行时,这将添加tools.jar在classpath之后(在windows下使用";.")。
4.2. Bootstrap和ClassPrepareRequest
执行调试器程序不会给你结果因为我们没有准备调试的类并且设置断点。
VirtualMachine类有eventRequestManager方法去创造不同的需求,例如ClassPrepareRequest,BreakpointRequest,和SetEventRequest。
所以,让我们添加enbaleClassPrepareRerqueset方法到JDIExampleDebugger类。
这将过滤JDIExampleDebuggee类和使用ClassPrepareRequest:
public void enableClassPrepareRequest(VirtualMachine vm) {
ClassPrepareRequest classPrepareRequest = vm.eventRequestManager().createClassPrepareRequest();
classPrepareRequest.addClassFilter(debugClass.getName());
classPrepareRequest.enable();
}
4.3. ClassPrepareEvent 和BreakpointRequest
一旦,对JDIExampleDebuggee的ClassPrepareReqeust使能后,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类,用于获得debuggee.JDI的所有可见变量的列表。
因此,让我们添加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 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.
耶!我们已经成功地调试了JDIExampleDebuggee类。同时,我们已经显示了在指定断点的位置(行6和行9)的变量的值
到此为止,我们的自定义debugger已经准备好了。
5.1. StepRequest
**调试也需要单步功能并且检查变量在下一步的状态。**因此,我们将创建一个在断点的单步调试需求。
当创建一个StepRequest的实例时,我们必须提供单步的size和深度。我们将分别定义定义STEP_LINE和STEP_OVER。
我们写一个函数去启用step需求。
简单来说,我们将开始在最后一个断点开始单步调试(行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时启动step需求。
if (event instanceof BreakpointEvent) {
debuggerInstance.enableStepRequest(vm, (BreakpointEvent)event);
}
5.2. StepEvent
与BreakPointEvent类似,我们也能够在StepEvent中看变量。
让我们更新main函数:
if (event instanceof StepEvent) {
debuggerInstance.displayVariables((StepEvent) event);
}
最后,我们执行这个debugger去看看当代码中有单步调试时变量的状态:
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的debugger并显示所有步骤上的变量。
6. 阅读执行输出
我们可能注意到JDIExampleDebuggee类的println语句并没有在debugger中输出。
如JDK文档所属,如果我们通过LaunchingConnector执行VM,它的输出和错误流必须被进程对象读到。
因此,让我们在主函数中加上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();
}
现在,执行debugger程序也会打印JDIExampleDebuggee类中的println输出:
Hi Everyone, Welcome to Java Platform Debugger Architecture
Today, we'll dive into Java Debug Interface
7. 总结
在本文中,我们研究了在the Java Platform Debugger Architecture (JPDA)下的Java Debug Interface(JDI) API。
同时,我们构建了一个自定义的debugger,该debugger使用了手工编写的基于JDI的接口。同时,我们也为这个debugger添加了单步调试功能。
作为介绍JDI的文章,推荐大家去看以下其他的在JDK API下的接口的实现。
所有的代码都可以在[Github]((https://github.com/eugenp/tutorials/tree/master/java-jdi)中获得。