自己動手實現java斷點/單步除錯(二)
阿新 • • 發佈:2021-02-05
自從上一篇《自己動手實現java斷點/單步除錯(一)》
是時候應該總結一下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斷點除錯的文章暫時告一個段落了,需要詳細原始碼可以關注一下同名公眾號,讓我有動力繼續研究網上搜索不到的東