比较分析不同Java程序调试环境的异同和优缺点。
充分利用 J2EE 调试工具--Java 开发环境的增强功能有助于调试J2EE 应用程序
我的第一个 Java 项目包括接管由我所在公司的一个部门编写的一个门户,该部门现在已经关闭了。当我凝视着运行门户代码的服务器终端时,我看到一行接一行的调试代码涌现在在屏幕上。进一步的研究之后,我发现 JavaServer Pages ( JSP )和 servlet 中到处都是 Systemoutprintln (“ [Debug Statement] ”)。由于我对我接管的语言和代码有些陌生,我坚持自己为终端编写调试语句,因为我不知道调试 Java 代码的其他方法。
然而,在漫长而乏味的调试过程中,我不断失败。我发现在将代码提交给 QA 之前很难移除所有的调试语句。同样,调试涉及到重新编译、重新部署的代码和在执行代码时观察终端也非常困难。为了查找代码中断的点,我时常在方法中的每一行代码后加入 println 语句,在这种情况下,过程常常为找到的每一个故障重复。
最后,在我发现 Java 平台调试架构( Java Platform Debugger Architecture , JPDA )的时候,我摆脱了调试梦魇。 JPDA 是一套组成构建调试应用程序框架的 API 。幸运的是,我们大多数人不需要自己考虑创建调试应用程序的事,因为这些是与主要的 IDE 捆绑在一起提供的。调试在这些新的 IDE 中是一个相当简单和轻松的过程。
您必须执行几个步骤才可以使您的开发环境成为您可以进行调试的环境。如果您正在使用 Sun 的 Java Virtual Machine ( JVM )进行调试,您必须在命令行中指定启动 J2EE 容器的时间,其中, JVM 已经以调试模式启动。为了执行该操作,只需在 java 命令的后边添加 -Xdebug 参数。我们随后将研究一个命令行调试参数的示例。为了加载 Java Debug Wire Protocol ( JDWP ) 的 JPDA 参数实现,需要使用具有随后指定的 JPDA 选项的 -Xrunjdwp 。该参数加载进行中的调试资料库,而其子选项提供了调试服务器如何与调试客户端交互的细节。我们将研究一份 JPDA 子选项列表,该列表可以帮助设置正确的调试环境。
首先,指定传输选项。 transport 用于在调试程序和 VM 使用的进程之间通讯。 Win32 平台的 VM 提供了很多其他平台使用的共享内存传输和套接字传输。共享的内存传输(仅在 Win32 平台中受支持)要求调试应用程序和目标 VM 存放在相同的机器中。套接字传输使用标准的 TCP/IP 套接字来与调试信息通信。调试客户端和调试服务器可以位于使用套接字传输的相同或者不同的机器中。对于远程调试 Enterprise JavaBeans ( EJB )和 servlet 来说,我们关注于使用套接字传输,因为它受所有平台支持:
transport=dt_socket
我们正在启动的 VM 需要作为调试服务器执行。如前所述,调试服务器是运行以调试模式编译的应用程序的 JVM (以后讲述),并具有允许客户端连接和 监听 应用程序的开放套接字。为了使 VM 成为一台服务器,需要提供服务器选项,并将其值设置为 yes :
server=yes
接下来,我们需要指出调试服务器将要使用的端口号,同时也是端口客户端用来连接服务器的端口号。只有知道正在运行服务器的端口号的客户端才可以建立连接,因为不存在标准的调试端口,也就是 HTTP 服务器。任何未使用的端口都适用。在我们的情况中,我们使用了端口 4000 :
address=4000
避免混乱
您可以提供一个选项,仅在调试客户端建立连接之后启动 VM ( suspend=y )。我的 J2EE 容器具有一种服务,它可以在容器登录之后立即启动,而不用添加 suspend=y 到参数列表。我发现在我的那项服务启动之前,启动客户端并连接到 VM 有些混乱 。使用该选项,我就可以启动 VM 。当它暂停的时候,我打开 IDE ,设置中断点,然后建立调试连接。一旦连接建立,容器继续登录,而我可以一步步启动服务。
onthrow 选项推迟初始化 JDWP 资料库,直到引发指定类的异常。例如,如果需要在引发异常(如 ServletException )时执行一些操作,您需要包含如下选项:
onthrow=
javaxservletServletException
同样, JDWP 资料库初始化也可以推迟,直到引发异常但没有捕获时,该操作可以通过提供 onuncaught=y 选项指定。
启动选项指出 VM 应当基于调试事件来启动应用程序,该事件与 onthrow 或者 onuncaught 选项一起提供:
launch=/usr/home/mydir/debugapp
让我们研究一些我经常使用的命令行调试参数的示例。在第一个示例中,我们将指导 VM 使用套接字传输。调用的 VM 是服务器,它应当监听端口 4000 。我们还指出 VM 应当在调试服务器建立连接后才可以暂停:
-Xrunjdwp:transport=
dt_socket,server=y,address=4000,
suspend=n
第二套参数类似于前一个示例,不同的是其传输现在是一种共享内存传输(仅限于 Windows ):
-X runjdwp:transport=
dt_shmem,server=y,address=4000,
suspend=n
第三套参数会导致 VM 通过端口 4000 上的套接字连接附加到正在运行中的调试服务器中,这将要求 VM 以调试模式运行在端口 4000 上的 SomeHost 中:
-X runjdwp:transport=
dt_socket,server=n,address=
SomeHost:4000, suspend=n
第四套参数将导致 VM 在开始的时候暂停。 VM 将等待建立与调试客户端的连接,然后再继续加载。 VM 使用端口 4000 来监听客户端,并使用套接字连接:
-X runjdwp:transport=
dt_socket,server=y,address=
4000, suspend=y
使用第五套参数,当引发 javaxmanagementInstanceNotFoundException 的时候, VM 将暂停,并启动 c:\debugbat 。 debugbat 脚本可以启动调试应用程序,该应用程序可以连接服务器,并开始调试会话:
-Xrunjdwp:transport=
dt_socket,address=4000,server=
y,suspend=y,onthrow=
javaxmanagement
InstanceNotFoundException,
launch=c:\debugbat
最后一套参数将在引发异常但没有捕获的时候执行 debugbat 脚本:
-Xrunjdwp:transport=
dt_socket,address=4000,server=
y,suspend=n,onuncaught=
y,launch=c:\debugbat
我们现在可以使用一些现有的通用 IDE 来说明如何调试 J2EE 。我们在示例中使用的 J2EE 容器是 JBoss 服务器;然而,任何标准的 J2EE 容器都是可用的。 JBoss 服务器从 bin 目录中的 runbat 文件启动。 BAT 文件可以接受调试命令行自变量,并将它们发送给 JVM 。作为一名开发人员,我发现我很少需要启动开发环境(除了调试模式),因此我将 runbat 复制到一个名叫 debugbat 的新文件中,并且在该文件中添加了调试参数。以下是 JBoss debugbat 文件的内容:
rem Read all command line
rem arguments
set ARGS=
:loop
if [%1] == [] goto endloop
set ARGS=%ARGS% %1
shift
goto loop
:endloop
set JAVA_OPTS=
-Dprogramname=runbat
set JAVAC_JAR=
%JAVA_HOME%\lib\toolsjar
set RUNJAR=\runjar
set JBOSS_CLASSPATH=
%JBOSS_CLASSPATH%;%JAVAC_JAR%;
%RUNJAR%
rem Set the debug options here
set DEBUG_OPTS =
-Xdebug -Xnoagent
-Djavacompiler=
NONE -Xrunjdwp:transport=
dt_socket,address=
4000,server=y,suspend=n
Java %JAVA_OPTS% %DEBUG_OPTS%
-classpath "%JBOSS_CLASSPATH%"
orgjbossMain %ARGS%
调试信息
在将 IDE 的调试程序连接到服务器之前,将应用程序放在服务器上。确保使用打开的调试信息对应用程序进行编译。这样做可以在已经编译的类文件中提供行编号方式。如果您使用 Ant 来构建项目,请确保 javac 任务包含:
debug="on"
否则, IDE 应当包含在编译期间打开调试的选项。
我提供了 从简单的 DocBook 文件生成 PDF E-Books 的 J2EE 应用程序 。该应用程序由会话 bean 、 JSP 文件和 servlet 组成。会话 bean 可以列举存储在服务器中的 DocBook 文档和 XSL 样式表文件。 JSP 使用会话 bean 来显示书籍和 XSL 文件的列表,以下拉列表的方式显示。用户可以选择他们要将哪些文件转换成 PDF ,并指定要将哪些样式表用于转换。作为最后一个步骤, servlet 将 DocBook 文件转换为 PDF ,并将 PDF 显示在用户的 Web 浏览器中。 Apache 的 FOP 资料库用于转换文档(参阅 资源 )。
如果您想使用我构建的应用程序,请编辑 Ebookproperties 文件,并提供到您的机器中某个位置的有效路径,该位置中存放 DocBook 和 XSL 文件。示例 DocBook 和 XSL 文件还可以下载。请将这些文件存放在您指定的位置中。
我已经提供了一份 Ant buildxml 文件来构建应用程序和将类封装到企业归档文件( EAR )中。将 EAR 文件部署到 JBoss 的目录中,并启动具有以下参数的容器:
-Xrunjdwp:transport=
dt_socket,server=y,address=4000,
suspend=n
现在,我们已经准备好在调试模式中连接代码
Intelli-J IDEA 调试
为了在Intelli-J中连接调试服务器,只需点击工具条中的Debug按钮,打开调试配置界面即可。然后选择Remote标记,来查看哪些调试配置可用。最初,没有配置可以使用。点击屏幕左上方的+按钮来添加新的远程调试配置。输入主机名称或者IP地址和要连接到的端口(参见图1)。
图 1 远程 Intelli-J 调试
图中有一个 Intelli-J 中的连接到本地机器的远程调试配置。
确保服务器启动时具有已经为 VM 进行设置的调试选项,然后点击 Debug 按钮。您应当连接到 VM ,并且您的调试屏幕中将出现在 Intelli-J 屏幕的底部。您可以通过点击 Edit 窗口右边空白区域来轻松的在 IDE 中设置中断点。在中断的地方会出现一个红点。如果圆点中有一个 X ,则您指定的中断点无效,或者服务器中的代码在编译时没有打开调试。如果圆点中有一个校验标记,则说明您已经成功的连接到调试服务器,和已经选择了有效的中断点位置。您可以进行调试了。如果您使用的是本文提供的 示例代码 ,则请在 EJB 的 getDocBooks() 方法中设置中断点。将 Web 浏览器指向 http://localhost:8080/DocBookToPDF/DocBookjsp 。您的浏览器应当显示为悬挂状态,并且 Intelli-J 在设置中断点的行中应当有一个蓝条。恭喜,您正在调试 EJB ,您的 IDE 已经为进入代码准备就绪(参见 图 2)。
图 2 Intelli-J 中断点
Intelli-J 中断点在其设置的地方以蓝色突出显示。图中,中断点已经设置,可以进行下一步骤。
们正在调试的方法查找在 DocBookToPDFproperties 文件中指定的目录,并返回所有以 XML 为结尾的文件。该方法假设每一个文档都是 DocBook 文件。在我们继续之前,确保目录具有有效的 DocBook 和 XSL 文件,以将文档转换为 PDF 。
完成进入代码之后,注意 JSP 在下拉列表中显示 DocBook 和 XSL 文件。下一步骤是调试 DocBookToPDFServlet servlet 。将中断点放在 Edit 窗口左边, processRequest() 方法中的某个位置。在 Web 浏览器中选择 DocBook 文件和有效的 XSL 文件,然后点击 Web 页面中的 Generate Book 按钮。 servlet 中的中断点将呈现为蓝色, servlet 代码可以进入。
NetBeans/Forte 调试
在 NetBeans 中创建一个项目,该项目包含您需要调试的代码。使用打开的调试选项编译它,并像在前面 Intelli-J 部分中讨论的那样,将应用程序部署到服务器中。我提供的 Ant build XML 文件演示了如何以调试模式编译代码。
为了开始调试,请选择 Debug | Start Session | Attach 菜单选项。这时,将出现一个对话框,要求填写连接信息。指定 JPDA 为调试程序, SocketAttach 为连接程序, localhost 为主机,端口 4000 为要连接到的器。点击 OK 按钮,您应当附加到 J2EE 容器的 VM (参见 图 3 )中。 Debug 窗口将成为活动窗口,您现在可以使用相同的方式来设置中断点,就像使用 Intelli-J 那样。在 EJB 的 getDocBooks() 方法中设置中断点,并将 Web 浏览器指向 http://localhost:8080/DocBookToPDF/DocBookjsp 。 Web 页面应当显示为悬挂状态,而 NetBeans 中的中断点将被突出显示(参见 图 4 )。您可以在 IDE 中点击调试工具栏中的 Step Over 和 Step Into 按钮来进入 NetBeans 中的代码。您还可以在界面左边框架中设置观察。
图 3 远程 NetBeans 调试
图中是 NetBeans 中本地机器的远程调试配置
图 4 NetBeans 中断点
NetBeans 中断点在活动状态时以绿色突出显示。图中是已经设置的中断点,已经准备好进行调试。
为了调试 servlet ,请在 processRequest() 方法中设置中断点。当 JSP 在 Web 浏览器中完成加载,并在机器中显示 DocBooks 和 XSL 文件的列表之后,选择 DocBook 文件和 XSL 文件,然后点击 Web 页面中的 Generate Book 按钮。在 NetBeans 中,中断点将变为突出显示状态,您可以开始调试 servlet 代码了。
Eclipse
为了在Eclipse中调试,需要在Java透视图中创建一个项目。如果您正在使用DocBook-to-PDF代码示例,则使用Ant buildxml编译代码,以编译打开调试的类。突出显示Java应用程序,并选择Run | Debug菜单项。此时,将出现一个对话框。突出显示Remote Java Application,然后点击对话框左手边底部的New按钮。对话框项目的名称应当为在Java透视图中突出显示的项目的名称。对话框中连接部分的主机字段应当是您需要连接到的服务器,端口字段中应当具有容器的VM正在使用的调试端口(参见图5)
图 5 远程 Eclipse 调试
图中是 Eclipse 中本地机器的远程调试配置。
接下来,点击Debug按钮。Eclipse将转到Debug透视图。为了调试代码,请返回到Java透视图中,并在编辑器中打开EJB文件或者servlet。双击EJB的getDocBooks()方法的左手边的窗格来设置一个中断点。有效的中断点具有一个蓝色的圆球,并且圆球中有一个校验标记。如果中断点中没有校验标记,双击该中断点,然后尝试不同的点。
接下来,将Web浏览器指向相同的JSP和servlet文件,就像前面NetBeans和Intelli-J中所描述的那样。当设置中断点时,中断点所在的行将被突出显示,您可以像使用其他IDE那样来调试代码(参见图6)。
图 6 Eclipse 中断点
Eclipse 中有效的中断点以蓝色圆球和校验标记指示。该图说明了如何在 Eclipse IDE 中调试代码。当前的调试位置以绿色突出显示
使用具有流行 IDE 的 JPDA 资料库应当会缩短开发时间,并且允许您创建包含极少缺陷的更优秀的代码。如果您仍在代码中添加 Systemoutprintln() 语句,以发现程序失败或者中断的位置,那么请住手!您是在浪费时间和金钱。 Java IDE 已经推出很长时间了,它可以提供功能强大的调试工具。您仅需要对启动 VM 的方法做一些小小的改动,就可以轻松地调试 Java 应用程序了。
本文通过开发一个JSP 编辑器插件的示例,介绍了 Eclipse 中设置 JSP 断点的方法,以及如何远程调试 JSP。作为基础知识,本文的前两部分描述了 JAVA Debug 和 JSR-45 的基本原理。
环境要求: 本文的代码是在 Eclipse300,JDK142 和 Tomcat505 上测试过的。
JAVA 调试框架(JPDA)简介
JPDA 是一个多层的调试框架,包括 JVMDI、JDWP、JDI 三个层次。JAVA 虚拟机提供了 JPDA 的实现。其开发工具作为调试客户端,可以方便的与虚拟机通讯,进行调试。Eclipse 正是利用 JPDA 调试 JAVA 应用,事实上,所有 JAVA 开发工具都是这样做的。SUN JDK 还带了一个比较简单的调试工具以及示例。
JVMDI 定义了虚拟机需要实现的本地接口
JDWP 定义了JVM与调试客户端之间的通讯协议
调试客户端和JVM 既可以在同一台机器上,也可以远程调试。JDK 会包含一个默认的实现 jdwpdll,JVM 允许灵活的使用其他协议代替 JDWP。SUN JDK 有两种方式传输通讯协议:Socket 和共享内存(后者仅仅针对 Windows),一般我们都采用 Socket 方式。
你可以用下面的参数,以调试模式启动JVM
-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n -Xrunjdwp JVM 加载 jdwpdll 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 | |---------------------|
Eclipse作为一个基于 JAVA 的调试客户端,利用 orgeclipsejdtdebug Plugin 提供了JDI 的具体实现。JDI 接口主要包含下面 4 个包
comsunjdi comsunjdiconnect comsunjdievent comsunjdirequest
本文不对 JDI 进行深入阐述,这里重点介绍 JDI 中与断点相关的接口。
comsunjdi
主要是JVM(VirtualMachine) 线程(ThreadReference) 调用栈(StackFrame) 以及类型、实例的描述。利用这组接口,调试客户端可以用类似类反射的方式,得到所有类型的定义,动态调用 Class 的方法。
comsunjdievent
封装了JVM 产生的事件, JVM 正是将这些事件通知给调试客户端的。例如 BreakpointEvent 就是 JVM 执行到断点的时候,发出的事件;ClassPrepareEvent就是 Class 被加载时发出的事件。
comsunjdirequest
封装了调试客户端可以向 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 50 做个测试,有两个 JSP,Hellojsp 和 greetingjsp,前者 include 后者。Tomcat会将他们编译成 JAVA 代码(Hello_jspjava),JAVA Class(Hello_jspclass) 以及 JSP 文件名/行号和 JAVA 行号之间的对应表(SMAP)。
Hellojsp:
1 2 3 4 5 6 7 8
greetingjsp: 1 Hello There! 2 Goodbye on
JSP 编译后产生的Hello_jspjava 如下:Hello_jspjava: 1 package orgapachejsp; 2 3 import javaxservlet; 4 import javaxservlethttp; 5 import javaxservletjsp; 6 7 public final class Hello_jsp extends orgapachejasperruntimeHttpJspBase 8 implements orgapachejasperruntimeJspSourceDependent { 9 10 private static javautilVector _jspx_dependants; 11 12 static { 13 _jspx_dependants = new javautilVector(1); 14 _jspx_dependantsadd("/greetingjsp"); 15 } 16 17 public javautilList getDependants() { 18 return _jspx_dependants; 19 } 20 21 public void _jspService(HttpServletRequest request, HttpServletResponse response) 22 throws javaioIOException, 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 = JspFactorygetDefaultFactory(); 36 responsesetContentType("text/html"); 37 pageContext = _jspxFactorygetPageContext(this, request, response, 38 null, true, 8192, true); 39 application = pageContextgetServletContext(); 40 config = pageContextgetServletConfig(); 41 session = pageContextgetSession(); 42 out = pageContextgetOut(); 43 _jspx_out = out; 44 45 outwrite(""); 46 outwrite(""); 47 outwrite(""); 49 outwrite(""); 50 outwrite(""); 51 outwrite("Hello There!"); 52 outwrite("Goodbye on "); 53 outwrite(StringvalueOf( new javautilDate() )); 54 outwrite(" "); 55 outwrite(" "); 56 outwrite(""); 57 outwrite(""); 58 } catch (Throwable t) { 59 if (!(t instanceof javaxservletjspSkipPageException)){ 60 out = _jspx_out; 61 if (out != null
outgetBufferSize() != 0) 62 outclearBuffer(); 63 if (pageContext != null) pageContexthandlePageException(t); 64 } 65 } finally { 66 if (_jspxFactory != null) _jspxFactoryreleasePageContext ( pageContext); 67 } 68 } 69 }
Tomcat 又将这个 JAVA 代码编译为 Hello_jspclass,他们位于: $Tomcat_install_path$workStandalonelocalhost\_ 目录下。但是 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_jspclass", };
for(int k = 0; k
fileslength; k++){
String file = files[k];
Systemoutprintln("Class : " + file);
ClassFile classFile = new ClassFile(new DataInputStream(new FileInputStream(file)));
AttributeInfo attributeInfo = classFilegetAttribute("SourceDebugExtension");
Systemoutprintln("attribute name :" + attributeInfogetName() + "]");
byte[]bytes = attributeInfoget();
String str = new String(bytes);
Systemoutprintln(str); } }
这段代码显示了SourceDebugExtension 属性,你可以看到SMAP 的内容。编译JSP后,SMAP 就被写入 Class 中, 你也可以利用 javassist 修改 ClassFile 的属性。
下面就是 Hello_jspclass 中保存的 SMAP 内容:SMAP E:Tomcat5_0_5workCatalinalocalhost\_orgapachejspHello_jspjava JSP S JSP F + 0 Hellojsp /Hellojsp + 1 greetingjsp /greetingjsp 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_jspjava,然后是 stratum 名称: JSP。随后是两个JSP文件的名称 :Hellojsp、greetingjsp。两个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(没有源文件代号,默认为Hellojsp)
本文通过开发一个JSP 编辑器插件的示例,介绍了 Eclipse 中设置 JSP 断点的方法,以及如何远程调试 JSP。作为基础知识,本文的前两部分描述了 JAVA Debug 和 JSR-45 的基本原理。
环境要求: 本文的代码是在 Eclipse300,JDK142 和 Tomcat505 上测试过的。
JAVA 调试框架(JPDA)简介
JPDA 是一个多层的调试框架,包括 JVMDI、JDWP、JDI 三个层次。JAVA 虚拟机提供了 JPDA 的实现。其开发工具作为调试客户端,可以方便的与虚拟机通讯,进行调试。Eclipse 正是利用 JPDA 调试 JAVA 应用,事实上,所有 JAVA 开发工具都是这样做的。SUN JDK 还带了一个比较简单的调试工具以及示例。JVMDI 定义了虚拟机需要实现的本地接口 JDWP 定义了JVM与调试客户端之间的通讯协议 调试客户端和JVM 既可以在同一台机器上,也可以远程调试。JDK 会包含一个默认的实现 jdwpdll,JVM 允许灵活的使用其他协议代替 JDWP。SUN JDK 有两种方式传输通讯协议:Socket 和共享内存(后者仅仅针对 Windows),一般我们都采用 Socket 方式。你可以用下面的参数,以调试模式启动JVM-Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n -Xrunjdwp JVM 加载 jdwpdll 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 | |---------------------|
Eclipse作为一个基于 JAVA 的调试客户端,利用 orgeclipsejdtdebug Plugin 提供了JDI 的具体实现。JDI 接口主要包含下面 4 个包comsunjdi comsunjdiconnect comsunjdievent comsunjdirequest
本文不对 JDI 进行深入阐述,这里重点介绍 JDI 中与断点相关的接口。comsunjdi 主要是JVM(VirtualMachine) 线程(ThreadReference) 调用栈(StackFrame) 以及类型、实例的描述。利用这组接口,调试客户端可以用类似类反射的方式,得到所有类型的定义,动态调用 Class 的方法。 comsunjdievent 封装了JVM 产生的事件, JVM 正是将这些事件通知给调试客户端的。例如 BreakpointEvent 就是 JVM 执行到断点的时候,发出的事件;ClassPrepareEvent就是 Class 被加载时发出的事件。 comsunjdirequest 封装了调试客户端可以向 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 50 做个测试,有两个 JSP,Hellojsp 和 greetingjsp,前者 include 后者。Tomcat会将他们编译成 JAVA 代码(Hello_jspjava),JAVA Class(Hello_jspclass) 以及 JSP 文件名/行号和 JAVA 行号之间的对应表(SMAP)。
Hellojsp: 1 2 3 4 5 6 7 8
greetingjsp: 1 Hello There! 2 Goodbye on
JSP 编译后产生的Hello_jspjava 如下:Hello_jspjava: 1 package orgapachejsp; 2 3 import javaxservlet; 4 import javaxservlethttp; 5 import javaxservletjsp; 6 7 public final class Hello_jsp extends orgapachejasperruntimeHttpJspBase 8 implements orgapachejasperruntimeJspSourceDependent { 9 10 private static javautilVector _jspx_dependants; 11 12 static { 13 _jspx_dependants = new javautilVector(1); 14 _jspx_dependantsadd("/greetingjsp"); 15 } 16 17 public javautilList getDependants() { 18 return _jspx_dependants; 19 } 20 21 public void _jspService(HttpServletRequest request, HttpServletResponse response) 22 throws javaioIOException, ServletException { 23 24 JspFactory _jspxFacto
这份材料介绍JAVA的调试技术 范围涵盖普通程序和服务器端程序的调试 很多程序员并没有认识到排除软件的错误的价值 如果你是一个JAVA开发者 就很值得读一读这个材料 在现代工具的帮助下 开发者成为一个好的调试者和成为一个好的程序员的重要性一样 这个材料假设你已经有基本的JAVA编程的知识 如果你精通JAVA 这个材料也可以增加你很多知识 如果你有其他语言的调试经验 你可以跳过基本知识部分 即使是高级程序员开发的小程序也可能包含错误 你只需要理解调试的概念并熟悉合适的工具就可以成为好的调试者 这份材料将讲解JAVA调试的基本概念 也讨论高级的调试类型 我们将浏览不同的技术并且提供一些好的建议去帮助避免 追踪并最终修正程序的错误 我们将通过一个调试范例以使你熟悉调试技术 我们也将使用开发源代码工具Jikes 和JDB向你演示如何调试服务器端和客户端程序 为了编译和运行范例代码 你需要先安装一个Java Development Kit (JDK) 你可以参考后面的部分获得Jikes 和 JDB调试器 关于作者 如果对这个材料的内容有任何问题 你可以联系作者Laura Bennett 如果对中文版的翻译有何意见和建议 请联系翻译者cherami Laura Bennett 是IBM的资深软件工程师 她获得Pace大学的计算机科学学士学位和Columbia大学的计算机科学硕士学位 她是developerWorks的JAVA传教士 同时也是站点的建设者 在他的空余时间 她喜欢和她的Lego MindStorm 机器人玩乐以及和她四岁大的TinkerToys搭建物体 Cherami是一个软件工程师 闲暇之余翻译一些计算机文献 以期为中国的计算机软件事业做出一点微薄的贡献 调试的基础知识 开始的情况 在JAVA语言的早期 一个典型的开发者使用非常陈旧的方法调试程序 使用System out println() 方法 代码的跟踪信息被打印到控制台 文件或者套接字 很少有人能在第一次就写出完美的(没有任何错误)代码 因此 市场认识到了对于像C++ 程序员使用的调试器那样的工具的需要 Java开发者现在有很多调试工具可以选择 选择什么样的工具依赖于你的技术等级 通常新手使用GUI调试工具而有更多经验的程序员趋向于避免使用所见即所得的工具而更关心有更多的控制能力 没有哪个开发者不使用任何调试工具 调试器允许你穿越代码 冻结输出以及检查变量 开发者越有经验 调试工具越可以帮助他更快定位程序问题的位置 Java调试器的类型 这里有几种Java调试技术的工具: IDE(集成开发环境) 包含它们自己的调试器 (例如IBM的VisualAge for Java Symantec Visual Café以及 Borland JBuilder) 单独的GUI工具 (例如Jikes Java 平台调试器 javadt 以及JProbe) 基于文本和命令行的工具 (例如Sun JDB) 野蛮的使用编辑器 (例如Notepad 或者 VI) 检查堆栈描绘(stack traces) 你使用的 JDK JSDI JSP 和HTML对你的选择都有影响 IDE 和独立的GUI 调试器对于初学者是最容易的并且被证明是最节省时间的 调试器将引导你到程序崩溃的地方 在调试器里面执行程序 使用鼠标设置断点并穿越代码 使用这些调试器的不利方面是并非所有的IDE调试器都支持最新的Java API和技术 (例如servlets 和 EJB 组件) 基于文本和野蛮的使用编辑器的技术提供更多的控制但是对于没有太多经验的程序员可能会花费更长的时间找出错误 我们称它们为 可怜人的 调试方法 如果上面的都不满足你的需求 Java平台引入Java Debugging APIs使你可以创建符合你自己特定需求的调试器 调试类型 这儿有很多调试方法 无论是在客户端还是服务器端 我们在这个材料里面包含下面的方法: 基本的Java字节码 (也就是使用System out println()) 使用注释 附加在一个正在运行的程序上 远程调试 需求调试(Debugging on demand) 优化代码的调试 Servlet JSP 文件以及EJB 组件的调试 在后面会详细说明每一种类型的调试 共同的错误类型 为了给你一个你将遇到什么的提示 我们在下面列出了开发者一次又一次遇到的编辑或句法错误 是你最先和最容易遇到的错误 它们通常是键入错误引起的 逻辑错误 不同于运行时错误 因为没有任何异常被抛出 但是输出不是期望的东西 这些错误的范围从缓冲区溢出到内存泄漏 运行时错误 在程序执行时发生并且通常产生一个Java异常 线程错误 是最难重复和跟踪的 Java debugging APIs Sun已经定义了调试的结构 它们称之为JBUG 这是为了回应对真正的Java调试器的需要做出的 这些APIs帮助程序员建立符合自己需要的调试器: 接口应该和语言的风格一样是面向对象的 例如线程和监视器这样的Java运行时特性应该被前面的支持 可以进行远程调试 在通常操作下的安全性不能被损害 修正的Java Debugger (JDB) 既是体现Java Debugging API的概念 同时又是一个有用的调试工具 它用Java Debug Interface (JDI)重写并且是JDK的一部分 JDB将在后面详细讨论 准备一个调试用的程序 Java平台为调试过程提供语言支持 你在用编译器编译你的程序时可以用编译选项指示编译器在目标文件中产生符号信息 如果你使用其它的编译器而不是javac 参考你的编译器的文档获得如何生成带有调试信息的目标文件 如果你使用javac 编译器创建调试代码 使用 g 编译选项 这个选项让你在调试的时候可以检查本机类实例和静态变量 如果你没有使用该选项生成你的类文件你也可以设置断点和追踪代码 但是你将不能检查变量 (断点是手工指定的程序运行停止的点 ) 即使你使用 g选项编译你的程序也不能调试JAVA平台的核心系统类的局部变量 如果你需要列出某些系统类的局部变量的列表 你需要使用 g选项编译这些类 也就是使用 g选项重新编译rt jar 的类或者是 src zip 里面的文件 然后指定你的 classpath 为正确的类文件使你用新编译的类运行你的程序 在Java 下 使用 boot classpath 选项使得新类被首先加载 记住如果你使用 O 选项优化你的代码 你就不能调试你的类 优化会将所有的调试信息从类中去掉 注意: 检查你的 CLASSPATH 环境变量是正确的才能让调试器和Java 程序知道在哪儿寻找你的类库 你也应该检查你的调试工具看是否需要其它的什么或者是环境变量 设置断点 调试的第一步就是找到代码出错的位置 断点设置能帮你完成这个 断点是你你放置在程序里面的临时标记 它使得调试器知道在哪儿停止程序的执行 例如 如果程序里面的某个申明引发问题 你可以将断点设置在包含那个申明的行上 然后运行程序 在那个申明被执行前程序停止执行 然后你可以检查变量 寄存器 存储器以及堆栈的内容 然后跨过(或执行)那个申明查看问题是怎么引起的 不同的调试器支持不同的断点 一些通用的类型是: 行断点 在程序特定行的代码被执行前被引发 方法断点 在到达被设置成断点的方法时被引发 计数断点 在某个计数器达到或超过某个特定值时被引发 异常断点 在代码抛出一个特定异常时被引发 储存变化断点 在存储在特定地址范围的内容被修改时引发 地址断点 在被设置成断点的地址达到时被引发 注意: 一些调试器只在编译版本的Java代码 (使用just in time 编译器生成的代码) 上支持某些断点类型而不支持解释代码(使用javac 工具生成的代码) 一个例子就是地址断点 每个工具在你能设置断点的方式上可能有些不同 检查你的工具的文档 你可能会问 我如何知道在哪儿放置断点? 如果你对这个问题完全没有感觉 你可以在main() 方法的开始设置断点 如果你的代码产生堆栈复写(stack trace) 在程序产生它的地方设置断点 你将在堆栈复写里面看到源代码中出问题的行号 如果你的输出或者图形显示的特定部分没有正确的显示预定信息(例如文本域显示错误的文本) 你可以在该组件被创建的地方设置断点 然后你可以单步执行你的程序显示和GUI对象相关的值 经验将在最合适的地方设置断点 你在一个类或者程序里面可以设置多个断点 通常 你在调试代码的时候会禁止 激活 添加 删除断点 工具会允许你查看你所设置的所有断点的位置同时给你一次删除所有断点的选项 单步执行程序 单步执行程序是最终解决那些棘手的调试问题的方法 它允许你追踪类里面的方法体的整个执行过程 注意 你不需要设置断点就可以停止一个GUI程序的执行 设置断点后在调试器里面开始执行程序 当遇到第一个断点后 你可以越过申明 进入方法体或类体 也可以继续运行直到下一个断点或程序结束 在调试程序的时候经常遇到的术语有 进入 执行当前行 如果当前行包含一个方法调用 执行被调用方法的第一行 如果类中的方法是用不带调试信息的选项编译的 (也就是没有使用 g 选项) lishixinzhi/Article/program/Java/Javascript/201311/25398
0条评论