Java 應(yīng)用遠(yuǎn)程調(diào)試

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

cover

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)由三層組成:

  1. JVM TI - Java VM Tool Interface:定義了由虛擬機(jī)提供的調(diào)試服務(wù)
  2. JDWP - Java Debug Wire Protocol:定義了 debuggee(調(diào)試應(yīng)用服務(wù)端)和 debugger(調(diào)試服務(wù)客戶端)進(jìn)程之間的通訊協(xié)議
  3. 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...

Edit Configurations

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

Remote JVM Debug

填寫 Host、Port,選擇 Use module classpath

Configuration

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

Debug

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

Connected

之后即可使用與本地調(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 的常用操作,更多操作方式可參考幫助文檔或 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è)。

參考資料

A Practical Guide to Java Remote Debugging

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 集群前后臺(tái)協(xié)議需要做一些修改,我負(fù)責(zé)jdbc這邊的修改。按照協(xié)議內(nèi)容修改完代碼之后卻面臨一個(gè)測(cè)試的問題:修改后的后...
    德彪閱讀 4,282評(píng)論 0 2
  • 本文主要基于一篇英文原作翻譯而成,刪減部分無(wú)用文字,添加了必要的注解和補(bǔ)充。 *英文原文是一篇對(duì)遠(yuǎn)程調(diào)試講解很通俗...
    曲水流觴TechRill閱讀 52,601評(píng)論 6 45
  • 程序開發(fā)過(guò)程中,debug 是必不可少的一部分,它能幫助我們及時(shí)發(fā)現(xiàn)一些不易察覺的 bug,但并不是所有的 bug...
    小魚愛小蝦閱讀 4,082評(píng)論 1 3
  • 遠(yuǎn)程debug調(diào)試java代碼 日常環(huán)境和預(yù)發(fā)環(huán)境遇到問題時(shí),可以用遠(yuǎn)程調(diào)試的方法本地打斷點(diǎn),在本地調(diào)試。生產(chǎn)環(huán)境...
    Statham_Jessie閱讀 604評(píng)論 0 0
  • 前言 一直以來(lái),產(chǎn)線環(huán)境的問題總是很棘手。由于數(shù)據(jù)集、業(yè)務(wù)狀態(tài)、壓力規(guī)模等問題,很難在開發(fā)環(huán)境進(jìn)行模擬。即使可以建...
    本然酋長(zhǎng)閱讀 1,110評(píng)論 0 0

友情鏈接更多精彩內(nèi)容