自从上一篇《自己动手实现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. python之logging 模块(下篇)

    四.日志处理流程(第二种日志使用方式) 上面简单配置的方法例子中我们了解到了logging.debug().logging.info().logging.warning().logging.error ...

  2. Scaled-YOLOv4 快速开始,训练自定义数据集

    代码: https://github.com/ikuokuo/start-scaled-yolov4 Scaled-YOLOv4 代码: https://github.com/WongKinYiu/S ...

  3. 【函数分享】每日PHP函数分享(2021-1-7)

    ltrim() 删除字符串开头的空白字符(或其他字符). string ltrim ( string $str[, string $character_mask]) 参数描述str 输入的字符串. c ...

  4. TeamView WaitforConnectFailed错误原因

    更新到最新版本并重启如下服务 检查TCP IPV4是否选中

  5. 【高级排序算法】2、归并排序法的实现-Merge Sort

    简单记录 - bobo老师的玩转算法系列–玩转算法 -高级排序算法 Merge Sort 归并排序 Java实现归并排序 SortTestHelper 排序测试辅助类 package algo; im ...

  6. ctfhub技能树—sql注入—整数型注入

    打开靶机 查看页面信息 查看回显位 查询数据库名 查询表名 查询字段 查询字段信息 使用sqlmap食用效果更佳 查数据库名 python2 sqlmap.py -u http://challenge ...

  7. os-Bytes环境变量劫持

    信息收集 netdiscovery -i eth0 nmap -sV -sC 192.168.43.74 -oA os-Bytes gobuster -u 192.168.43.74 -w /usr/ ...

  8. SwiftUI 中一些和响应式状态有关的属性包装器的用途

    SwiftUI 借鉴了 React 等 UI 框架的概念,通过 state 的变化,对 View 进行响应式的渲染.主要通过 @State, @StateObject, @ObservedObject ...

  9. RocketMQ在linx安装及其有关问题解决

    Linx安装和使用: rocketmq官网:http://rocketmq.apache.org/ 首先安装JDK(推荐使用JDK1.8),并配置环境变量 下载rocketmq压碎包并解压到指定目录 ...

  10. Nifi组件脚本开发—ExecuteScript 使用指南(一)

    Part 1 - 介绍 NiFi API 和 FlowFiles ExecuteScript 是一个万能的处理器,允许用户使用编程语言定义自己的数据处理功能, 在每一次 ExecuteScript p ...