JPDA(Java Platform Debugger Architecture) 是Java 平台偵錯體系結構的縮寫,透過JPDA 提供的API,開發人員可以方便且靈活的建立Java 偵錯應用程式。
JPDA 主要由三個部分組成:Java 虛擬機器工具介面(JVMTI),Java 偵錯線協定(JDWP),以及 Java 偵錯介面(JDI)。
Java 程式都是在Java 虛擬機器上運作的,我們要除錯Java 程序,事實上需要向Java 虛擬機器要求目前執行狀態的狀態,並對虛擬機器發出一定的指令,設定一些回呼等等,那麼Java 的調試體系,就是虛擬機器的一整套用來調試的工具和介面。
1:編輯器作為客戶端和伺服器程式透過暴露的監聽連接埠建立socket連線
2:IDE客戶端將斷點位置建立了斷點事件透過JDI 介面傳給了服務端(程式端)的VM,VM 呼叫suspend 將VM 掛起
3:VM 掛起之後將客戶端需要取得的VM 訊息返回給客戶端,返回之後VM resume 恢復其運行狀態
4:客戶端獲取到VM 返回的資訊之後可以透過不同的方式進行展示
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】建構一個SpringBoot的WEB專案。我們目前正在使用的SpringBoot版本為2.3.0.RELEASE。對應的tomcat版本是9.X。
將該SpringBoot專案進行打包,並將應用程式的連接埠設定為9999。部署此程式至Linux伺服器,無論採用JAR套件或Docker方式,與遠端偵錯無關。
【3】部署程式的程式碼參考如下,就是一個簡單的請求處理列印輸出訊息
/** * 测试程序 * @author zhangyu * @date 2022/2/17 */ @SpringBootApplication @RestController public class DebuggerApplication { public static void main(String[] args) { SpringApplication.run(DebuggerApplication.class, args); } @GetMapping("/test") 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項目,參考如下:
小於tomcat9 版本
tomcat 中 bin/catalina.sh 中增加 CATALINA_OPTS=‘-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=18006’
如下图所示:
大于等于 tomcat9 版本
tomcat 中 bin/catalina.sh 中的 JPDA_ADDRESS=“localhost:8000” 这一句中的localhost修改为0.0.0.0(允许所有ip连接到8000端口,而不仅是本地)8000是端口,端口号可以任意修改成没有占用的即可
如下图所示:
【5】测试部署的程序正常后,下面构建客户端远程调试,当前以IDEA工具作为客户端
参考:
【1】-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888
【2】Host:远程服务器地址
【3】Port:远程服务器开放的调试通信端口,非应用端口
测试接口:http://XXX:9999/test。注意本地代码需要和远程部署程序一致。
通过上图可以看到客户端设置断点已经生效,其中在客户端执行了一个调试输出,这个是自定义输出的内容服务器程序并没有,在执行后右侧的服务器控制台日志输出了该信息,因此远程Debug是正常通信和处理的。
(一)调试参数详解
-Xdebug
:启用调试特性
-Xrunjdwp
:在目标 VM 中加载 JDWP 实现。它通过传输和 JDWP 协议与独立的调试器应用程序通信。下面介绍一些特定的子选项
从 Java V5 开始,您可以使用 -agentlib:jdwp 选项,而不是 -Xdebug 和 -Xrunjdwp。如果连接到VM版本低于V5的情况下,只能使用 -Xdebug 和 -Xrunjdwp 选项。下面简单描述 -Xrunjdwp 子选项。
-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 不会挂起
(1)被调试程序
创建一个SpringBoot的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; @SpringBootApplication @RestController public class DebuggerApplication { public static void main(String[] args) { SpringApplication.run(DebuggerApplication.class, args); } @GetMapping("/test") 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配置
(2)自定义调试器代码
开发调试器需要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)代码分析
【1】通过Bootstrap.virtualMachineManager();获取连接器,客户端即通过相应的connector进行连接,配置服务器程序ip地址和端口,连接后获取对应服务器的VM信息。
定位目标debug的类文件,可通过遍历获取的类集合并结合VirtualMachine获取类信息实现
【3】对目标类代码特定位置设置并启用断点
【4】记录断点信息,阻塞服务器线程,并根据对应事件获取相应的信息
【5】执行event.resume释放断点,服务器程序继续运行
(4)运行测试
启动 SpringBoot 的 web 项目,也就是服务器程序。启动调试器代码时使用debug模式,并在该位置查看所获取的信息,同时避免直接释放断点。
【2】設定斷點位置為DebuggerApplication類別的第22行
【3】啟動後測試該接口,可以發現伺服器程式控制台列印如下結果。第22行還沒有執行。
【4】此時,在觀察偵錯器程式。可以看到獲取到了伺服器程式堆疊幀的資料
【5】釋放斷點,伺服器正常運行完本次請求處理流程
#
以上是Java平台調試系統原理是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!