级别: 初级 Andrei Malacinski (malacins@us.ibm.com), WebSphere 软件工程师, IBM
2001 年 1 月 01 日 如今不管什么项目都有严格的进度表,并且总是要求有更多的功能,所以开发人员通常没有时间,或者不花时间来考虑调试(或跟踪)策略。本文免费 -- Java 源代码以及全部内容 -- 为开发人员提供了一种策略,这样他们就不必再自行设计和实现自己的跟踪策略,从而使他们能够将精力集中在应用程序的主要逻辑上。本文提出了两项技术,一项用于开发时的跟踪,另一项用于运行时的跟踪。
试考虑以下情况:您最忠实、最重要的一个客户运行您的软件时遇到了问题。这个客户报告当按某种使用方式运行您的应用程序时,应用程序就会意外终止。客户可以有规律地重现这个问题。客户很不满意,并希望问题尽快得到解决。不幸的是,在开发环境或测试环境中,您按客户的相同使用方式运行应用程序却无法重现那个问题。在您的环境中一切正常,但是在客户的环境中却有规律地发生失败。下一步该怎么办?
通过在您的配置中使用运行时跟踪技术,如本文提出的这项技术,您的客户就可以打开运行时跟踪来将调试(或跟踪)消息输出到一个文件中,然后再次运行应用程序以重现问题,并将跟踪消息发送给您,从而帮助您调试他们的问题。跟踪消息将使您能够查看程序在客户环境中的流程。
在讨论实际的跟踪机制之前,首先看一下我们将要讨论的两种类型的跟踪。
-
开发时跟踪可以配置您的应用程序代码,以便在应用程序开发阶段打开调试消息的输出作为一种调试手段,而在部署应用程序时将其关闭。客户永远不会看到您的开发跟踪消息。
一种常用的开发时跟踪机制是,在构建 Java
源代码时设定一个编译标志,这样就可以通过这个标志触发 Java
System.out.println()
调用。但您很容易扩展这种基本模型。在代码开发期间,同样嵌入您将为运行时跟踪创建的跟踪函数。此外,请对它进行设置,以便当您关闭编译标志时,所有的开发跟踪调用都会被从编译后的代码中去掉。检查
.class
文件的字节码,发现文件中并不存在开发跟踪调用。欲了解使用这项技术所需的
Java 源代码以及操作说明,请参阅
“开发时跟踪机制”。
-
运行时跟踪对您的应用程序进行设置,以使客户能够打开跟踪消息的输出。如果客户在运行您的应用程序时遇到问题,他们就可以打开跟踪,再次运行应用程序,并将跟踪消息发送给您,以帮助您调试他们的问题。
一种简单的运行时跟踪机制由基本的 Java
System.out.println()
调用组成。但只要稍作思考,就可以将它扩展开。例如,您可以使用客户在运行时可将其动态打开的多级跟踪。另外,您也可以让客户选择将跟踪的输出发送到什么地方,如控制台或文件。在实现这些跟踪时,创建一组您和您的开发伙伴可以共享的可重用跟踪函数很有意义。这些函数封装了跟踪细节,例如,向何处发送跟踪输出,跟踪语句中是否包括日期/时间戳,等等。要获得一组跟踪函数以及用来说明这组跟踪函数的
Java 源代码样例,请参阅
“运行时跟踪机制”。
开发时跟踪机制
开发时跟踪涉及在您的代码中插入跟踪语句作为调试辅助手段。在应用程序的开发和测试阶段打开这些语句,而在部署时关闭这些语句。这项技术利用名为
PrintMessage
的类中的一组可重用方法。我们在运行时跟踪机制中将使用同一组方法。另外,编译标志是这样声明的,当标志被关闭时,编译后的代码将不包含任何开发跟踪调用。可以按以下步骤启用开发时跟踪:
-
首先,声明一组编译标志来实现跟踪。(请参阅 Java 源代码样例,
SharedConstants.java
,其中使用了
static 和
final 关键字。)将变量声明为
final
表明(并强制)变量的值不会从其初始值改为其他值。
-
在开发期间,将 DEBUG
变量设置为所需的输出跟踪级别。在本例中,TRACE_MEDIUM
包括低级和中级跟踪,TRACE_HIGH 包括低级、中级和高级跟踪,而
TRACE_ALL 包括全部跟踪级别。TRACE_ALL 相当于
TRACE_HIGH。在部署应用程序时,将 DEBUG 变量设置为
TRACE_OFF。如果使用了这项部署时跟踪技术,您就可以根据需要定义每个级别的跟踪范围。
注: 当更改上述任一个变量时,例如在开发的最后阶段关闭跟踪,您必须重新编译使用这种变量的全部
Java 源代码,以及那些间接使用这些变量的代码。这个步骤是必需的,因为
Java
编译器通过直接将这些变量的值插入使用它们的代码中来优化您的代码。因此,编译器不会自动通过必要的检查来检测是否需要编译全部受影响的文件。
- 创建一个包含跟踪函数的实用程序类,如
PrintMessage 。这个类封装支持开发时调试所必需的全部代码。在您的
Java 类中包括一个像
PrintMessage
这样的类,从而使编写应用程序代码的每个开发人员都能利用它的功能。
PrintMessage 类通过包含诸如以下的声明来包括开发时跟踪功能:
public static final boolean traceMedium =
((SharedConstants.DEBUG &
SharedConstants.TRACE_MEDIUM) != 0)
? true : false;
public static final boolean traceAll =
((SharedConstants.DEBUG &
SharedConstants.TRACE_ALL) != 0)
? true : false;
|
注: 这些变量被声明为
static final 。
这里是
PrintMessage
类
,它具有开发时跟踪功能。其中调用了诸如
printMessageWithDateTime(String msg) 和
printMessage(String msg)
之类的跟踪输出函数,以输出每个跟踪消息。
为灵活起见,请在您的
PrintMessage
类中实现多个形式的
PrintMessage
方法。在上面的示例中:
-
printMessage(String msg)
只输出方法调用者预定的跟踪消息。
-
printMessageWithDateTime(String msg)
在输出跟踪消息之前添加日期/时间戳。如果您正在跟踪的应用程序涉及数据库访问、网络通信、系统
I/O、或时间戳将有助于对其进行调试的其他复杂事务,那么这可能很有用。
-
printMessageNoNewLine(String msg)
输出不带换行符的跟踪行。
还可能有许多其他形式的方法。
-
在您的源代码中包括跟踪语句,并使用您的
PrintMessage
类中的方法。
MainApplication.java
中的代码样例使用
PrintMessage
的跟踪函数来启用开发时调试。
作为开发人员,您根据每条消息所在的上下文决定它的输出跟踪级别。
注: 为了保持 Java 代码的优化目标,上面的示例直接检查
static final
变量的值。当编译这段代码时,您就可以实现优化。在本例中,编译器注意到形式为
if (Expression) Statement 的代码,如表达式
if
(PrintMessage.traceMedium) ,总是得出相同的结果。这个结果与运行时的执行状态无关。因此,编译器就可以优化这段代码。如果编译器注意到代码块的
if (Expression) 条件部分总是得出
false,它就知道代码块的
Statement
部分永远不会执行。因此,编译器不会为
Statement
代码块生成字节码。结果,这个代码块中的跟踪语句就不会包括在
.class 文件中。
-
验证开发跟踪消息已被 Java 编译器去掉,因此不包括在
.class
文件字节码中。要完成这一步,请使用一个程序反汇编
.class 文件。反汇编
.class
文件是从编译后的组成
.class 文件的 Java 字节码重新生成
Java 源代码的技术。当反汇编
.class 文件之后,请检查
Java 源代码来验证跟踪消息不存在。Java 开发工具包 (JDK)
提供了一个称为
javap
的反汇编器。请按以下方式执行它:
javap -c packageName.MyApplication
|
注: 当调用
javap 时,必须提供
.class 文件的包名。

 |

|
运行时跟踪机制
运行时跟踪也涉及在您的代码中插入调试语句。在本例中,客户通过在调用您的应用程序时向
Java
应用程序提供一个命令行选项变量来打开调试。在此运行应用程序的上下文内,此变量及其值可从
Java
系统属性中获得。在应用程序初始化期间,您的代码读取该系统属性的值来确定客户是否打开了运行时跟踪。请按以下步骤启用运行时跟踪:
-
创建一个包含跟踪函数的实用程序类,如
PrintMessage 。请参阅源代码样例
PrintMessage.java
,它实现了这项技术的一种变化形式。这个类封装了支持运行时调试所必需的全部代码。
公用方法
isRunTimeDebugOn() 返回静态变量
debugOn 的值,这个值是在类加载时初始化的。随后对
isRunTimeDebugOn()
的方法调用不会带来太大的性能开销,因为该方法只返回一个变量的值。
-
在您的源代码中包括跟踪语句,并使用
PrintMessage
类中的方法。例如,请参阅
MainApplication.java
代码样例的一种变化形式,它利用
PrintMessage
的跟踪函数启用运行时调试。
-
按您的客户的方式启动您的 Java 应用程序,如下所示:
jre -Ddebug=1 MainApplication
|
或者
java -Ddebug=1 MainApplication
|
如果您的应用程序通过从前端可执行程序(如 C/C++ 代码)或脚本启动
JVM 来隐藏对 Java Virtual Machine (JVM)
的调用,则您可以用稍微不同的方式将调试变量呈现给客户。在您的可执行程序或脚本逻辑中,您将用上面的调试变量设置启动
JVM。
-
您可以用以下几种方法增强这个基本设计:
-
客户提供的调试变量的值可以是跟踪输出的位置(如系统控制台或一个完全限定的文件名)。这将使客户在收集跟踪输出时有更大的灵活性,跟踪输出可被发送给您和您的应用程序开发小组,以帮助您进行调试。
-
变量也可以指定输出跟踪级别(如低、中或高)。低级跟踪和中级跟踪可以包括对应用程序特定点的跟踪,而高级跟踪还可以包括方法的进入和退出通知,或者循环迭代通知。级别的定义和含意由您决定。
-
如果您的应用程序逻辑上分为若干组件,则可用调试变量指定要跟踪哪个应用程序组件。
注:既可以将以上信息包括在单个变量中,也可以用不同的变量分别指定。例如,用一个变量指定输出位置,用另一个变量指定跟踪级别,再用一个变量指定要跟踪的组件。
请参阅
PrintMessage.java
源代码样例的这种变化形式,它通过将跟踪语句输出到一个文件中实现了这项技术。在本例中,如果指定调试变量,则它的值被解释为完全限定的文件名。在实际应用中,应该在文件
I/O
部分增加更多的错误检查和错误恢复功能。另外,如果您预计会有大量的跟踪语句,则可以限制文件的大小。一种策略是,一旦达到文件大小界限,就清除文件中最早的语句块,从而为添加新的语句让出空间。将这项技术再改进一步,您可以通过应用程序的用户界面将跟踪功能呈现给客户。您的界面可以允许用户选择跟踪间隔、跟踪输出的位置、文件名、文件大小限制、
文件切短机制,等等。

 |

|
其他提示
在应用程序开发过程中,使用开发时跟踪技术可能是一种有效的调试手段。一旦部署应用程序之后,就转而使用运行时跟踪技术,这允许客户将跟踪输出反馈给开发小组,这也可能是一种有效的调试手段。本文提出的技术使开发人员不再需要设计和实现自己的跟踪策略,从而允许他们将精力集中在应用程序的主要逻辑上。
本文提出的跟踪技术只是应用程序开发人员可用的许多跟踪策略中的少数几个。例如,尽管异常本身不是一种输出跟踪机制,但它是在应用程序代码中进行执行流和错误流处理或跟踪的一种编程结构。您可以将输出跟踪机制(如本文提出的技术之一)与在应用程序代码中选择的异常处理策略结合使用。实际上,本文提供的源代码样例也说明了在异常处理代码中插入跟踪语句的方式。
断言 (assertion)
是在应用程序代码中执行错误检测的另一种有用的编程结构。断言的确切行为和功能取决于实现断言函数的语言或程序包。但是,通常可将断言定义为布尔表达式,您可以将这些断言添加到您的代码中来检查表达式的结果(即断言状态正常,可以继续)。如果断言失败,程序通常就会终止。断言策略也可以与输出跟踪机制(如本文提出的技术之一)结合使用。断言的
API 及其功能与具体实现有关。例如,过去的 C/C++
语言和工具箱提供特定的断言函数用于状态检查。Java 语言和 JDK
目前都不提供内建的断言功能。
参考资料
关于作者  | |  |
Andrei Malacinski 是 WebSphere
工具开发领域的一名软件工程师。他在 IBM Application &
Integration Middleware Lab 工作,该实验室位于北卡罗来纳州的
Research Triangle Park。Andrei 于 1990
年在印第安那大学获得计算机科学学士学位。自那以后,他作为开发人员和小组负责人参与了
IBM 的许多应用程序开发产品的开发,涉及的平台包括 Windows、OS/2 和
UNIX。可以通过
malacins@us.ibm.com 与 Andrei
联系。
|
对本文的评价
|