Java平臺調試體系原理分析和實踐整理 遠程Debug
一、原理分析
(一)介紹
JPDA(Java Platform Debugger Architecture) 是 Java 平臺調試體系結構的縮寫,通過 JPDA 提供的 API,開發(fā)人員可以方便靈活的搭建 Java 調試應用程序。
JPDA 主要由三個部分組成:Java 虛擬機工具接口(JVMTI),Java 調試線協(xié)議(JDWP),以及 Java 調試接口(JDI)。
Java 程序都是運行在 Java 虛擬機上的,我們要調試 Java 程序,事實上就需要向 Java 虛擬機請求當前運行態(tài)的狀態(tài),并對虛擬機發(fā)出一定的指令,設置一些回調等等,那么 Java 的調試體系,就是虛擬機的一整套用于調試的工具和接口。
(二)IDEA和eclipse 調試原理為
1:編輯器作為客戶端和服務器程序通過暴露的監(jiān)聽端口建立socket連接
2:IDE客戶端將斷點位置創(chuàng)建了斷點事件通過 JDI 接口傳給了 服務端(程序端)的 VM,VM 調用 suspend 將 VM 掛起
3:VM 掛起之后將客戶端需要獲取的 VM 信息返回給客戶端,返回之后 VM resume 恢復其運行狀態(tài)
4:客戶端獲取到 VM 返回的信息之后可以通過不同的方式進行展示
(三)架構體系
JPDA 定義了一個完整獨立的體系,它由三個相對獨立的層次共同組成,而且規(guī)定了它們?nèi)咧g的交互方式,或者說定義了它們通信的接口。
這三個層次由低到高分別是 Java 虛擬機工具接口(JVMTI),Java 調試線協(xié)議(JDWP)以及 Java 調試接口(JDI)。
這三個模塊把調試過程分解成幾個很自然的概念:調試者(debugger)和被調試者(debuggee),以及他們中間的通信器。
被調試者運行于我們想調試的 Java 虛擬機之上,它可以通過 JVMTI 這個標準接口,監(jiān)控當前虛擬機的信息;調試者定義了用戶可使用的調試接口,通過這些接口,用戶可以對被調試虛擬機發(fā)送調試命令,同時調試者接受并顯示調試結果。
在調試者和被調試著之間,調試命令和調試結果,都是通過 JDWP 的通訊協(xié)議傳輸?shù)?。所有的命令被封裝成 JDWP 命令包,通過傳輸層發(fā)送給被調試者,被調試者接收到 JDWP 命令包后,解析這個命令并轉化為 JVMTI 的調用,在被調試者上運行。
類似的,JVMTI 的運行結果,被格式化成 JDWP 數(shù)據(jù)包,發(fā)送給調試者并返回給 JDI 調用。而調試器開發(fā)人員就是通過 JDI 得到數(shù)據(jù),發(fā)出指令。
如上圖所示JPDA 由三層組成:
JVM TI
- Java VM 工具接口。定義 VM 提供的調試服務。JDWP
- Java 調試通信協(xié)議。定義被調試者和調試器進程之間的通信。JDI
- Java 調試接口。定義一個高級 Java 語言接口,工具開發(fā)人員可以輕松地使用它來編寫遠程調試器應用程序。
通過 JPDA 這套接口,我們就可以開發(fā)自己的調試工具。通過這些 JPDA 提供的接口和協(xié)議,調試器開發(fā)人員就能根據(jù)特定開發(fā)者的需求,擴展定制 Java 調試應用程序。
前面我們提到的 IDE 調試工具都是基于 JPDA 體系開發(fā)的,區(qū)別僅僅在于它們可能提供了不同的圖形界面、具有一些不同的自定義功能。
另外,我們要注意的是,JPDA 是一套標準,任何的 JDK 實現(xiàn)都必須完成這個標準,因此,通過 JPDA 開發(fā)出來的調試工具先天具有跨平臺、不依賴虛擬機實現(xiàn)、JDK 版本無關等移植優(yōu)點,因此大部分的調試工具都是基于這個體系的。
二、遠程調試實例
【1】構建一個SpringBoot的WEB項目。當前所選擇的SpringBoot版本是2.3.0.RELEASE。對應的tomcat版本是9.X。
【2】打包該SpringBoot項目,開發(fā)應用程序端口為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】部署程序啟動參數(shù)如下
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。注意本地代碼需要和遠程部署程序一致。
通過上圖可以看到客戶端設置斷點已經(jīng)生效,其中在客戶端執(zhí)行了一個調試輸出,這個是自定義輸出的內(nèi)容服務器程序并沒有,在執(zhí)行后右側的服務器控制臺日志輸出了該信息,因此遠程Debug是正常通信和處理的。
(一)調試參數(shù)詳解
-Xdebug
:啟用調試特性-Xrunjdwp
:在目標 VM 中加載 JDWP 實現(xiàn)。它通過傳輸和 JDWP 協(xié)議與獨立的調試器應用程序通信。下面介紹一些特定的子選項
從 Java V5 開始,您可以使用 -agentlib:jdwp 選項,而不是 -Xdebug 和 -Xrunjdwp。但如果連接到 V5 以前的 VM,只能選擇 -Xdebug 和 -Xrunjdwp。下面簡單描述 -Xrunjdwp 子選項。
-Djava.compiler=NONE
: 禁止 JIT 編譯器的加載transport
: 傳輸方式,有 socket 和 shared memory 兩種,我們通常使用 socket(套接字)傳輸,但是在 Windows 平臺上也可以使用shared memory(共享內(nèi)存)傳輸。server(y/n)
: VM 是否需要作為調試服務器執(zhí)行address
: 調試服務器的端口號,客戶端用來連接服務器的端口號suspend(y/n)
:值是 y 或者 n,若為 y,啟動時候自己程序的 VM 將會暫停(掛起),直到客戶端進行連接,若為 n,自己程序的 VM 不會掛起
三、JDI工具代碼實踐
(一)JDI技術架構
(二)實踐案例
(1)被調試程序
創(chuàng)建一個SpringBoot的WEB項目,提供一個簡單的測試接口,并在測試方法中提供一些方法參數(shù)變量和局部變量作為后面的調試測試用。
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)自定義調試器代碼
開發(fā)調試器需要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(); //創(chuàng)建一個代碼判斷,因此需要獲取相應的類,以及具體的斷點位置,即相應的代碼行。 ClassType clazz = (ClassType) vm.classesByName("com.zy.debugger.DebuggerApplication").get(0); //設置斷點代碼位置 Location location = clazz.locationsOfLine(22).get(0); //創(chuàng)建新斷點并設置阻塞模式為線程阻塞,即只有當前線程被阻塞 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,表示繼續(xù)運行 eventSet.resume(); } } /** * 處理監(jiān)聽到事件 * @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(); //輸出當前線程棧幀保存的變量數(shù)據(jù) localVariables.forEach(t -> { Value value = stackFrame.getValue(t); System.out.println("local->" + value.type() + "," + value.getClass() + "," + value); }); } }
(3)代碼分析
【1】通過Bootstrap.virtualMachineManager();獲取連接器,客戶端即通過相應的connector進行連接,配置服務器程序ip地址和端口,連接后獲取對應服務器的VM信息。
【2】通過VirtualMachine獲取類信息,通過遍歷獲取的類集合定位到目標debug的類文件上
【3】對目標類代碼特定位置設置并啟用斷點
【4】記錄斷點信息,阻塞服務器線程,并根據(jù)對應事件獲取相應的信息
【5】執(zhí)行event.resume釋放斷點,服務器程序繼續(xù)運行
(4)運行測試
【1】啟動服務器程序,即上面的SpringBoot的web項目。本地以debug方式啟動調試器代碼,待會在這個位置看看獲取的信息,同時避免直接釋放了斷點。
【2】設置斷點位置為DebuggerApplication類的第22行
【3】啟動后測試該接口,可以發(fā)現(xiàn)服務器程序控制臺打印了如下結果。第22行還沒有執(zhí)行。
【4】此時,在觀察調試器程序??梢钥吹将@取到了服務器程序棧幀的數(shù)據(jù)
【5】釋放斷點,服務器正常運行完本次請求處理流程
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
java 實現(xiàn)截取字符串并按字節(jié)分別輸出實例代碼
這篇文章主要介紹了java 實現(xiàn)截取字符串并按字節(jié)分別輸出實例代碼的相關資料,需要的朋友可以參考下2017-03-03關于@OnetoMany關系映射的排序問題,使用注解@OrderBy
這篇文章主要介紹了關于@OnetoMany關系映射的排序問題,使用注解@OrderBy,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12IDEA 重新導入依賴maven 命令 reimport的方法
這篇文章主要介紹了IDEA 重新導入依賴maven 命令 reimport的相關知識,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04IntelliJ IDEA 常用設置(配置)吐血整理(首次安裝必需)
這篇文章主要介紹了IntelliJ IDEA 常用設置(配置)吐血整理(首次安裝必需),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-06-06Java實戰(zhàn)房屋租賃網(wǎng)的實現(xiàn)流程
讀萬卷書不如行萬里路,只學書上的理論是遠遠不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實現(xiàn)一個房屋租賃網(wǎng)站,大家可以在過程中查缺補漏,提升水平2021-11-11基于mybatis-plus-generator實現(xiàn)代碼自動生成器
這篇文章專門為小白準備了入門級mybatis-plus-generator代碼自動生成器,可以提高開發(fā)效率。文中的示例代碼講解詳細,感興趣的可以了解一下2022-05-05