本文通过开发一个JSP 编辑器插件的示例,介绍了 Eclipse 中设置 JSP 断点的方法,以及如何远程调试 JSP。作为基础知识,本文的前两部分描述了 JAVA Debug 和 JSR-45 的基本原理。
环境要求: 本文的代码是在 Eclipse3.0.0,JDK1.4.2 和 Tomcat5.0.5 上测试过的。
JAVA 调试框架(JPDA)简介 JPDA 是一个多层的调试框架,包括 JVMDI、JDWP、JDI 三个层次。JAVA 虚拟机提供了 JPDA 的实现。其开发工具作为调试客户端,可以方便的与虚拟机通讯,进行调试。Eclipse 正是利用 JPDA 调试 JAVA 应用,事实上,所有 JAVA 开发工具都是这样做的。SUN JDK 还带了一个比较简单的调试工具以及示例。
- JVMDI 定义了虚拟机需要实现的本地接口
- JDWP 定义了JVM与调试客户端之间的通讯协议
调试客户端和JVM 既可以在同一台机器上,也可以远程调试。JDK 会包含一个默认的实现 jdwp.dll,JVM 允许灵活的使用其他协议代替 JDWP。SUN JDK 有两种方式传输通讯协议:Socket 和共享内存(后者仅仅针对 Windows),一般我们都采用 Socket 方式。
你可以用下面的参数,以调试模式启动JVM
-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n
-Xrunjdwp JVM 加载 jdwp.dll
transport=dt_socket 使用 Socket 传输
address 表示调试端口号
server=y 表示 JVM 作为服务器,建立 Socket
suspend=n 表示启动过程中,JVM 不会挂起去等待调试客户端连接
- JDI 则是一组JAVA接口
如果是一个 JAVA 的调试客户端,只要实现 JDI 接口,利用JDWP协议,与虚拟机通讯,就可以调用JVMDI了。
下图为 JPDA 的基本架构:
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 |
|---------------------|
参见:
http://java.sun.com/j2se/1.4.2/docs/guide/jpda/architecture.html Eclipse作为一个基于 JAVA 的调试客户端,利用 org.eclipse.jdt.debug Plugin 提供了JDI 的具体实现。JDI 接口主要包含下面 4 个包
com.sun.jdi
com.sun.jdi.connect
com.sun.jdi.event
com.sun.jdi.request
本文不对 JDI 进行深入阐述,这里重点介绍 JDI 中与断点相关的接口。
- com.sun.jdi
主要是JVM(VirtualMachine) 线程(ThreadReference) 调用栈(StackFrame) 以及类型、实例的描述。利用这组接口,调试客户端可以用类似类反射的方式,得到所有类型的定义,动态调用 Class 的方法。
- com.sun.jdi.event
封装了JVM 产生的事件, JVM 正是将这些事件通知给调试客户端的。例如 BreakpointEvent 就是 JVM 执行到断点的时候,发出的事件;ClassPrepareEvent就是 Class 被加载时发出的事件。
- com.sun.jdi.request
封装了调试客户端可以向 JVM发起的请求。例如 BreakpointRequest 向 JVM 发起一个添加断点的请求;ClassPrepareRequest 向 JVM 注册一个类加载请求,JVM 在加载指定 Class 的时候,就会发出一个 ClassPrepareEvent 事件。
JSR-45规范 JSR-45(Debugging Support for Other Languages)为那些非 JAVA 语言写成,却需要编译成 JAVA 代码,运行在 JVM 中的程序,提供了一个进行调试的标准机制。也许字面的意思有点不好理解,什么算是非 JAVA 语言呢?其实 JSP 就是一个再好不过的例子,JSR-45 的样例就是一个 JSP。
JSP的调试一直依赖于具体应用服务器的实现,没有一个统一的模式,JSR-45 针对这种情况,提供了一个标准的模式。我们知道,JAVA 的调试中,主要根据行号作为标志,进行定位。但是 JSP 被编译为 JAVA 代码之后,JAVA 行号与 JSP 行号无法一一对应,怎样解决呢?
JSR-45 是这样规定的:JSP 被编译成 JAVA 代码时,同时生成一份 JSP 文件名和行号与 JAVA 行号之间的对应表(SMAP)。JVM 在接受到调试客户端请求后,可以根据这个对应表(SMAP),从 JSP 的行号转换到 JAVA 代码的行号;JVM 发出事件通知前, 也根据对应表(SMAP)进行转化,直接将 JSP 的文件名和行号通知调试客户端。
我们用 Tomcat 5.0 做个测试,有两个 JSP,Hello.jsp 和 greeting.jsp,前者 include 后者。Tomcat会将他们编译成 JAVA 代码(Hello_jsp.java),JAVA Class(Hello_jsp.class) 以及 JSP 文件名/行号和 JAVA 行号之间的对应表(SMAP)。
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 又将这个 JAVA 代码编译为 Hello_jsp.class,他们位于: $Tomcat_install_path$\work\Standalone\localhost\_ 目录下。但是 JSP 文件名/行号和 JAVA 行号的对应表(以下简称SMAP) 在哪里呢?答案是,它保存在 Class 中。如果用 UltraEdit 打开这个 Class 文件,就可以找到 SourceDebugExtension 属性,这个属性用来保存 SMAP。
JVM 规范定义了 ClassFile 中可以包含 SourceDebugExtension 属性,保存 SMAP:
SourceDebugExtension_attribute {
u2 attribute_name_index;
u4 attribute_length;
u1 debug_extension[attribute_length];
}
我用 javassist 做了一个测试(javassist可是一个好东东,它可以动态改变Class的结构,JBOSS 的 AOP就利用了javassist,这里我们只使用它读取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);
}
}
这段代码显示了SourceDebugExtension 属性,你可以看到SMAP 的内容。编译JSP后,SMAP 就被写入 Class 中, 你也可以利用 javassist 修改 ClassFile 的属性。
下面就是 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代码的名称:Hello_jsp.java,然后是 stratum 名称: JSP。随后是两个JSP文件的名称 :Hello.jsp、greeting.jsp。两个JSP文件共10行,产生的Hello_jsp共69行代码。最后也是最重要的内容就是源文件文件名/行号和目标文件行号的对应关系(*L 与 *E之间的部分)
在规范定义了这样的格式:
源文件行号 # 源文件代号,重复次数 : 目标文件开始行号,目标文件行号每次增加的数量
(InputStartLine # LineFileID , RepeatCount : OutputStartLine , OutputLineIncrement)
源文件行号(InputStartLine) 目标文件开始行号(OutputStartLine) 是必须的。下面是对这个SMAP具体的说明:
1:45 2:46 3:47 3:48 4:49 5:50(没有源文件代号,默认为Hello.jsp)
开始行号 结束行号
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
开发一个JSP编辑器 Eclipse 提供了 TextEditor,作为文本编辑器的父类。由于 Editor 的开发不是本文的重点,不做具体论述。我们可以利用 Eclipse 的 Plugin 项目向导,生成一个简单的 JSP 编辑器:
(1)点击 File 菜单,New -> Project -> Plug-in Project ;
(2)输入项目名称 JSP_DEBUG,下一步;
(3)输入 plugin ID : com.jsp.debug
Plugin Class name : com.jsp.debug.JSP_DebugPlugin
(4)选择用模板创建
使用 Plug-in with editor,输入
Java Package Name :com.jsp.editors
Editor Class Name :JSPEditor
File extension :jsp
一个 jsp editor 就产生了。
运行这个Plugin,新建一个JAVA项目,新建一个 Hello.jsp 和 greeting.jsp,在 Navigator 视图双击 jsp,这个editor就打开了。
在JSP编辑器中设置断点 在编辑器中添加断点的操作方式有两种,一种是在编辑器左侧垂直标尺上双击,另一种是在左侧垂直标尺上点击鼠标右键,选择菜单"添加/删除断点"。
在 Eclipse 的实现中,添加断点实际上就是为 IFile 添加一个marker ,类型是IBreakpoint.BREAKPOINT_MARKER,然后将断点注册到 BreakpointManager。
BreakpointManager 将产生一个 BreakpointRequest,通知正在运行的JVM Target,如果此时还没有启动 JVM,会在 JVM 启动的时候,将所有断点一起通知 JVM Target。
添加断点使用一个 AbstractRulerActionDelegate,重载 createAction 方法,返回一个 IAction ManageBreakpointRulerAction动作:
public class ManageBreakpointRulerActionDelegate extends AbstractRulerActionDelegate{
protected IAction createAction(ITextEditor editor, IVerticalRulerInfo rulerInfo) {
return new ManageBreakpointRulerAction(rulerInfo, editor);
}
}
为了将 ManageBreakpointRulerActionDelegate 添加到文本编辑器左侧标尺的鼠标右键菜单,并且能够处理左侧标尺的鼠标双击事件,在 plugin.xml 中加入定义。
处理双击事件:
<extension point="org.eclipse.ui.editorActions">
<editorContribution
targetID="com.jiaoly.editors.JSPEditor"
id="com.jiaoly.debug.ManageBreakpointRulerActionDelegate">
<action
label="添加/删除断点"
class="com.jiaoly.debug.ManageBreakpointRulerActionDelegate"
actionID="RulerDoubleClick"
id="com.jiaoly.debug.ManageBreakpointRulerActionDelegate">
</action>
</editorContribution>
</extension>
添加右键菜单:
<extension point="org.eclipse.ui.popupMenus">
<viewerContribution
targetID="#TextRulerContext"
id="com.jiaoly.debug.ManageBreakpointRulerActionDelegate">
<action
label="添加/删除断点"
class="com.jiaoly.debug.ManageBreakpointRulerActionDelegate"
menubarPath="addition"
id="com.jiaoly.debug.ManageBreakpointRulerActionDelegate">
</action>
</viewerContribution>
</extension>
ManageBreakpointRulerAction 是实际添加断点的Action,实现了 IUpdate 接口,这个Action的工作,就是判断当前选中行是否存在断点类型的 Marker,如果不存在创建一个,如果存在,将它删除。
public class ManageBreakpointRulerAction extends Action implements IUpdate{
private IVerticalRulerInfo rulerInfo;
private ITextEditor textEditor;
private String BPmarkerType ; //当点Marker的类型
private List allMarkers; //当前鼠标点击行所有的Marker
private String addBP; //Action 的显示名称
public ManageBreakpointRulerAction(IVerticalRulerInfo ruler, ITextEditor editor){
this.rulerInfo = ruler;
this.textEditor = editor;
BPmarkerType = IBreakpoint.BREAKPOINT_MARKER;
addBP = "添加/删除断点"; //$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);
}
}