自己動手實現java斷點/單步調試(二)


自從上一篇《自己動手實現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();
    }
}

至此,對於遠程斷點調試的功能已經基本完成了,雖然寫的過程中確實很虐,但是寫完后還是發現挺簡單的。擴展思路(個人感覺作為遠程的調試沒有必要做以下擴展):

  1. 加入類似IDE調試界面左邊的方法棧信息

    只需要加入MethodEntryEvent和MethodExitEvent事件並引入一個stack對象,每當進入方法的時候把調試信息壓棧,退出方法時出棧調試信息,然后調試返回信息加上這個棧的信息返回就可以了

  2. 加入條件斷點功能這里可以通過ognl、spring的spEL表達式都可以實現
  3. 手動方法執行返回結果其實解決方案同2


好了,自己動手實現JAVA斷點調試的文章暫時告一個段落了,需要詳細源碼可以關注一下同名公眾號,讓我有動力繼續研究網上搜索不到的東西。


免責聲明!

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



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