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

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

  1. 自己动手实现java断点/单步调试(一)

    又是好长时间没有写博客了,今天我们就来谈一下java程序的断点调试.写这篇主题的主要原因是身边的公司或者个人都执着于做apaas平台,简单来说apaas平台就是一个零代码或者低代码的配置平台,通过配置 ...

  2. Atitit. 脚本语言的断点单步调试的设计与实现 attialx 总结 php 参照java

    Atitit. 脚本语言的断点单步调试的设计与实现 attialx 总结 php 参照java 1. 断点的实现:手动断点 die和exit是等价的 1 2. 变量表的实现 1 3. print_r( ...

  3. android NDK开发在本地C/C++源码中设置断点单步调试具体教程

    近期在学android NDK开发,折腾了一天,最终可以成功在ADT中设置断点单步调试本地C/C++源码了.网上关于这方面的资料太少了,并且大都不全,并且调试过程中会出现各种各样的问题,真是非常磨人. ...

  4. Intellij idea远程debug连接tomcat,实现单步调试

    转载:http://blog.csdn.net/boling_cavalry/article/details/73384036 web项目部署到tomcat上之后,有时需要打断点单步调试,如果用的是I ...

  5. IntelliJ IDEA远程连接tomcat,实现单步调试

    web项目部署到tomcat上之后,有时需要打断点单步调试,如果用的是Intellij idea,可以通过如下方法实现: 开启debug端口,启动tomcat 以tomcat7.0.75为例,打开bi ...

  6. Java基础(61):Java单步调试(转)

    Eclipse 的单步调试 1.设置断点在程序里面放置一个断点,也就是双击需要放置断点的程序左边的栏目上. 2.调试(1)点击"打开透视图"按钮,选择调试透视图,则打开调试透视图界 ...

  7. JAVA 单步调试快捷键

    JAVA 单步调试快捷键以debug方式运行java程序后 (F8)直接执行程序.遇到断点时暂停:(F5)单步执行程序,遇到方法时进入:(F6)单步执行程序,遇到方法时跳过:(F7)单步执行程序,从当 ...

  8. C#.NET常见问题(FAQ)-程序如何单步调试和设置断点

    对于控制台程序而言,直接按F10(不按F5运行)就可以单步运行,当前运行行会显示为黄色(不管是一条语句,还是一个函数,都会直接执行完毕得到结果)   你可以在变量名上右击添加监视(会自动放到监视1中) ...

  9. 解决JAVA单步调试键盘输入被JDB占用的问题

    解决JAVA单步调试键盘输入被JDB占用的问题 问题来源: 在完成本周任务时,编写的代码中含有Scanner类,编译及运行过程均正确,但使用JDB单步调试时,运行到输入行无法在JDB内部输入变量值. ...

随机推荐

  1. spark的运行指标监控

    sparkUi的4040界面已经有了运行监控指标,为什么我们还要自定义存入redis? 1.结合自己的业务,可以将监控页面集成到自己的数据平台内,方便问题查找,邮件告警 2.可以在sparkUi的基础 ...

  2. Spring Security OAuth2.0认证授权四:分布式系统认证授权

    Spring Security OAuth2.0认证授权系列文章 Spring Security OAuth2.0认证授权一:框架搭建和认证测试 Spring Security OAuth2.0认证授 ...

  3. 【剑指 Offer】05.替换空格

    题目描述 请实现一个函数,把字符串 s 中的每个空格替换成"%20". 示例 1: 输入:s = "We are happy." 输出:"We%20a ...

  4. 【Spring】Spring 入门

    Spring 入门 文章源码 Spring 概述 Spring Spring 是分层的 Java SE/EE 应用全栈式轻量级开源框架,以 IOC(Inverse Of Control,反转控制)和 ...

  5. Java 反射修改类的常量值、静态变量值、属性值

    前言 有的时候,我们需要修改一个变量的值,但变量也许存在于 Jar 包中或其他位置,导致我们不能从代码层面进行修改,于是我们就用到了下面的场景,通过反射来进行修改变量的值. 定义一个实体类 class ...

  6. DHCP最佳实践(一)

    这是Windows DHCP最佳实践和技巧的最终指南. 如果您有任何最佳做法或技巧,请在下面的评论中发布它们. 在本指南(一)中,我将分享以下DHCP最佳实践和技巧. 不要将DHCP放在您的域控制器上 ...

  7. 总结下MySql优化。防止数据灾难的发生。

    在PHP开发中用到的数据库中MySql是最牛逼的数据库,没有之一--^_^ 相比Sqlite个人最喜欢的特性就是"支持多线程,充分利用 CPU 资源",不像Sqlite那样,动不动 ...

  8. Java 安全之Weblogic 2018-2628&2018-2893分析

    Java 安全之Weblogic 2018-2628&2018-2893分析 0x00 前言 续上一个weblogic T3协议的反序列化漏洞接着分析该补丁的绕过方式,根据weblogic的补 ...

  9. 入门训练 - 蓝桥杯(Python实现)

    A+B问题: 题目: 资源限制 时间限制:1.0s 内存限制:256.0MB 问题描述 输入A.B,输出A+B. 输入格式 输入的第一行包括两个整数,由空格分隔,分别表示A.B. 输出格式 输出一行, ...

  10. 数据分析 Pandas 简介和它的的数据结构

    本文主要讲Pandas 的Series和DataFrame 的相关属性和操作 1.Series的相关属性和操作# --Series是一种类似于一维数组的对象,只能存放一维数组!由以下两部分组成:# v ...