平时我们进行程序开发时经常需要通过Debug的方式查看代码运行到某一行时的上下文变量信息,以便进行代码调式分析。但是在生产环境下,正在线上运行的程序如果发现有问题需要进行调式,如果手动SocketRemote连接并设置断点并跟踪断点信息,会造成程序阻塞,影响线上正在运行的业务。本文将通过Java JDI技术,介绍如何在程序正常运行的情况下,通过Java代码在不阻塞程序正常运行的情况下获取某一个代码行执行时的上下文变量信息,可以动态打断点并输出断点信息。
在介绍JDI之前,首先看一下传统的Socket Remote Debug
使用IDE进行Debug,可以使用IDE中内嵌的Tomcat进行调式,或使用Socket Remote方式调式,例如在Intellij IDEA中可以参照如下方式为程序设置远程调式端口:
启动上面的程序。之后设置进行debug的代码,如下设置要debug远程的地址和端口:
配置好后,按照如下方法启动debug即可:
什么时JDI
JPDA(Java Platform Debugger Architecture) 是 Java 平台调试体系结构的缩写,通过 JPDA 提供的API,开发人员可以方便灵活的搭建 Java 调试应用程序。JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),Java调试线协议(JDWP),以及 Java 调试接口(JDI)。JDI(Java Debug Interface)是 JPDA三层模块中最高层的接口,定义了调试器(Debugger)所需要的一些调试接口。基于这些接口,调试器可以及时地了解目标虚拟机的状态,例如查看目标虚拟机上有哪些类和实例等。另外,调试者还可以控制目标虚拟机的执行,例如挂起和恢复目标虚拟机上的线程,设置断点等。
使用JDI编写代码进行远程调式
1. 被调式程序设置调式端口
被调式程序启动时添加允许远程调式的启动参数:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
2. 引用类库
每个Java JDK都有自己的JDI接口实现,标准的Oracle JDK的JDI接口放在jdk/lib/tools.jar中,因此写代码时首先需要引入这个jar。
3. 连接远程JVM
/**
* 连接远程JVM
* @param hostname
* @param port
* @return
* @throws Exception
* @author 李文锴
*/
private VirtualMachine connectVirtualMachine(String hostname, Integer port) throws Exception{
VirtualMachineManager vmm = Bootstrap.virtualMachineManager();
List connectors = vmm.attachingConnectors();
SocketAttachingConnector sac = null;
for(AttachingConnector ac : connectors){
if(ac instanceof SocketAttachingConnector){
sac = (SocketAttachingConnector) ac;
}
}
if (sac == null) throw new Exception("未找到可用的SocketAttachingConnector连接器");
Map arguments = sac.defaultArguments();
arguments.get("hostname").setValue(hostname);
arguments.get("port").setValue(String.valueOf(port));
return sac.attach(arguments);
}
4. 标记断点
如下代码,需指定要标记断点的类和行数。注意一个类或者行可能会有多个线程来调用,但是示例代码中为了简单考虑只获取了第一个调用线程的结果,即get(0)。此外,给行打断点时,所设置的行数必须是有效代码的行。例如如果所设置的行是回车没有Java代码,则会抛出异常。
/**
* 标记断点
* @param clazz
* @param line
* @throws Exception
* @author 李文锴 */ public void markBreakpoint(Class clazz, Integer line) throws Exception{ EventRequestManager eventRequestManager = vm.eventRequestManager(); List rt = vm.classesByName(clazz.getName()); if(rt == null || rt.isEmpty()) throw new Exception("无法获取有效的debug类"); ClassType classType = (ClassType) rt.get(0); List locations = classType.locationsOfLine(line); if(locations == null || locations.isEmpty()) throw new Exception("无法获取有效的debug行"); Location location = locations.get(0); BreakpointRequest breakpointRequest = eventRequestManager.createBreakpointRequest(location); breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); breakpointRequest.enable(); }
如上注册了一个Breakpoint类型的事件请求,而系统除了提供断点事件外还提供了很多其他的事件类型:
不同的事件需要被分类地添加到不同的事件集合(EventSet)中,事件集是事件发送的最小单位。事件集一旦创建出来,便不可再被修改。生成的事件将被依次地加入到目标虚拟机的事件队列(EventQueue)中。然后,EventQueue将这些事件集以“先进先出”策略依次地发送到调试器端。EventQueue 负责管理来自目标虚拟机的事件,一个被调试的目标虚拟机上有且仅有一个EventQueue实例。特别地,随着一次事件集的发送,目标虚拟机上可能会有一部分的线程因此而被挂起。如果一直不恢复这些线程,有可能会导致目标虚拟机挂机。因此,在处理好一个事件集中的事件后,建议调用事件集的resume() 方法,恢复所有可能被挂起的线程。
5. 跟踪断点
如下代码,获取事件时需要根据事件类型进行相应的处理:
/**
* 跟踪断点
* @throws Exception
* @author 李文锴 */ public void traceBreakpoint() throws Exception{ EventQueue eventQueue = vm.eventQueue(); try { boolean runable = true; while (runable) { EventSet eventSet = eventQueue.remove(); EventIterator eventIterator = eventSet.eventIterator(); while (eventIterator.hasNext()) { Event event = eventIterator.next(); if (event instanceof BreakpointEvent) logBreadkpoint((BreakpointEvent) event); // 断开链接肯定是最后一个事件,所以自动跳出最内层循环 if (event instanceof VMDisconnectEvent) runable = false; } eventSet.resume(); } } catch (Exception e) { throw new RuntimeException(e); } }
6. 获取断点信息
如下代码:
private void logBreadkpoint(BreakpointEvent event) throws Exception{
ThreadReference threadReference = event.thread();
StackFrame stackFrame = threadReference.frame(0);
logger.log("<<================<< " + stackFrame.location()
+ " / " + stackFrame.location().method().name()
+ " >>================>>");
logger.log("[方法参数]");
List argsList = stackFrame.getArgumentValues();
for (Value arg : argsList){
logger.log(arg.type().name() + " = " + parseValue(arg));
}
logger.log("[变量信息]");
try {
List varList = stackFrame.visibleVariables();
for(LocalVariable localVariable : varList){
logLocalVariable(stackFrame, localVariable);
}
} catch (AbsentInformationException e) {
throw new RuntimeException(e);
}
}
private void logLocalVariable(StackFrame stackFrame, LocalVariable localVariable){
StringBuilder out = new StringBuilder(localVariable.name());
if(localVariable.isArgument()){
out.append("[*]");
}
out.append(" = ");
out.append(parseValue(stackFrame.getValue(localVariable)));
logger.log(out.toString());
}调用 BreakpointEvent 的 thread() 可以获取产生事件的线程镜像(ThreadReference),调用ThreadReference 的 frame(int) 可获得当前代码行所在的堆栈(StackFrame),调用 StackFrame 的visibleVariables() 可获取当前堆栈中的所有本地变(LocaleVariable)。通过调用 BreakpointEvent 的location() 可获得断点所在的代码行号(Location),调用 Location 的 method()可获得当前代码行所归属的方法。通过以上调用,调试器便可获得了目标虚拟机上线程、对象、变量等镜像信息。
7. 解析获取的变量
private String parseValue(Value value){
StringBuilder out = new StringBuilder();
if(value instanceof StringReference || value instanceof IntegerValue || value instanceof BooleanValue
|| value instanceof ByteValue || value instanceof CharValue || value instanceof ShortValue
|| value instanceof LongValue || value instanceof FloatValue || value instanceof DoubleValue){
out.append(value);
}else if(value instanceof ObjectReference){
ObjectReference obj = (ObjectReference) value;
String type = obj.referenceType().name();
if("java.lang.Integer".equals(type) || "java.lang.Boolean".equals(type) || "java.lang.Float".equals(type)
|| "java.lang.Double".equals(type) || "java.lang.Long".equals(type) || "java.lang.Byte".equals(type)
|| "java.lang.Character".equals(type)){
Field f = obj.referenceType().fieldByName("value");
out.append(obj.getValue(f));
}else if("java.util.Date".equals(type)) {
Field field = obj.referenceType().fieldByName("fastTime");
Date date = new Date(Long.parseLong("" + obj.getValue(field)));
out.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
}else if(value instanceof ArrayReference){
ArrayReference ar = (ArrayReference) value;
List values = ar.getValues();
out.append("[");
for (int i = 0; i < values.size(); i++){
if( i != 0 ) out.append(" ,");
out.append(parseValue(values.get(i)));
}
out.append("]");
}else {
out.append(type);
}
}
return out.toString();
}
Value 和 Type 接口分别代表着目标虚拟机中对象、实例变量和方法变量的值和类型。通过 Value 接口的 type(),可以获取该值对应的类型。JDI中定义了两种基本的数据类型:原始类型(PrimitiveType)和引用类(ReferenceType)。与其对应的数值类型分别是原始值(PrimtiveValue)和对象引用(ObjectReference),Value和 Type 的具体对应关系如下:
8. 测试:启动目标程序
测试目标类如下,计划在System.out.println行打断点,为了演示方便通过Thread.sleep添加暂停事件,以便启动调式程序:
public class Test {
public static void main(String[] args) throws Exception {
test("hello", 100);
}
private static void test(String a, Integer b) throws Exception{
Thread.sleep(10*1000);
Boolean isOk = false;
Date date = new Date();
System.out.println("over");
}
}
启动时添加远程debug参数并运行。
9. 测试:执行debug程序
代码如下:
public class Debug {
public static void main(String[] args) throws Exception{
DebugRemoteJava drj = new DebugRemoteJava("127.0.0.1", 5005);
drj.markBreakpoint(Test.class, 18);
drj.traceBreakpoint();
}
}输出结果如下: