(给ImportNew加星标,提高Java技能)
一、原理分析
1.2 IDEA 和 Eclipse 调试原理
编辑器作为客户端和服务器程序通过暴露的监听端口建立 socket 连接。
IDE 客户端将断点位置创建了断点事件,通过 JDI 接口传给了服务端(程序端)的 VM,VM 调用 suspend 将 VM 挂起。
VM 挂起之后,将客户端需要获取的 VM 信息返回给客户端,返回之后 VM resume 恢复其运行状态。
客户端获取到 VM 返回的信息之后,可以通过不同的方式进行展示。
1.3 架构体系
JPDA 定义了一个完整独立的体系,它由三个相对独立的层次共同组成,而且规定了它们三者之间的交互方式,或者说定义了它们通信的接口。这三个层次由低到高分别是 Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP)以及 Java 调试接口(JDI)。
这三个模块把调试过程分解成几个很自然的概念:调试者(debugger)和被调试者(debuggee),以及他们中间的通信器。被调试者运行于我们想调试的 Java 虚拟机之上,它可以通过 JVMTI 这个标准接口,监控当前虚拟机的信息;调试者定义了用户可使用的调试接口,通过这些接口,用户可以对被调试虚拟机发送调试命令,同时调试者接受并显示调试结果。
在调试者和被调试着之间,调试命令和调试结果,都是通过 JDWP 的通讯协议传输的。所有的命令被封装成 JDWP 命令包,通过传输层发送给被调试者,被调试者接收到 JDWP 命令包后,解析这个命令并转化为 JVMTI 的调用,在被调试者上运行。类似的,JVMTI 的运行结果,被格式化成 JDWP 数据包,发送给调试者并返回给 JDI 调用。而调试器开发人员就是通过 JDI 得到数据,发出指令。
如上图所示 JPDA 由三层组成:
JVM TI - Java VM 工具接口。定义 VM 提供的调试服务。
JDWP - Java 调试通信协议。定义被调试者和调试器进程之间的通信。
JDI - Java 调试接口。定义一个高级 Java 语言接口,工具开发人员可以轻松地使用它来编写远程调试器应用程序。
通过 JPDA 这套接口,我们就可以开发自己的调试工具。通过这些 JPDA 提供的接口和协议,调试器开发人员就能根据特定开发者的需求,扩展定制 Java 调试应用程序。
前面我们提到的 IDE 调试工具都是基于 JPDA 体系开发的,区别仅仅在于它们可能提供了不同的图形界面、具有一些不同的自定义功能。
另外,我们要注意的是,JPDA 是一套标准,任何的 JDK 实现都必须完成这个标准。因此,通过 JPDA 开发出来的调试工具先天具有跨平台、不依赖虚拟机实现、JDK 版本无关等移植优点。因此大部分的调试工具都是基于这个体系的。
二、远程调试实例
1、构建一个 Spring Boot 的 WEB 项目。当前所选择的 Spring Boot 版本是 2.3.0.RELEASE,对应的 Tomcat 版本是 9.X。
2、打包该 Spring Boot 项目。开发应用程序端口为 9999。将该程序部署到 Linux 服务器上,可以是 JAR 包方式也可以 Docker 的方式,远程调试和这个没有关系。
3、部署程序的代码。参考如下,就是一个简单的请求处理打印输出信息。
/**
* 测试程序
* @author zhangyu
* @date 2022/2/17
*/
public class DebuggerApplication {
public static void main(String[] args) {
SpringApplication.run(DebuggerApplication.class, args);
}
public String test(){
System.out.println(111);
System.out.println(222);
return "OK";
}
}
4、部署程序启动参数如下
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8888 -jar debugger-0.0.1-SNAPSHOT.jar
其中 address=8888 表示开启 8888 端口作为远程调试的 Socket 通信端口。
如果是部署在 Tomcat 下的普通 Web 项目,参考如下:
1)小于 Tomcat 9 版本
Tomcat 中 bin/catalina.sh 中增加
CATALINA_OPTS='-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=18006'
如下图所示:
2)大于等于 Tomcat 9 版本
Tomcat 中 bin/catalina.sh 中的 JPDA_ADDRESS="localhost:8000" 这一句中的 localhost 修改为 0.0.0.0(允许所有 IP 连接到 8000 端口,而不仅是本地)。8000 是端口,端口号可以任意修改成没有占用的即可,如下图所示:
5、测试部署的程序正常后,下面构建客户端远程调试,当前以 IDEA 工具作为客户端
参考:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888
Host:远程服务器地址
Port:远程服务器开放的调试通信端口,非应用端口
测试接口:http://XXX:9999/test。注意本地代码需要和远程部署程序一致。通过上图可以看到客户端设置断点已经生效,其中在客户端执行了一个调试输出,这个是自定义输出的内容服务器程序并没有,在执行后右侧的服务器控制台日志输出了该信息,因此远程 Debug 是正常通信和处理的。
三、调试参数详解
-Xdebug :启用调试特性
-Xrunjdwp: 在目标 VM 中加载 JDWP 实现。它通过传输和 JDWP 协议与独立的调试器应用程序通信。下面介绍一些特定的子选项
-Djava.compiler=NONE:禁止 JIT 编译器的加载
transport :传输方式,有 socket 和 shared memory 两种,我们通常使用 socket(套接字)传输,但是在 Windows 平台上也可以使用shared memory(共享内存)传输。
server(y/n):VM 是否需要作为调试服务器执行
address:调试服务器的端口号,客户端用来连接服务器的端口号
suspend(y/n):值是 y 或者 n,若为 y,启动时候自己程序的 VM 将会暂停(挂起),直到客户端进行连接,若为 n,自己程序的 VM 不会挂起
从 Java V5 开始,您可以使用 -agentlib:jdwp 选项,而不是 -Xdebug 和 -Xrunjdwp。但如果连接到 V5 以前的 VM,只能选择 -Xdebug 和 -Xrunjdwp。下面简单描述 -Xrunjdwp 子选项。
四、JDI 工具代码实践
4.1 JDI 技术架构
参考:https://segmentfault.com/a/1190000040469952/en
创建一个 Spring Boot 的 Web 项目,提供一个简单的测试接口,并在测试方法中提供一些方法参数变量和局部变量作为后面的调试测试用。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
public class DebuggerApplication {
public static void main(String[] args) {
SpringApplication.run(DebuggerApplication.class, args);
}
public String test(String name){
System.out.println("进入方法");
int var=100;
System.out.println(name);
System.out.println(var);
System.out.println("方法结束");
return "OK";
}
}
项目启动配置参考,需要启用 Debug 配置:
开发调试器需要 JNI 工具支持,JDI 操作的 API 工具在 tools.jar 中,需要在 CLASSPATH 中添加 /lib/tools.jar。
import com.sun.jdi.*;
import com.sun.jdi.connect.AttachingConnector;
import com.sun.jdi.connect.Connector;
import com.sun.jdi.event.*;
import com.sun.jdi.request.BreakpointRequest;
import com.sun.jdi.request.EventRequest;
import com.sun.jdi.request.EventRequestManager;
import com.sun.tools.jdi.SocketAttachingConnector;
import java.util.List;
import java.util.Map;
/**
* 通过JNI工具测试Debug
* @author zhangyu
* @date 2022/2/20
*/
public class TestDebugVirtualMachine {
private static VirtualMachine vm;
public static void main(String[] args) throws Exception {
//获取SocketAttachingConnector,连接其它JVM称之为附加(attach)操作
VirtualMachineManager vmm = Bootstrap.virtualMachineManager();
List<AttachingConnector> connectors = vmm.attachingConnectors();
SocketAttachingConnector sac = null;
for(AttachingConnector ac : connectors) {
if(ac instanceof SocketAttachingConnector) {
sac = (SocketAttachingConnector) ac;
}
}
assert sac != null;
//设置好主机地址,端口信息
Map<String, Connector.Argument> arguments = sac.defaultArguments();
Connector.Argument hostArg = arguments.get("hostname");
Connector.Argument portArg = arguments.get("port");
hostArg.setValue("127.0.0.1");
portArg.setValue(String.valueOf(8800));
//进行连接
vm = sac.attach(arguments);
//相应的请求调用通过requestManager来完成
EventRequestManager eventRequestManager = vm.eventRequestManager();
//创建一个代码判断,因此需要获取相应的类,以及具体的断点位置,即相应的代码行。
ClassType clazz = (ClassType) vm.classesByName("com.zy.debugger.DebuggerApplication").get(0);
//设置断点代码位置
Location location = clazz.locationsOfLine(22).get(0);
//创建新断点并设置阻塞模式为线程阻塞,即只有当前线程被阻塞
BreakpointRequest breakpointRequest = eventRequestManager.createBreakpointRequest(location);
//设置阻塞并启动
breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
breakpointRequest.enable();
//获取vm的事件队列
EventQueue eventQueue = vm.eventQueue();
while(true) {
//不断地读取事件并处理断点记录事件
EventSet eventSet = eventQueue.remove();
EventIterator eventIterator = eventSet.eventIterator();
while(eventIterator.hasNext()) {
Event event = eventIterator.next();
execute(event);
}
//将相应线程resume,表示继续运行
eventSet.resume();
}
}
/**
* 处理监听到事件
* @author zhangyu
* @date 2022/2/20
*/
public static void execute(Event event) throws Exception {
//获取的event为一个抽象的事件记录,可以通过类型判断转型为具体的事件,这里我们转型为BreakpointEvent,即断点记录,
BreakpointEvent breakpointEvent = (BreakpointEvent) event;
//并通过断点处的线程拿到线程帧,进而获取相应的变量信息,并打印记录。
ThreadReference threadReference = breakpointEvent.thread();
StackFrame stackFrame = threadReference.frame(0);
List<LocalVariable> localVariables = stackFrame.visibleVariables();
//输出当前线程栈帧保存的变量数据
localVariables.forEach(t -> {
Value value = stackFrame.getValue(t);
System.out.println("local->" + value.type() + "," + value.getClass() + "," + value);
});
}
}
3) 代码分析
通过 Bootstrap.virtualMachineManager() 获取连接器。客户端即通过相应的 connector 进行连接,配置服务器程序 IP 地址和端口,连接后获取对应服务器的 VM 信息。
通过 VirtualMachine 获取类信息,通过遍历获取的类集合定位到目标 debug 的类文件上。
对目标类代码特定位置设置并启用断点。
记录断点信息,阻塞服务器线程,并根据对应事件获取相应的信息。
执行 event.resume 释放断点,服务器程序继续运行。
4) 运行测试
启动服务器程序,即上面的 Spring Boot 的 web 项目。本地以 debug 方式启动调试器代码,待会在这个位置看看获取的信息,同时避免直接释放断点。
设置断点位置为 DebuggerApplication 类的第 22 行。
启动后测试该接口,可以发现服务器程序控制台打印了如下结果。第 22 行还没有执行。
此时,在观察调试器程序。可以看到获取到了服务器程序栈帧的数据:
释放断点,服务器正常运行完本次请求处理流程:
转自:ZWZhangYu,
链接:blog.csdn.net/Octopus21/article/details/123049808
- EOF -
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
点赞和在看就是最大的支持❤️