自己动手实现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断点调试的文章暂时告一个段落了,需要详细源码可以关注一下同名公众号,让我有动力继续研究网上搜索不到的东西。
自己动手实现java断点/单步调试(二)的更多相关文章
- 自己动手实现java断点/单步调试(一)
又是好长时间没有写博客了,今天我们就来谈一下java程序的断点调试.写这篇主题的主要原因是身边的公司或者个人都执着于做apaas平台,简单来说apaas平台就是一个零代码或者低代码的配置平台,通过配置 ...
- Atitit. 脚本语言的断点单步调试的设计与实现 attialx 总结 php 参照java
Atitit. 脚本语言的断点单步调试的设计与实现 attialx 总结 php 参照java 1. 断点的实现:手动断点 die和exit是等价的 1 2. 变量表的实现 1 3. print_r( ...
- android NDK开发在本地C/C++源码中设置断点单步调试具体教程
近期在学android NDK开发,折腾了一天,最终可以成功在ADT中设置断点单步调试本地C/C++源码了.网上关于这方面的资料太少了,并且大都不全,并且调试过程中会出现各种各样的问题,真是非常磨人. ...
- Intellij idea远程debug连接tomcat,实现单步调试
转载:http://blog.csdn.net/boling_cavalry/article/details/73384036 web项目部署到tomcat上之后,有时需要打断点单步调试,如果用的是I ...
- IntelliJ IDEA远程连接tomcat,实现单步调试
web项目部署到tomcat上之后,有时需要打断点单步调试,如果用的是Intellij idea,可以通过如下方法实现: 开启debug端口,启动tomcat 以tomcat7.0.75为例,打开bi ...
- Java基础(61):Java单步调试(转)
Eclipse 的单步调试 1.设置断点在程序里面放置一个断点,也就是双击需要放置断点的程序左边的栏目上. 2.调试(1)点击"打开透视图"按钮,选择调试透视图,则打开调试透视图界 ...
- JAVA 单步调试快捷键
JAVA 单步调试快捷键以debug方式运行java程序后 (F8)直接执行程序.遇到断点时暂停:(F5)单步执行程序,遇到方法时进入:(F6)单步执行程序,遇到方法时跳过:(F7)单步执行程序,从当 ...
- C#.NET常见问题(FAQ)-程序如何单步调试和设置断点
对于控制台程序而言,直接按F10(不按F5运行)就可以单步运行,当前运行行会显示为黄色(不管是一条语句,还是一个函数,都会直接执行完毕得到结果) 你可以在变量名上右击添加监视(会自动放到监视1中) ...
- 解决JAVA单步调试键盘输入被JDB占用的问题
解决JAVA单步调试键盘输入被JDB占用的问题 问题来源: 在完成本周任务时,编写的代码中含有Scanner类,编译及运行过程均正确,但使用JDB单步调试时,运行到输入行无法在JDB内部输入变量值. ...
随机推荐
- python之logging 模块(下篇)
四.日志处理流程(第二种日志使用方式) 上面简单配置的方法例子中我们了解到了logging.debug().logging.info().logging.warning().logging.error ...
- Scaled-YOLOv4 快速开始,训练自定义数据集
代码: https://github.com/ikuokuo/start-scaled-yolov4 Scaled-YOLOv4 代码: https://github.com/WongKinYiu/S ...
- 【函数分享】每日PHP函数分享(2021-1-7)
ltrim() 删除字符串开头的空白字符(或其他字符). string ltrim ( string $str[, string $character_mask]) 参数描述str 输入的字符串. c ...
- TeamView WaitforConnectFailed错误原因
更新到最新版本并重启如下服务 检查TCP IPV4是否选中
- 【高级排序算法】2、归并排序法的实现-Merge Sort
简单记录 - bobo老师的玩转算法系列–玩转算法 -高级排序算法 Merge Sort 归并排序 Java实现归并排序 SortTestHelper 排序测试辅助类 package algo; im ...
- ctfhub技能树—sql注入—整数型注入
打开靶机 查看页面信息 查看回显位 查询数据库名 查询表名 查询字段 查询字段信息 使用sqlmap食用效果更佳 查数据库名 python2 sqlmap.py -u http://challenge ...
- os-Bytes环境变量劫持
信息收集 netdiscovery -i eth0 nmap -sV -sC 192.168.43.74 -oA os-Bytes gobuster -u 192.168.43.74 -w /usr/ ...
- SwiftUI 中一些和响应式状态有关的属性包装器的用途
SwiftUI 借鉴了 React 等 UI 框架的概念,通过 state 的变化,对 View 进行响应式的渲染.主要通过 @State, @StateObject, @ObservedObject ...
- RocketMQ在linx安装及其有关问题解决
Linx安装和使用: rocketmq官网:http://rocketmq.apache.org/ 首先安装JDK(推荐使用JDK1.8),并配置环境变量 下载rocketmq压碎包并解压到指定目录 ...
- Nifi组件脚本开发—ExecuteScript 使用指南(一)
Part 1 - 介绍 NiFi API 和 FlowFiles ExecuteScript 是一个万能的处理器,允许用户使用编程语言定义自己的数据处理功能, 在每一次 ExecuteScript p ...