遠程調試方式


使用特定JVM參數運行服務端代碼

要讓遠程服務器運行的代碼支持遠程調試,則啟動的時候必須加上特定的JVM參數,這些參數是:

1
-Xdebug -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=${debug_port}

其中的${debug_port}是用戶自定義的,為debug端口,本例以5555端口為例。

transport: 表示信息傳遞的連接方式, 其中,dt_socket是指用SOCKET模式,另有dt_shmem指用共享內存方式,其中,dt_shmem只適用於Windows平台。

server: server=y 表示是監聽其他debug client端的請求

address 表示等待調試的連接網絡端口

suspend 表示是否在啟動目標虛擬機后掛起虛擬機,如果要調試啟動過程,請使用y

參數說明:

  1. address為遠程調試的端口號,目前我們服務器上8412是一個開放的端口(staging環境已驗證)
  2. -Xrunjdwp  使用jdwp(Java Debug Wire Protocol)進行調試
  3. transport=dt_socket 使用socket方式進行連接,還可以使用其他通信方式如(dt_shmem 共享內存)
  4. suspend=n JVM監聽address端口傳來的信號時不掛起JVM中運行的進程

示例方式:

  1. IntelliJ遠程調試
  2. 使用JDI進行線上程序斷點信息記錄

IntelliJ遠程調試

打開Intellij IDEA,在頂部靠右的地方選擇”Edit Configurations…”,進去之后點擊+號,選擇”Remote”,按照下圖的只是填寫紅框內的內容,其中host為遠程代碼運行的機器的ip/hostname,port為上一步指定的debug_port,本例是5555。然后點擊Apply,最后點擊OK即可

啟動debug模式

現在在上一步選擇”Edit Configurations…”的下拉框的位置選擇上一步創建的remote的名字,然后點擊右邊的debug按鈕(長的像臭蟲那個),看控制台日志,如果出現類似“Connected to the target VM, address: ‘xx.xx.xx.xx:5555’, transport: ‘socket’”的字樣,就表示連接成功過了。

設置斷點,開始調試

遠程debug模式已經開啟,現在可以在需要調試的代碼中打斷點了,比如:

 

如圖中所示,如果斷點內有√,則表示選取的斷點正確。

現在在本地發送一個到遠程服務器的請求,看本地控制台的bug界面,划到debugger這個標簽,可以看到當前遠程服務的內部狀態(各種變量)已經全部顯示出來了,並且在剛才設置了斷點的地方,也顯示了該行的變量值。

 

使用JDI進行線上程序斷點信息記錄

java的整個調試體系為JDPA,Oracle提供了高級的jdi接口以方便使用java來連接調試程序進行相應的調試。這樣,只需要調用相應的java接口,就能進行打斷點,記錄斷點,然后繼續運行,清除斷點這樣基本的斷點調試手法了。

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;

/**
 * Created by nijianfeng on 16/10/24.
 */
public class DebugTest {

    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(5555));
        //進行連接
        VirtualMachine vm = sac.attach(arguments);
        //相應的請求調用通過requestManager來完成
        EventRequestManager eventRequestManager = vm.eventRequestManager();
        //創建一個代碼判斷,因此需要獲取相應的類,以及具體的斷點位置,即相應的代碼行。
        ClassType clazz = (ClassType) vm.classesByName("cn.gov.zcy.fixed.controller.base.AttachController").get(0);
        Location location = clazz.locationsOfLine(91).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();
        }
    }
    public static void execute(Event event) throws Exception {
        System.out.println(event.getClass().getCanonicalName());
        //獲取的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);
        });

//        com.sun.tools.jdi.EventSetImpl.BreakpointEventImpl
//        local->class java.lang.String (no class loader),class com.sun.tools.jdi.StringReferenceImpl,"1016MD/339900/039514a0-ea9d-449a-ba91-a6681acf30cd"
    }
}

連接遠程JVM

連接其它JVM稱之為附加(attach)操作,當前實現中有2種,如果是本地JVM,則通過Process的方式即可如果是遠程,則需要通過socket的方式才能進行連接。首先是server端需要開啟調試agent,並且指定相應的端口,如下啟動命令所示:

-Xdebug -agentlib:jdwp=transport=dt_socket,address=1234,server=y,suspend=n

上面的參數表示監聽指定端口(1234),並且當存在斷點時並不主動阻塞。舊的JVM也有使用Xrunjdwp參數的,但不再被建議使用.

客戶端即通過相應的connector進行連接,如下的socket連接方式:

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(1234));
 
vm = sac.attach(arguments);

即設置好主機地址,端口信息,然后即可以進行連接。

開啟新斷點

相應的請求調用通過requestManager來完成,由剛才成功連接上的vm(VirtualMachine)來獲取。由於這里是創建一個代碼判斷,因此需要獲取相應的類,以及具體的斷點位置,即相應的代碼行。 

要獲取代碼行,則要求源文件(class)中必須存在lineCode信息,即通過在拋出異常時,在異常信息中所指定的行數。這些信息默認情況下編譯時會自動輸出,但使用編譯開關-g:none時,則不會輸出。如果沒有line信息,則不能創建相應的斷點信息。創建信息如下所示:

eventRequestManager = vm.eventRequestManager();
 
ClassType clazz = (ClassType) vm.classesByName("A1").get(0);
Location location = clazz.locationsOfLine(15).get(0);
 
BreakpointRequest breakpointRequest
        = eventRequestManager.createBreakpointRequest(location);
breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
breakpointRequest.enable();

以上即拿到類A1的第15行的執行段(可能有多個,一行代碼可能有多個調用),並設置阻塞模式為線程阻塞,即只有當前線程被阻塞。最終啟用之,即成功創建新斷點。這里必須將其設置為阻塞,否則將斷點記錄產生時,由於當前線程繼續運行了,將不能拿到相應的信息(表示為產生IncompatibleThreadStateException)

記錄斷點信息

接下來就是從vm的事件隊列中,不斷地讀取事件並處理斷點記錄事件即可。由於斷點產生時,server端線程被阻塞,因此必須盡可能快地處理斷相應的,以避免出現業務阻塞。標准的事件循環如下所示:

eventQueue = vm.eventQueue();
while(true) {
    eventSet = eventQueue.remove();
    EventIterator eventIterator = eventSet.eventIterator();
    while(eventIterator.hasNext()) {
        Event event = eventIterator.next();
        execute(event);
    }
 
    eventSet.resume();
}

上面在事件記錄處理完之后,必須將相應線程resume,表示繼續運行。

這里獲取的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);
});

上面的value也是抽象形式,也可以通過類型轉換以打印不同類型的對象。

斷點繼續運行

必須調用相應的event.resume

清除斷點

如果記錄的信息又足夠(如記錄了半天的數據了),則可能通過requestManager.deleteEventRequest調用相應的斷點,或者調用deleteAllBreakpoints刪除所有斷點信息。或者通過vm.dispose直接中斷相應的調試連接即可。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM