是时候应该总结一下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断点调试的文章暂时告一个段落了,需要详细源码可以关注一下同名公众号,让我有动力继续研究网上搜索不到的东西。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END