使用特定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
參數說明:
- address為遠程調試的端口號,目前我們服務器上8412是一個開放的端口(staging環境已驗證)
- -Xrunjdwp 使用jdwp(Java Debug Wire Protocol)進行調試
- transport=dt_socket 使用socket方式進行連接,還可以使用其他通信方式如(dt_shmem 共享內存)
- suspend=n JVM監聽address端口傳來的信號時不掛起JVM中運行的進程
示例方式:
- IntelliJ遠程調試
- 使用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直接中斷相應的調試連接即可。