分享

java另类调试技术

 Moteme 2012-05-05

前言:曾经有开发人员跟我说过,Qone所用的技术太simple 了,远没法同做网络安全、多媒体等应用相提并论。我只能说这个开发人员太simple,太naive了。且不说Qone应用中涉及到的J2EE企业级应用架构、设计模式,要想熟练掌握并做到灵活应用已经是很难的事情了,就是Qone应用本身,也可以挖掘出一批java语言中深层次技术。接下来的一系列文章里,我会结合Qone应用的实例,向大家展示java语言中的一些高级特性及应用技巧。

  序的调试是开发人员的主要工作之一,结合JVM良好的设计及IDE强大的调试功能,程序的本地调试已经是很轻松容易的事情。可是,当程序部署到客户现场以后,调试就不是那么容易了。客户可能在外地(没办法现场调试)、客户的数据不能提供给开发人员、程序日志记录信息太少、客户对程序问题描述不清等等,这些情况都导致开发人员解决程序问题的时间大大延长。近来,随着Qone市场的不断扩大,我们开发人员为客户解决问题的工作也越来越多,下面的场景相信很多开发人员都比较熟悉:
  客服人员反馈了一个客户的bug,并附上了程序日志。开发人员根据日志判断出程序的某行代码出现了空指针,可是根据代码的逻辑判断这个地方是不应该出空指针的(初步判断)。为了进一步判断问题原因,就同客户商量能不能把数据库拷给我们,一般情况下这种要求客户都是不答应的。此路不通,怎么办那。好吧,我们写一些sql语句发给客户,让客户执行,然后把结果反馈给我们,并且这样的过程往往要反复几次,才能最终定位问题。这样处理问题的过程带来两个问题1、降低了问题的处理速度2、占用了客户的一部分时间,难免造成客户的不满意。
  上面这个场景里,虽然浪费了一定的时间,最终毕竟还是把问题解决了。可是,另外一类问题,就不是那么好解决的了。最近,我在处理Qone关闭问题时,就碰到了这样的情况。Qone启动一段时间后,会不定期的reload。这种reload同Qone的使用没有关系,从日志上也不是由Qone 程序引起的。初步怀疑是同系统里的某些软件发生冲突导致,可具体的原因无论通过tomcat日志还是Qone日志都是分析不出来的,并且这类问题往往同客户的具体软硬件环境相关,很难在我们这边重现。
  为了解决上面提到的问题,我们很自然想到,如果可以在客户那里建立一个类似于本地开发的调试环境就好了,在代码里设置好断点,当异常发生时停到断点处,断点处的调用栈、变量名称、变量值一览无余,这样我们的调试就方便多了。当然,在客户现场建立一个图形化的调试环境基本上不可能。不过没关系,没有图形化的调试环境,我们可以把程序执行过程中的数据序列化到文件,然后随程序日志一起发给我们,这样就跟我们平时的调试差不多了。
  总结一下这种调试方案的内容是:
  提供一种机制可以获取程序执行到制定位置时的运行时数据(调用栈、局部变量、方法参数等等),并将这些数据序列化成
文件。
  要实现上面的调试方案,有三种技术方案,下面引入第一种方案。
技术方案一
1.准备工作
  上面我已经提到过我们是要仿照本地IDE的调试方式,因此自然的想到可以利用jvm提供的调试框架来实现我们的想法,这里要用到JavaTM Platform Debugger Architecture(JPDA)(http://java./j2se/1.5.0/docs/guide/jpda/)技术。具体内容大家参考链接里的JPDA文档,这个文档的内容还比较多,大家可以先看Java Debug Interface (JDI)这部分内容,也是方案一主要用到API。SUN的文档里对JDI文字性的介绍很少,只有javadoc和几个example。那几个 example还是不错的,基本上包含了JDI的所有特性,而方案一就是在trace那个example的基础上改的,所以建议大家仔细看看。
2.方案内容
  好的,准备工作完成以后,就开始我们实现我们的方案:
  第一步:开启服务器的remote debug功能
  为了调试tomcat上的程序,我们需要开启jvm上的Remote Debug功能。Remote Debug一直是我比较喜欢的调试方式,这种方式比较灵活,不要求debugger同application在同一个虚拟机上,而调试功能同本机调试没有任何区别。在tomcat中开始Remote Debug的具体做法为,修改catalina.bat文件,找到”set JPDA=”,并添加“
set JPDA_ADDRESS=6473
set JPDA_TRANSPORT=dt_socket”

其中6473是监听的端口号,大家可以按照自己的习惯修改。

修改完成后,启动tomat也与以往稍有不同,需要通过catalina jpda start(run)命令启动。
  第二步:连接tomcat服务器
  启动了tomat之后,就要把我们的程序连接到debug接口上,代码如下:
//获得系统所有的connectors
List connectors = Bootstrap.virtualMachineManager().allConnectors();
List vms = Bootstrap.virtualMachineManager().connectedVirtualMachines();
Iterator iter = connectors.iterator();
while (iter.hasNext()) {
Connector connector = (Connector) iter.next();
if (connector.name().equals("com.sun.jdi.SocketAttach")) {
return (com.sun.jdi.connect.AttachingConnector) connector;
}
}
我们知道JDI只是定义了一系列的接口,而具体的实现是由JVM做的。所以,首先我们要获得JVM为我们提供的所有connectors。这些connector包括
com.sun.jdi.CommandLineLaunch (defaults: home=D:Program FilesJavajdk1.6jre, options=, main=, suspend=true, quote=", vmexec=java)

com.sun.jdi.RawCommandLineLaunch (defaults: command=, quote=", address=),

com.sun.jdi.SocketAttach (defaults: timeout=, hostname=thoth, port=), com.sun.jdi.SocketListen (defaults: timeout=, port=, localAddress=), com.sun.jdi.SharedMemoryAttach (defaults: timeout=, com.sun.jdi.SharedMemoryListen (defaults: timeout=, com.sun.jdi.ProcessAttach (defaults: pid=, timeout=)
括号里面是这些connectors的参数。这里因为我们要连接Remote jvm,因此选择了com.sun.jdi.SocketAttach
接下来,要为SocketAttach connector设置参数,代码如下:
Map arguments = connector.defaultArguments();

Connector.IntegerArgument port_arg = (Connector.IntegerArgument) arguments.get("port");

port_arg.setValue(port);
Connector.IntegerArgument timeout_arg = (Connector.IntegerArgument) arguments
.get("timeout");
timeout_arg.setValue(delay);
Connector.StringArgument hostname_arg = (Connector.StringArgument) arguments
.get("hostname");
hostname_arg.setValue(host);
接下来,就是利用连接参数,连接remote jvm,代码如下:
VirtualMachine vm = connector.attach(arguments);
VirtualMachine对象可以说是整个技术方案一的关键,后续一系列工作都要依赖这个对象,好我们继续到下一步。
  第三步:设置断点
  这一步就是仿照debugger的功能为我们的程序设置断点,看代码
Iterator iter = vm.allClasses().iterator();
while (eventRequest == null && iter.hasNext()) {
ReferenceType refType = (ReferenceType) iter.next();
if (refType.isPrepared() && refType.name().equals(className)) {
}
}
其中className是我们准备下断点的类名,这段程序找到className对应的ReferenceType,继续代码:
Location location = null;
List locations = rt.locationsOfLine(lineNumber);
if (locations.size() > 0) {
location = (Location) locations.get(0);
}
lineNumber是我们准备下断点的行,程序的目的是获得lineNumber对应的location对象,
有了ReferenceType和Location后,我们就可以为vm添加断点了,代码如下:
EventRequestManager erm = vm.eventRequestManager();
EventRequest er = erm.createBreakpointRequest(location);
er.setSuspendPolicy(EventRequest.SUSPEND_ALL);
er.enable();
  第四步:创建监听程序
  在JDI的体系架构中,将程序的执行处理成一系列的事件,而debugger就是处理这些事件来实现debug的目的。那么,很自然的在我们的方案中需要建立一个监听线程来接受来自remote jvm的event,然后对这些event进行处理。所有JDI支持的event可以在com.sun.jdi.event.下找到
线程初始化的代码比较简单,就是把vm对象传给线程对象,这里就不写了,直接看线程的run方法:
EventQueue queue = vm.eventQueue();
while (connected) {
             EventSet eventSet = queue.remove();
             EventIterator it = eventSet.eventIterator();
             while (it.hasNext()) {
                 handleEvent(it.nextEvent());
             eventSet.resume();
         } catch (InterruptedException exc) {
         } catch (VMDisconnectedException discExc) {
             handleDisconnectedException();
}
首先,我们获得vm的event序列,这时候程序处于挂起状态,然后获得event序列里面的内容,对内容处理后,恢复程序的执行。下面我们来看handleEvent方法,代码如下:
if (event instanceof BreakpointEvent){
BreakpointEvent evt = (BreakpointEvent) event;
try {
List<StackFrame> statckFrames=evt.thread().frames();
for(StackFrame statckFrame:statckFrames){               System.out.println(statckFrame.toString());
}
} catch (IncompatibleThreadStateException e) {
e.printStackTrace();
}
}
statckFrames=evt.thread().frames();statckFrames就是按照调用顺序排列的调用栈中的各个对象及方法、变量。考察StackFrame我们可以看到
List<LocalVariable> visibleVariables()
ObjectReference thisObject();
LocalVariable visibleVariableByName(String paramString)
getValue(LocalVariable paramLocalVariable);
List<Value> getArgumentValues();
。。。。
  有了这些方法以后,获得程序运行时的变量就很容易了。
  第五步:序列化
  这部分比较简单,就不贴代码了。
3.方案总结
  这个方案实现了之前我们所有的需求:获得调用栈,获得程序执行过程中的变量信息。并且对原有的程序没有任何的侵入性。可以想象,有了这个程序以后以后,我们为客户解决问题的程序是这样的:
  1、收到用户反馈及日志,初步定位问题的位置
  2、针对问题的位置编写debug程序,以便获得程序运行时信息
  3、将程序发给用户,运行程序以后,获得序列化的程序运行时信息
  4、用户将结果反馈给开发人员,开发人员分析结果后,给出问题的准确原因
  不过这个方案也有一个很大的缺点,是效率很差。大家应该注意到监听线程里面那段while语句,这意味着remote程序产生的每一个 event都要经过debug程序处理一下,可想而知效率是非常低的。那么能不能在尽量不影响程序效率的前提下,实现我们的目的呢,具体方案,且听下回分解。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多