自从上一篇《自己动手实现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 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 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断点调试的文章暂时告一个段落了,需要详细源码可以关注一下同名公众号,让我有动力继续研究网上搜索不到的东西。

自己动手实现java断点/单步调试(二)的更多相关文章

  1. Java 生成随机字符的示例代码

    示例代码:import java.util.Random;import java.util.UUID;public class Dept {/*** 生成随机字符串 uuid*/public static String getUUID() {return UUID.randomUUID().toSt......

  2. Java压缩集合的三种方法

    前言这个问题算是开发当中偶尔会遇到的一个小问题,比如如何将两个集合压缩成为一个逻辑集合。如果你不理解,我们可以看一个简单的例子,去说明什么是压缩集合。本文文章不长,但是还算是比较实用的小技巧。主要内容来源于国外小哥Baeldung的博客:下面给出个地址https://www.baeldung.com......

  3. java中判断Object对象类型

    Object param = params.get(i); if (param instanceof Integer) { int value = ((Integer) param).intValue(); prepStatement.setInt(i + , value); }......

  4. java实现给图片加铺满的网格式文字水印

    效果:原图加水印后的图片废话不多说,直接上代码代码:package com.example.demo;import java.awt.AlphaComposite;import java.awt.Color;import java.awt.Font;import java.awt.Graphics2......

  5. Java并发包源码学习系列:阻塞队列实现之SynchronousQueue源码解析

    目录SynchronousQueue概述 使用案例 类图结构 put与take方法 void put(E e) E take() Transfer 公平模式TransferQueue QNode transfer awaitFulfill tryCancel clean TransferQueue总......

  6. 深入浅出Java线程池:使用篇

    前言很高兴遇见你~借助于很多强大的框架,现在我们已经很少直接去管理线程,框架的内部都会为我们自动维护一个线程池。例如我们使用最多的okHttp以及他的封装框架Retrofit,线程封装框架RxJava和kotlin协程等等。为了更好地使用这些框架,则必须了解他的实现原理,而了解他的原理,线程池是永远......

  7. Java HashMap源码分析(含散列表、红黑树、扰动函数等重点问题分析)

    Java HashMap源码分析(含散列表、红黑树、扰动函数等重点问题分析)写在最前面这个项目是从20年末就立好的 flag,经过几年的学习,回过头再去看很多知识点又有新的理解。所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项目,专注 Java 后端面......

  8. Java中while语句的简单知识及应用

    先谈谈while循环的三要素while循环的三要素:(1)初始化变量(2)循环条件(3)改变循环变量的值当你要用while循环时主要知道这三个要素什么,那么循环起来就得心应手了。下面是while循环语法和特点:while语句的形式while语句的执行过程:① 先计算条件表达式的值 ;② 如果该表达式......

  9. java中日期格式化的大坑

    前言我们都知道在java中进行日期格式化使用simpledateformat。通过格式 yyyy-MM-dd 等来进行格式化,但是你知道其中微小的坑吗?yyyy 和 YYYY示例代码@Testpublic void testWeekBasedYear() {Calendar calendar = C......

  10. Java 执行过程中的内存模型

    一、前言本文的主要工作:尝试以时间顺序追踪一遍 Java 执行的整个过程,以及展示 JVM 中内存模型的相应变化。本文的主要目的:希望能够通过 Java 执行过程的冰山一角,增进对编程语言工作原理的理解。以下面这段代码为例,追踪它的执行过程:public class Car {private int......

随机推荐

  1. PostgreSQL 实现给查询列表增加序号操作

    利用 ROW_NUMBER() over( ) 给查询序列增加排序字段SELECT ROW_NUMBER() over(ORDER bY biztypename DESC ) AS num,biztypename FROM (SELECT DISTINCT biztypename FROM bizm......

  2. Java 执行过程中的内存模型

    一、前言本文的主要工作:尝试以时间顺序追踪一遍 Java 执行的整个过程,以及展示 JVM 中内存模型的相应变化。本文的主要目的:希望能够通过 Java 执行过程的冰山一角,增进对编程语言工作原理的理解。以下面这段代码为例,追踪它的执行过程:public class Car {private int......

  3. MySql8 WITH RECURSIVE递归查询父子集的方法

    背景开发过程中遇到类似评论的功能是,需要时用查询所有评论的子集。不同数据库中实现方式也不同,本文使用Mysql数据库,版本为8.0Oracle数据库中可使用START [Param] CONNECT BY PRIORMysql 中需要使用 WITH RECURSIVE需求找到name为张三的孩子和孙......

  4. C语言之漫谈指针(下)

    C语言之漫谈指针(下)在上节我们讲到了一些关于指针的基础知识:详见:C语言之漫谈指针(上)本节大纲:零.小tips一.字符指针二.指针数组与数组指针三.数组传参与指针传参四.函数指针及函数指针数组五.回调函数六.例题讲解 零.小tips在正式开始下节之前,我们先来穿插两个小tips:1.打印函数......

  5. vue 递归组件的简单使用示例

    前言递归 相信很多同学已经不陌生了,算法中我们经常用递归来解决问题。比如经典的:从一个全为数字的数组中找出其中相加能等于目标数的组合。思路也不难,循环数组取值,不断递归相加,直到满足目标数条件。递归虽然能解决大部分,但弊处在于,很容易写出死循环的代码,导致爆栈。下面以我实际业务场景讲讲递归在vue组......

  6. ASP.NET Core中如何实现重定向详解

    前言ASP.NET Core 是一个跨平台,开源的,轻量级的,模块化的,用于构建高性能的 web 开发框架, ASP.NET Core MVC 内置了多种方式将一个 request 请求跳转到指定的url,这篇文章我们就来讨论如何去实现。理解 RedirectActionResultASP.NET ......

  7. Python数据结构-集合

    1.集合"""集合(set):没有重复元素且没有顺序的数据结构定义语法:s = set({}) #空集合s = set({1, 2, 3, 4, 5})增加:add() 往集合添加一条数据update() 合并,支持传入列表、字典、元......

  8. Python学习(4)( If 判断语句 、逻辑运算、elif、if嵌套、随机数、石头剪刀布程序)

    Python学习(4)一、python的 if 判断语句二、python的逻辑运算1. and2. or3. not三、python的 elif 判断语句四、python的if 嵌套五、随机数的处理六、石头剪刀布 ---演练一、python的 if 判断语句在python 中,if 语句 就是用来进......

  9. C#通过NI-VISA操作Tektronix TBS 2000B系列示波器

    一、概述本文描述采用C#语言访问控制Tektronix TBS 2000B 系列示波器。接口协议采用NI-VISA。最近一个项目需要和一款示波器进行通信,需要对示波器进行一些简单控制并获取到波形数据。经过一段时间研究,大致了解了相关操作,因为发现相关资料不是很多,所以把我了解的相关知识和大家分享一下......

  10. Linux下使用timedatectl命令时间时区操作详解

    timedatectl命令对于RHEL / CentOS 7和基于Fedora 21+的分布式系统来说,是一个新工具,它作为systemd系统和服务管理器的一部分,代替旧的传统的用在基于Linux分布式系统的sysvinit守护进程的date命令。timedatectl命令可以查询和更改系统时钟和设......