是時候應該總結一下JDI的事件了
事件類型 | 描述 |
---|---|
ClassPrepareEvent | 裝載某個指定的類所引發的事件 |
ClassUnloadEvent | 卸載某個指定的類所引發的事件 |
BreakingpointEvent | 設置斷點所引發的事件 |
ExceptionEvent | 目標虛擬機運行中拋出指定異常所引發的事件 |
MethodEntryEvent | 進入某個指定方法體時引發的事件 |
MethodExitEvent | 某個指定方法執行完成后引發的事件 |
MonitorContendedEnteredEvent | 線程已經進入某個指定 Monitor 資源所引發的事件 |
MonitorContendedEnterEvent | 線程將要進入某個指定 Monitor 資源所引發的事件 |
MonitorWaitedEvent | 線程完成對某個指定 Monitor 資源等待所引發的事件 |
MonitorWaitEvent | 線程開始等待對某個指定 Monitor 資源所引發的事件 |
StepEvent | 目標應用程序執行下一條指令或者代碼行所引發的事件 |
AccessWatchpointEvent | 查看類的某個指定 Field 所引發的事件 |
ModificationWatchpointEvent | 修改類的某個指定 Field 值所引發的事件 |
ThreadDeathEvent | 某個指定線程運行完成所引發的事件 |
ThreadStartEvent | 某個指定線程開始運行所引發的事件 |
VMDeathEvent | 目標虛擬機停止運行所以的事件 |
VMDisconnectEvent | 目標虛擬機與調試器斷開鏈接所引發的事件 |
VMStartEvent | 目標虛擬機初始化時所引發的事件 |
在上一篇之中我們只是用到了BreakingpointEvent和VMDisconnectEvent事件,這一篇我們為了加單步調試會用到StepEvent事件了,創建執行下一條、進入方法,跳出方法的事件代碼如下
/** * 眾所周知,debug單步調試過程最重要的幾個調試方式:執行下一條(step_over),執行方法里面(step_into), * 跳出方法(step_out)。 * @param eventType 斷點調試事件類型 STEP_INTO(1),STEP_OVER(2),STEP_OUT(3) * @return * @throws Exception */ private EventRequest createEvent(EventType eventType) throws Exception { /** * 根據事件類型獲取對應的事件請求對象並激活,最終會被放到事件隊列中 */ EventRequestManager eventRequestManager = virtualMachine.eventRequestManager(); /** * 主要是為了把當前事件請求刪掉,要不然執行到下一行 * 又要發送一個單步調試的事件,就會報一個線程只能有一種單步調試事件,這里很多細節都是 * 本人花費大量事件調試得到的,可能不是最優雅的,但是肯定是可實現的 */ if(eventRequest != null) { eventRequestManager.deleteEventRequest(eventRequest); } eventRequest = eventRequestManager.createStepRequest(threadReference,StepRequest.STEP_LINE,eventType.getIndex()); eventRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); eventRequest.enable(); /** * 同上創建斷點事件,這里也是創建完事件,就釋放被調試程序 */ if(eventsSet != null) { eventsSet.resume(); } return eventRequest; }
獲取當前本地變量,成員變量,方法信息,類信息等方法修改為如下
/** * 消費調試的事件請求,然后拿到當前執行的方法,參數,變量等信息,也就是debug過程中我們關注的那一堆變量信息 * @return * @throws Exception */ private DebugInfo getInfo() throws Exception { DebugInfo debugInfo = new DebugInfo(); EventQueue eventQueue = virtualMachine.eventQueue(); /** * 這個是阻塞方法,當有事件發出這里才可以remove拿到EventsSet */ eventsSet= eventQueue.remove(); EventIterator eventIterator = eventsSet.eventIterator(); if(eventIterator.hasNext()) { Event event = eventIterator.next(); /** * 一個debug程序能夠debug肯定要有個斷點,直接從斷點事件這里拿到當前被調試程序當前的執行線程引用, * 這個引用是后面可以拿到信息的關鍵,所以保存在成員變量中,歸屬於當前的調試對象 */ if(event instanceof BreakpointEvent) { threadReference = ((BreakpointEvent) event).thread(); } else if(event instanceof VMDisconnectEvent) { /** * 這種事件是屬於講武德的判斷方式,斷點到最后一行之后調用virtualMachine.dispose()結束調試連接 */ debugInfo.setEnd(true); return debugInfo; } else if(event instanceof StepEvent) { threadReference = ((StepEvent) event).thread(); } try { /** * 獲取被調試類當前執行的棧幀,然后獲取當前執行的位置 */ StackFrame stackFrame = threadReference.frame(0); Location location = stackFrame.location(); /** * 當前走到線程退出了,就over了,這里其實是我在調試過程中發現如果調試的時候不講武德,明明到了最后一行 * 還要發送一個STEP_OVER事件出來,就會報錯。本着調試端就是客戶,客戶就是上帝的心態,做了一個不太優雅 * 的判斷 */ if("java.lang.Thread.exit()".equals(location.method().toString())) { debugInfo.setEnd(true); return debugInfo; } /** * 無腦的封裝返回對象 */ debugInfo.setClassName(location.declaringType().name()); debugInfo.setMethodName(location.method().name()); debugInfo.setLineNumber(location.lineNumber()); /** * 封裝成員變量 */ ObjectReference or = stackFrame.thisObject(); if(or != null) { List<Field> fields = ((LocationImpl) location).declaringType().fields(); for(int i = 0;fields != null && i < fields.size();i++) { Field field = fields.get(i); Object val = parseValue(or.getValue(field),0); DebugInfo.VarInfo varInfo = new DebugInfo.VarInfo(field.name(),field.typeName(),val); debugInfo.getFields().add(varInfo); } } /** * 封裝局部變量和參數,參數是方法傳入的參數 */ List<LocalVariable> varList = stackFrame.visibleVariables(); for (LocalVariable localVariable : varList) { /** * 這地方使用threadReference.frame(0)而不是使用上面已經拿到的stackFrame,從代碼上看是等價, * 但是有個很坑的地方,如果使用stackFrame由於下面使用threadReference執行過invokeMethod會導致 * stackFrame的isValid為false,再次通過stackFrame.getValue就會報錯,每次重新threadReference.frame(0) * 就沒有問題,由於看不到源碼,個人推測threadReference.frame(0)這里會生成一份拷貝stackFrame,由於手動執行方法, * 方法需要用到棧幀會導致執行完方法,這個拷貝的棧幀被銷毀而變得不可用,而每次重新獲取最上面得棧幀,就不會有問題 */ DebugInfo.VarInfo varInfo = new DebugInfo.VarInfo(localVariable.name(),localVariable.typeName(),parseValue(threadReference.frame(0).getValue(localVariable),0)); if(localVariable.isArgument()) { debugInfo.getArgs().add(varInfo); } else { debugInfo.getVars().add(varInfo); } } } catch(AbsentInformationException | VMDisconnectedException e1) { debugInfo.setEnd(true); return debugInfo; } catch(Exception e) { debugInfo.setEnd(true); return debugInfo; } } return debugInfo; }
事件枚舉如下
/** * 調試事件類型 * @author rongdi * @date 2021/1/31 */ public enum EventType { // 進入方法 STEP_INTO(1), // 下一條 STEP_OVER(2), // 跳出方法 STEP_OUT(3); private int index; private EventType(int index) { this.index = index; } public int getIndex() { return index; } public static EventType getType(Integer type) { if(type == null) { return STEP_OVER; } if(type.equals(1)) { return STEP_INTO; } else if(type.equals(3)){ return STEP_OUT; } else { return STEP_OVER; } } }
為了方便使用,我們合並一下方法,統一對外提供的工具方法如下
/** * 打斷點並獲取當前執行的類,方法,各種變量信息,主要是給調試端斷點調試的場景, * 當前執行之后有斷點,使用此方法會直接運行到斷點處,需要注意的是不要兩次請求打同一行的斷點,這樣會導致第二次斷點 * 執行時如果后續沒有斷點了,會直接執行到連接斷開 * @param className * @param lineNumber * @return * @throws Exception */ public DebugInfo markBpAndGetInfo(String className, Integer lineNumber) throws Exception { markBreakpoint(className, lineNumber); return getInfo(); } /** * 單步調試, * STEP_INTO(1) 執行到方法里 * STEP_OVER(2) 執行下一行代碼 * STEP_OUT(3) 跳出方法執行 * @param eventType * @return * @throws Exception */ public DebugInfo stepAndGetInfo(EventType eventType) throws Exception { createEvent(eventType); return getInfo(); } /** * 當斷點到最后一行后,調用斷開連接結束調試 */ public DebugInfo disconnect() throws Exception { virtualMachine.dispose(); map.remove(tag); return getInfo(); }
最后我們提供一個統一的接口類,統一對外提供斷點/單步調試服務
/** * 調試接口 * @author rongdi * @date 2021/1/31 */ @RestController public class DebuggerController { @RequestMapping("/breakpoint") public DebugInfo breakpoint(@RequestParam String tag, @RequestParam String hostname, @RequestParam Integer port, @RequestParam String className, @RequestParam Integer lineNumber) throws Exception { Debugger debugger = Debugger.getInstance(tag,hostname,port); return debugger.markBpAndGetInfo(className,lineNumber); } @RequestMapping("/stepInto") public DebugInfo stepInto(@RequestParam String tag) throws Exception { Debugger debugger = Debugger.getInstance(tag); return debugger.stepAndGetInfo(EventType.STEP_INTO); } @RequestMapping("/stepOver") public DebugInfo stepOver(@RequestParam String tag) throws Exception { Debugger debugger = Debugger.getInstance(tag); return debugger.stepAndGetInfo(EventType.STEP_OVER); } @RequestMapping("/stepOut") public DebugInfo step(@RequestParam String tag) throws Exception { Debugger debugger = Debugger.getInstance(tag); return debugger.stepAndGetInfo(EventType.STEP_OUT); } @RequestMapping("/disconnect") public DebugInfo disconnect(@RequestParam String tag) throws Exception { Debugger debugger = Debugger.getInstance(tag); return debugger.disconnect(); } }
至此,對於遠程斷點調試的功能已經基本完成了,雖然寫的過程中確實很虐,但是寫完后還是發現挺簡單的。擴展思路(個人感覺作為遠程的調試沒有必要做以下擴展):
-
加入類似IDE調試界面左邊的方法棧信息
只需要加入MethodEntryEvent和MethodExitEvent事件並引入一個stack對象,每當進入方法的時候把調試信息壓棧,退出方法時出棧調試信息,然后調試返回信息加上這個棧的信息返回就可以了
- 加入條件斷點功能這里可以通過ognl、spring的spEL表達式都可以實現
- 手動方法執行返回結果其實解決方案同2
好了,自己動手實現JAVA斷點調試的文章暫時告一個段落了,需要詳細源碼可以關注一下同名公眾號,讓我有動力繼續研究網上搜索不到的東西。