原文地址:https://alphahinex.github.io/2021/04/11/java-app-remote-debugging/

description: "本地環(huán)境無(wú)法重現(xiàn)問題?試試遠(yuǎn)程調(diào)試"
date: 2021.04.11 10:26
categories:
- Java
tags: [Java, Debug]
keywords: Java, Remote Debugging, JPDA, JDWP, 遠(yuǎn)程調(diào)試, IEEE, SCI, IDEA, JDB
軟件開發(fā)會(huì)時(shí)經(jīng)常會(huì)遇到這樣的場(chǎng)景:
- 現(xiàn)場(chǎng)反饋的問題,在本地環(huán)境無(wú)法重現(xiàn),可能需要將現(xiàn)場(chǎng)數(shù)據(jù)庫(kù)導(dǎo)回來(lái)才能重現(xiàn)問題
- 生產(chǎn)環(huán)境中的服務(wù)無(wú)法直接從本地開發(fā)環(huán)境進(jìn)行連接,進(jìn)而無(wú)法使用本地代碼進(jìn)行調(diào)試
在上面的場(chǎng)景中,無(wú)論是將現(xiàn)場(chǎng)庫(kù)導(dǎo)出,還是開通生產(chǎn)環(huán)境服務(wù)的訪問權(quán)限,都是非常困難且不安全的。
本文將介紹一種由 Java 平臺(tái)提供的,遠(yuǎn)程調(diào)試 Java 應(yīng)用的方法。
JPDA
JPDA(Java Platform Debugging Architecture)是一個(gè)多層調(diào)試架構(gòu),支持在不同操作系統(tǒng)、虛擬機(jī)及 JDK 版本中創(chuàng)建調(diào)試程序。
JPDA 的 架構(gòu)圖 如下:
Components Debugger Interfaces
/ |--------------|
/ | VM |
debuggee ----( |--------------| <------- JVM TI - Java VM Tool Interface
\ | back-end |
\ |--------------|
/ |
comm channel -( | <--------------- JDWP - Java Debug Wire Protocol
\ |
|--------------|
| front-end |
|--------------| <------- JDI - Java Debug Interface
| UI |
|--------------|
架構(gòu)由三層組成:
- JVM TI - Java VM Tool Interface:定義了由虛擬機(jī)提供的調(diào)試服務(wù)
- JDWP - Java Debug Wire Protocol:定義了 debuggee(調(diào)試應(yīng)用服務(wù)端)和 debugger(調(diào)試服務(wù)客戶端)進(jìn)程之間的通訊協(xié)議
- JDI - Java Debug Interface:定義了高層次的 Java 語(yǔ)言接口,使得工具開發(fā)者可以方便的編寫遠(yuǎn)程調(diào)試應(yīng)用
由上可知,Java 應(yīng)用遠(yuǎn)程調(diào)試時(shí),需先開啟服務(wù)端的遠(yuǎn)程調(diào)試服務(wù),再通過(guò) debugger 應(yīng)用進(jìn)行連接,實(shí)現(xiàn)遠(yuǎn)程調(diào)試。
服務(wù)端
服務(wù)端開啟遠(yuǎn)程調(diào)試功能時(shí),需在啟動(dòng)時(shí)增加啟動(dòng)參數(shù),不同 JDK 版本的啟動(dòng)參數(shù)略有不同:
# JDK 1.3.x or earlier
-Xnoagent -Djava.compiler=NONE -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
# JDK 1.4.x
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005
# JDK 5 - 8
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
# JDK 9 or later
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
注意,上述啟動(dòng)參數(shù)要加到 java 命令參數(shù)的最前面,即可以直接加到 java 命令后面,之后再加其他參數(shù)。以 JDK 8 為例,啟動(dòng)命令如下所示:
$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -jar demo.jar
以 JDK 8 的啟動(dòng)參數(shù)為例,-agentlib:jdwp 表明使用 JDWP 協(xié)議,后面包括了 JDWP 的幾個(gè)重要參數(shù):
- transport:有兩個(gè)內(nèi)置類型,
dt_socket(使用 socket 接口)和dt_shmem(使用共享內(nèi)存)。共享內(nèi)存類型僅在本機(jī)調(diào)試時(shí)使用 - server:
y表明此虛擬機(jī)在調(diào)試中扮演服務(wù)端角色 - suspend:是否在客戶端連接前掛起主進(jìn)程。為不影響服務(wù)正常使用,通常可以設(shè)置為
n;當(dāng)需要調(diào)試啟動(dòng)過(guò)程時(shí),可設(shè)置為y - address:指定遠(yuǎn)程調(diào)試端口
服務(wù)端調(diào)試模式啟用時(shí),日志中會(huì)多出一行類似如下內(nèi)容:
Listening for transport dt_socket at address: 5005
客戶端
IDEA
服務(wù)端開啟調(diào)試模式后,可通過(guò) IDEA 方便的進(jìn)行遠(yuǎn)程連接及調(diào)試。
首先 Edit Configurations...:

然后在 Run/Debug Configurations 中創(chuàng)建 Remote JVM Debug:

填寫 Host、Port,選擇 Use module classpath:

配置完成后,以 debug 方式啟動(dòng)此服務(wù):

連接后可以看到類似提示:

之后即可使用與本地調(diào)試一樣的方式,調(diào)試遠(yuǎn)程服務(wù)。
注意:添加斷點(diǎn)時(shí),可以多試幾個(gè)位置,remote 的 class 和本地的源碼可能不完全一致,所以斷點(diǎn)位置可能也不完全一致
再注意:同時(shí)只能接受一個(gè)客戶端進(jìn)行 remote debugging,無(wú)法多人同時(shí)以此方式進(jìn)行遠(yuǎn)程調(diào)試
JDB
一般離岸開發(fā)的項(xiàng)目,開發(fā)人員不在項(xiàng)目實(shí)施地,現(xiàn)場(chǎng)可能僅有實(shí)施運(yùn)維人員。現(xiàn)場(chǎng)人員能連到線上環(huán)境但沒有源碼及 IDEA 等開發(fā)工具;開發(fā)人員有調(diào)試環(huán)境,但與線上環(huán)境網(wǎng)絡(luò)不通,此種情況下,還有沒有其他的遠(yuǎn)程調(diào)試方法呢?
JDK 中,提供了一個(gè)名為 jdb 的 Java Debugger,可以以命令行的方式連接至 debuggee 進(jìn)行調(diào)試。
由下面的 架構(gòu)圖 可知,JDB 是 JDI 的一種實(shí)現(xiàn)。

下面列舉一些 JDB 的常用操作,更多操作方式可參考幫助文檔或 JDB - Quick Guide。
連接 Debuggee
$ jdb -attach remote:32738 -sourcepath ./src/main/java
設(shè)置未捕獲的java.lang.Throwable
設(shè)置延遲的未捕獲的java.lang.Throwable
正在初始化jdb...
>
不通過(guò) -sourcepath 指定源碼路徑也可以進(jìn)行調(diào)試,只是不會(huì)顯示出斷點(diǎn)行對(duì)應(yīng)的源碼內(nèi)容。
設(shè)置斷點(diǎn)(方法上)
> stop in cn.hinex.xxx.demo.DemoController.demo()
設(shè)置斷點(diǎn)cn.hinex.xxx.demo.DemoController.demo()
>
運(yùn)行至此方法時(shí),JDB 中會(huì)提示
斷點(diǎn)命中: "線程=http-nio-8888-exec-4", cn.hinex.xxx.demo.DemoController.demo(), 行=16 bci=0
16 StringBuffer msg = new StringBuffer("hello");
http-nio-8888-exec-4[1]
列出當(dāng)前斷點(diǎn)所在位置
http-nio-8888-exec-4[1] list
12 RedisTemplate redisTemplate;
13
14 @GetMapping
15 public String demo() {
16 => StringBuffer msg = new StringBuffer("hello");
17 for (Object clientInfo : redisTemplate.getClientList()) {
18 msg.append(clientInfo.toString()).append("\r\n");
19 }
20 for (Object key : redisTemplate.keys("*")) {
21 msg.append(key.toString()).append("\r\n");
顯示堆棧
http-nio-8888-exec-4[1] where
[1] cn.hinex.xxx.demo.DemoController.demo (DemoController.java:16)
[2] sun.reflect.NativeMethodAccessorImpl.invoke0 (本機(jī)方法)
[3] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62)
[4] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43)
[5] java.lang.reflect.Method.invoke (Method.java:498)
[6] org.springframework.web.method.support.InvocableHandlerMethod.doInvoke (InvocableHandlerMethod.java:209)
[7] org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest (InvocableHandlerMethod.java:136)
[8] org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle (ServletInvocableHandlerMethod.java:102)
[9] org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod (RequestMappingHandlerAdapter.java:894)
[10] org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal (RequestMappingHandlerAdapter.java:800)
……
運(yùn)行至下一步
http-nio-8888-exec-4[1] step
>
已完成的步驟: "線程=http-nio-8888-exec-4", cn.hinex.xxx.demo.DemoController.demo(), 行=17 bci=10
17 for (Object clientInfo : redisTemplate.getClientList()) {
在指定行設(shè)置斷點(diǎn)
http-nio-8888-exec-4[1] stop at cn.hinex.xxx.demo.DemoController:24
設(shè)置斷點(diǎn)cn.hinex.xxx.demo.DemoController:24
繼續(xù)運(yùn)行
http-nio-8888-exec-4[1] cont
>
斷點(diǎn)命中: "線程=http-nio-8888-exec-4", cn.hinex.xxx.demo.DemoController.demo(), 行=24 bci=109
24 return str;
可通過(guò) list 命令觀察此時(shí)斷點(diǎn)位置:
http-nio-8888-exec-4[1] list
20 for (Object key : redisTemplate.keys("*")) {
21 msg.append(key.toString()).append("\r\n");
22 }
23 String str = msg.toString();
24 => return str;
25 }
26
27 }
打印變量值
http-nio-8888-exec-4[1] print str
str = "hello"
修改變量值
http-nio-8888-exec-4[1] set str = "hinex"
str = "hinex" = "hinex"
其他
安全性問題
在使用遠(yuǎn)程調(diào)試時(shí),不能忽略由此所帶來(lái)的的性能影響及安全性問題。
有興趣的讀者可以閱讀一下 Hacking the Java Debug Wire Protocol – or – “How I met your Java debugger”,文中偽造了一個(gè)調(diào)試程序的客戶端,并通過(guò) java.lang.Runtime 類獲取到 getRuntime() 方法的實(shí)例,之后便可執(zhí)行運(yùn)行此 java 應(yīng)用的用戶所擁有權(quán)限執(zhí)行的命令。
也可以進(jìn)行一下簡(jiǎn)單的驗(yàn)證,在得知一個(gè)服務(wù)的 remote debugging 端口后,在一個(gè)會(huì)被頻繁調(diào)用的類上(如 java.net.ServerSocket.accept())設(shè)置斷點(diǎn),進(jìn)入斷點(diǎn)后在 jdb 中執(zhí)行 print java.lang.Runtime.getRuntime().exec("touch /home/testfile"),如果運(yùn)行此 java 應(yīng)用的用戶擁有在 /home 路徑下創(chuàng)建文件的權(quán)限,即可在服務(wù)器上完成此文件的創(chuàng)建。
故通常情況下,不應(yīng)該開啟調(diào)試模式。必須要開啟時(shí),也應(yīng)盡快完成調(diào)試,之后將調(diào)試模式關(guān)閉,并不要使用常用的端口,如 5005 等。
下載 IEEE 論文
查詢資料時(shí),如需查看 IEEE 中的論文,如 Multi-party collaborative debug service for Java application,可以試試 這個(gè),得到 這個(gè)。