环境要求: 本文的代码是?nbsp;Eclipse3.0.0QJDK1.4.2 ?nbsp;Tomcat5.0.5 上测试过的?br>
JAVA 调试框架QJPDAQ简?/strong>
JPDA 是一个多层的调试框架Q包?nbsp;JVMDI、JDWP、JDI 三个层次。JAVA 虚拟机提供了 JPDA 的实现。其开发工具作试客L(fng)Q可以方便的与虚拟机通讯Q进行调试。Eclipse 正是利用 JPDA 调试 JAVA 应用Q事实上Q所?nbsp;JAVA 开发工具都是这样做的。SUN JDK q带了一个比较简单的调试工具以及(qing)CZ?br>
- JVMDI 定义了虚拟机需要实现的本地接口
- JDWP 定义了JVM与调试客L(fng)之间的通讯协议
调试客户端和JVM 既可以在同一台机器上Q也可以q程调试。JDK ?x)包含一个默认的实现 jdwp.dllQJVM 允许灉|的用其他协议代?nbsp;JDWP。SUN JDK 有两U方式传输通讯协议QSocket 和共享内?后者仅仅针?nbsp;Windows)Q一般我们都采用 Socket 方式?br>
你可以用下面的参敎ͼ以调试模式启动JVM
-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n -Xrunjdwp JVM 加蝲 jdwp.dll transport=dt_socket 使用 Socket 传输 address 表示调试端口?nbsp; server=y 表示 JVM 作ؓ(f)服务器,建立 Socket suspend=n 表示启动q程中,JVM 不会(x)挂vȝ待调试客L(fng)q接
- JDI 则是一lJAVA接口
如果是一?nbsp;JAVA 的调试客L(fng)Q只要实?nbsp;JDI 接口Q利用JDWP协议Q与虚拟机通讯Q就可以调用JVMDI了?br>
Components Debugger Interface / |-----------------------| / | VM | debuggee ----( |-----------------------| <------- JVMDI - Java VM Debug Interface \ | back-end | \ |-----------------------| / | comm channel -( | <--------------- JDWP - Java Debug Wire Protocol \ | |---------------------| | front-end | |---------------------| <------- JDI - Java Debug Interface | UI | |---------------------|
参见Q?a >http://java.sun.com/j2se/1.4.2/docs/guide/jpda/architecture.html
Eclipse作ؓ(f)一个基?nbsp;JAVA 的调试客L(fng)Q利?nbsp;org.eclipse.jdt.debug Plugin 提供了JDI 的具体实现。JDI 接口主要包含下面 4 个包
com.sun.jdi com.sun.jdi.connect com.sun.jdi.event com.sun.jdi.request本文不对 JDI q行深入阐述Q这里重点介l?nbsp;JDI 中与断点相关的接口?br>
- com.sun.jdi
主要是JVM(VirtualMachine) U程(ThreadReference) 调用?StackFrame) 以及(qing)cd、实例的描述。利用这l接口,调试客户端可以用cMcd的方式Q得到所有类型的定义Q动态调?nbsp;Class 的方法?nbsp; - com.sun.jdi.event
装了JVM 产生的事Ӟ JVM 正是这些事仉知l调试客L(fng)的。例?nbsp;BreakpointEvent 是 JVM 执行到断点的时候,发出的事ӞClassPrepareEvent是 Class 被加载时发出的事件?nbsp;
- com.sun.jdi.request
装了调试客L(fng)可以?nbsp;JVM发v的请求。例?nbsp;BreakpointRequest ?nbsp;JVM 发v一个添加断点的hQClassPrepareRequest ?nbsp;JVM 注册一个类加蝲hQJVM 在加载指?nbsp;Class 的时候,׃(x)发出一?nbsp;ClassPrepareEvent 事g?nbsp;
JSR-45(Debugging Support for Other Languages)为那些非 JAVA 语言写成Q却需要编译成 JAVA 代码Q运行在 JVM 中的E序Q提供了一个进行调试的标准机制。也许字面的意思有点不好理解,什么算是非 JAVA 语言呢?其实 JSP 是一个再好不q的例子QJSR-45 的样例就是一?nbsp;JSP?br>
JSP的调试一直依赖于具体应用服务器的实现Q没有一个统一的模式,JSR-45 针对q种情况Q提供了一个标准的模式。我们知道,JAVA 的调试中Q主要根据行号作为标志,q行定位。但?nbsp;JSP 被编译ؓ(f) JAVA 代码之后QJAVA 行号?nbsp;JSP 行号无法一一对应Q怎样解决呢?
JSR-45 是这栯定的QJSP 被编译成 JAVA 代码Ӟ同时生成一?nbsp;JSP 文g名和行号?nbsp;JAVA 行号之间的对应表(SMAP)。JVM 在接受到调试客户端请求后Q可以根据这个对应表(SMAP)Q从 JSP 的行可{换到 JAVA 代码的行PJVM 发出事g通知? 也根据对应表(SMAP)q行转化Q直接将 JSP 的文件名和行号通知调试客户端?br>
我们?nbsp;Tomcat 5.0 做个试Q有两个 JSPQHello.jsp ?nbsp;greeting.jspQ前?nbsp;include 后者。Tomcat?x)将他们~译?nbsp;JAVA 代码(Hello_jsp.java)QJAVA Class(Hello_jsp.class) 以及(qing) JSP 文g?行号?nbsp;JAVA 行号之间的对应表(SMAP)?br>
Hello.jsp:
1 <HTML> 2 <HEAD> 3 <TITLE>Hello Example</TITLE> 4 </HEAD> 5 <BODY> 6 <%@ include file="greeting.jsp" %> 7 </BODY> 8 </HTML>
greeting.jsp:
1 Hello There!<P> 2 Goodbye on <%= new java.util.Date() %>
JSP ~译后生的Hello_jsp.java 如下:
Hello_jsp.java: 1 package org.apache.jsp; 2 3 import javax.servlet.*; 4 import javax.servlet.http.*; 5 import javax.servlet.jsp.*; 6 7 public final class Hello_jsp extends org.apache.jasper.runtime.HttpJspBase 8 implements org.apache.jasper.runtime.JspSourceDependent { 9 10 private static java.util.Vector _jspx_dependants; 11 12 static { 13 _jspx_dependants = new java.util.Vector(1); 14 _jspx_dependants.add("/greeting.jsp"); 15 } 16 17 public java.util.List getDependants() { 18 return _jspx_dependants; 19 } 20 21 public void _jspService(HttpServletRequest request, HttpServletResponse response) 22 throws java.io.IOException, ServletException { 23 24 JspFactory _jspxFactory = null; 25 PageContext pageContext = null; 26 HttpSession session = null; 27 ServletContext application = null; 28 ServletConfig config = null; 29 JspWriter out = null; 30 Object page = this; 31 JspWriter _jspx_out = null; 32 33 34 try { 35 _jspxFactory = JspFactory.getDefaultFactory(); 36 response.setContentType("text/html"); 37 pageContext = _jspxFactory.getPageContext(this, request, response, 38 null, true, 8192, true); 39 application = pageContext.getServletContext(); 40 config = pageContext.getServletConfig(); 41 session = pageContext.getSession(); 42 out = pageContext.getOut(); 43 _jspx_out = out; 44 45 out.write("<HTML> \r\n"); 46 out.write("<HEAD> \r\n"); 47 out.write("<TITLE>Hello Example"); 48 out.write("</TITLE> \r\n"); 49 out.write("</HEAD> \r\n"); 50 out.write("<BODY> \r\n"); 51 out.write("Hello There!"); 52 out.write("<P> \r\nGoodbye on "); 53 out.write(String.valueOf( new java.util.Date() )); 54 out.write(" \r\n"); 55 out.write(" \r\n"); 56 out.write("</BODY> \r\n"); 57 out.write("</HTML> \r\n"); 58 } catch (Throwable t) { 59 if (!(t instanceof javax.servlet.jsp.SkipPageException)){ 60 out = _jspx_out; 61 if (out != null && out.getBufferSize() != 0) 62 out.clearBuffer(); 63 if (pageContext != null) pageContext.handlePageException(t); 64 } 65 } finally { 66 if (_jspxFactory != null) _jspxFactory.releasePageContext ( pageContext); 67 } 68 } 69 }
Tomcat 又将q个 JAVA 代码~译?nbsp;Hello_jsp.classQ他们位于:(x) $Tomcat_install_path$\work\Standalone\localhost\_ 目录下。但?nbsp;JSP 文g?行号?nbsp;JAVA 行号的对应表(以下USMAP) 在哪里呢Q答案是Q它保存?nbsp;Class 中。如果用 UltraEdit 打开q个 Class 文gQ就可以扑ֈ SourceDebugExtension 属性,q个属性用来保?nbsp;SMAP?br>
JVM 规范定义?nbsp;ClassFile 中可以包?nbsp;SourceDebugExtension 属性,保存 SMAPQ?br>
SourceDebugExtension_attribute { u2 attribute_name_index; u4 attribute_length; u1 debug_extension[attribute_length]; }
我用 javassist 做了一个测?javassist可是一个好东东Q它可以动态改变Class的结构,JBOSS ?nbsp;AOP利用了javassistQ这里我们只使用它读取ClassFile的属?
public static void main(String[] args) throws Exception{ String[]files = { "E:\\Tomcat5_0_5\\work\\Catalina\\localhost\\_\\org\\apache\\jsp\\Hello_jsp.class", }; for(int k = 0; k < files.length; k++){ String file = files[k]; System.out.println("Class : " + file); ClassFile classFile = new ClassFile(new DataInputStream(new FileInputStream(file))); AttributeInfo attributeInfo = classFile.getAttribute("SourceDebugExtension"); System.out.println("attribute name :" + attributeInfo.getName() + "]\n\n"); byte[]bytes = attributeInfo.get(); String str = new String(bytes); System.out.println(str); } }
q段代码昄了SourceDebugExtension 属性,你可以看到SMAP 的内宏V编译JSP后,SMAP p写入 Class ? 你也可以利用 javassist 修改 ClassFile 的属性?br>
下面是 Hello_jsp.class 中保存的 SMAP 内容:
SMAP E:\Tomcat5_0_5\work\Catalina\localhost\_\org\apache\jsp\Hello_jsp.java JSP *S JSP *F + 0 Hello.jsp /Hello.jsp + 1 greeting.jsp /greeting.jsp *L 1:45 2:46 3:47 3:48 4:49 5:50 1#1:51 1:52 2:53 7#0:56 8:57 *E
首先注明JAVA代码的名Uͼ(x)Hello_jsp.javaQ然后是 stratum 名称Q?nbsp;JSP。随后是两个JSP文g的名U?nbsp;QHello.jsp、greeting.jsp。两个JSP文g?0行,产生的Hello_jsp?9行代码。最后也是最重要的内容就是源文g文g?行号和目标文件行L(fng)对应关系(*L ?nbsp;*E之间的部?
在规范定义了q样的格式:(x)
源文件行?nbsp;# 源文件代?重复ơ数 : 目标文g开始行?目标文g行号每次增加的数?nbsp;
(InputStartLine # LineFileID , RepeatCount : OutputStartLine , OutputLineIncrement)
源文件行?InputStartLine) 目标文g开始行?OutputStartLine) 是必ȝ。下面是对这个SMAP具体的说明:(x)
1:45 2:46 3:47 3:48 4:49 5:50(没有源文件代P默认为Hello.jsp) 开始行?nbsp; l束行号 Hello.jsp: 1 -> Hello_jsp.java: 45 2 -> 46 3 -> 47 48 4 -> 49 5 -> 50 1#1:51 1:52 2:53(1#1表示 greeting.jsp 的第1? greeting.jsp: 1 -> Hello_jsp.java: 51 52 2 -> 53 7#0:56 8:57(7#0表示 Hello.jsp 的第7? Hello.jsp: 7 -> Hello_jsp.java: 56 8 -> 57
Eclipse 提供?nbsp;TextEditorQ作为文本编辑器的父cR由?nbsp;Editor 的开发不是本文的重点Q不做具体论q。我们可以利?nbsp;Eclipse ?nbsp;Plugin 目向导Q生成一个简单的 JSP ~辑器:(x)
(1)点击 File 菜单QNew -> Project -> Plug-in Project Q?br>
(2)输入目名称 JSP_DEBUGQ下一步;
(3)输入 plugin ID Q?nbsp;com.jsp.debug
Plugin Class name Q?nbsp;com.jsp.debug.JSP_DebugPlugin
(4)选择用模板创?br>
使用 Plug-in with editorQ输?br>
Java Package Name Qcom.jsp.editors
Editor Class Name QJSPEditor
File extension Qjsp
一?nbsp;jsp editor ׃生了?br>
q行q个PluginQ新Z个JAVA目Q新Z?nbsp;Hello.jsp ?nbsp;greeting.jspQ在 Navigator 视图双击 jspQ这个editor打开了?br>
在JSP~辑器中讄断点
在编辑器中添加断点的操作方式有两U,一U是在编辑器左侧垂直标尺上双击,另一U是在左侧垂直标Z点击鼠标右键Q选择菜单"d/删除断点"?br>
?nbsp;Eclipse 的实CQ添加断点实际上是?nbsp;IFile d一个marker Q类型是IBreakpoint.BREAKPOINT_MARKERQ然后将断点注册?nbsp;BreakpointManager?br>
BreakpointManager 生一?nbsp;BreakpointRequestQ通知正在q行的JVM TargetQ如果此时还没有启动 JVMQ会(x)?nbsp;JVM 启动的时候,所有断点一起通知 JVM Target?br>
d断点使用一?nbsp;AbstractRulerActionDelegateQ重?nbsp;createAction Ҏ(gu)Q返回一?nbsp;IAction ManageBreakpointRulerAction动作Q?br>
public class ManageBreakpointRulerActionDelegate extends AbstractRulerActionDelegate{ protected IAction createAction(ITextEditor editor, IVerticalRulerInfo rulerInfo) { return new ManageBreakpointRulerAction(rulerInfo, editor); } }
Z?nbsp;ManageBreakpointRulerActionDelegate d到文本编辑器左侧标尺的鼠标右键菜单,q且能够处理左侧标尺的鼠标双MӞ?nbsp;plugin.xml 中加入定义?br>
处理双击事gQ?br>
<extension point="org.eclipse.ui.editorActions"> <editorContribution targetID="com.jiaoly.editors.JSPEditor" id="com.jiaoly.debug.ManageBreakpointRulerActionDelegate"> <action label="d/删除断点" class="com.jiaoly.debug.ManageBreakpointRulerActionDelegate" actionID="RulerDoubleClick" id="com.jiaoly.debug.ManageBreakpointRulerActionDelegate"> </action> </editorContribution> </extension>
d右键菜单Q?br>
<extension point="org.eclipse.ui.popupMenus"> <viewerContribution targetID="#TextRulerContext" id="com.jiaoly.debug.ManageBreakpointRulerActionDelegate"> <action label="d/删除断点" class="com.jiaoly.debug.ManageBreakpointRulerActionDelegate" menubarPath="addition" id="com.jiaoly.debug.ManageBreakpointRulerActionDelegate"> </action> </viewerContribution> </extension>
ManageBreakpointRulerAction 是实际添加断点的ActionQ实C IUpdate 接口Q这个Action的工作,是判断当前选中行是否存在断点类型的 MarkerQ如果不存在创徏一个,如果存在Q将它删除?br>
public class ManageBreakpointRulerAction extends Action implements IUpdate{ private IVerticalRulerInfo rulerInfo; private ITextEditor textEditor; private String BPmarkerType ; //当点Marker的类?nbsp; private List allMarkers; //当前鼠标点击行所有的Marker private String addBP; //Action 的显C名U?nbsp; public ManageBreakpointRulerAction(IVerticalRulerInfo ruler, ITextEditor editor){ this.rulerInfo = ruler; this.textEditor = editor; BPmarkerType = IBreakpoint.BREAKPOINT_MARKER; addBP = "d/删除断点"; //$NON-NLS-1$ setText(this.addBP); } public void update() { this.allMarkers = this.fetchBPMarkerList(); } public void run(){ if(this.allMarkers.isEmpty()) this.addMarker(); else this.removeMarkers(this.allMarkers); } }