在企业级的开发过程中,我们不可避免地会碰到很多问题;如果您希望在开发过程的后期能够有效地捕捉 bug,那就需要一种有效的日志策略。但是在一个企业的应用程序中要想实现有效地记录日志,需要进行一番规划,并设计一些准则。在本文中,顾问 Charles Chan 将向您介绍一些最好的实践,从而帮助您从项目一开始就编写有用的日志代码。
如果您是一名开发人员,那您很可能就已经具有这种经验:您已经开发了一些代码以及一些测试用例。应用程序经过了严格的 QA 测试,您确信代码可以完全适合业务的需求。然而,在将应用程序最终交付终端用户的手里时,却会出现一些预想不到的问题。如果没有适当的日志消息,可能需要花费几天的时间来诊断这些问题。不幸的是,大部分项目对于日志都没有一个清晰的策略。如果没有这种策略,系统产生的日志消息就有可能无益于问题的分析和解决。在本文中,我们将讨论企业应用程序日志的各个方面的问题。您将看到一个 Java™ 平台上日志 API 的概述,学习一些最好的编写日志代码的实践,并了解如果需要在产品环境中对详细日志重新进行排序,应该如何处理。
选择日志 API
在使用 Java 平台进行开发时,可以使用两个主要的日志 API:Apache Log4J 和 Java Logging API,在 1.4 及更高版本的 Java 平台中都提供了这两个 API。与 Java Logging API 相比,Log4J 更加成熟,特性也更加丰富。这两个日志的实现都采用了一个类似的设计模式(如图 1 所示)。除非您的公司限制要使用第三方的库,否则我强烈建议使用 Log4J。如果您不能决定使用哪个 API,就可以使用 Apache Commons Logging API,它对底层的日志实现进行了封装。从理论上来说,这样不用修改代码就可以进行日志实现的切换。然而,实际上您很少会切换日志的实现;因此,我不建议使用 Apache Commons Logging API,因为它的复杂性并不没有给您带来其他特性。
日志概述
Log4J 和 Java Logging API 都采用了类似的设计和使用模式(如图 1 和清单 1 所示)。消息首先被创建,然后传递给一个具有特定优先权的日志对象。这些消息的目的和格式是由输出处理程序及其布局所决定。
清单 1. 日志对象的实例化和使用
import org.apache.log4j.Logger;public class MyClass {
/*
* Obtain a logger for a message category. In this case, the message category is
* the fully qualified class name of MyClass.
*/
private static final Logger logger = Logger.getLogger(MyClass.class.getName());
...
public void myMethod() {
...
if (logger.isDebugEnabled()) {
logger.debug("Executing with parameters: " + param1 + ":" + param2);
}
}}
一个好的日志实现中提供了很多不同的输出处理程序,最常见的文件输出处理程序和终端输出处理程序。Log4J 还提供了一些处理程序将消息发布到一个 JMS 主题中,或者将消息插入一个数据库表中。尽管这编写一个定制的附加器并不困难,但是编写和维护这种代码的总体成本不应低估。消息的格式可以通过 Layout 对象进行配置。最常见的 layout 对象是 PatternLayout,它根据所提供的模式对消息进行格式化。
清单 2 给出了一个 Log4J 的样例配置文件,它负责配置 FileAppender。在这种配置中,com.ambrosesoft.log.MyClass 类中的错误消息被发送给 FileAppender,后者将其写入一个名为 log.txt 的文件中。这些消息是根据与这个添加器相关的 layout(在这种情况中是 PatternLayout)进行格式化的。
清单 2. Log4J XML 配置样例文件
<?xml version="1.0" encoding="UTF-8" ?<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"
<appender name="fileAppender" class="org.apache.log4j.FileAppender"
<param name="File" value="log.txt"/
<param name="Append" value="true"/
<layout class="org.apache.log4j.PatternLayout"
<param name="ConversionPattern" value="%d [%t] %p - %m%n"/
</layout
</appender
<category name="com.ambrosesoft.log.MyClass"
<priority value="error"/
<appender-ref ref="fileAppender"/
</category
<root
<priority value="debug"/
<appender-ref ref="fileAppender"/
</root</log4j:configuration
日志最佳实践
关于日志,您要做的一个最重要的选择可能是确定一种模式,将每个日志消息分配给一个特定的 类别。常见的一种实践是使用每个类的全名,这些类的操作会被作为一个消息类别在日志中记录(正如我们在清单 1 中看到的一样),这是因为这可以让开发人员更细粒度地记录每个类的设置。然而,这只有在使用日志消息来跟踪执行过程时才能良好地工作。在企业级的应用程序中,有很多其他类型的日志消息。举例来说,一条日志消息可能是为安全顾问产生的,而另外一条日志消息则可能是会为了帮助进行性能调优而产生的。如果这两条消息所关注的是同一个类,这样就会被分配给相同的类别,这将很难在日志输出结果中对其进行区分。
为了避免这个问题,应用程序应该具有一组专用的日志记录程序,它们都进行了独特的分类,如清单 3 所示。每个日志记录程序都可以配置自己的优先级和输出处理程序。例如,安全性日志记录程序可以在将日志写入目的地之前对消息进行加密。有时应用程序的设计者应该与使用日志的用户(例如安全顾问)一起来商讨日志的输出格式,从而对这些消息进行更好的控制。
清单 3. 专用的日志记录程序
import org.apache.log4j.Logger;public interface Loggers {
Logger performance = Logger.getLogger("performance");
Logger security = Logger.getLogger("security");
Logger business = Logger.getLogger("business");}...public class MyClass {
....
if (Loggers.security.isWarnEnabled()) {
Loggers.security.warn("Access denied: Username [" + userName + "] ...");
}
...}
选择日志的级别
一个 类别 (例如 security)中的消息可以具有不同的 优先级。有些消息是为了调试而产生的,有些是为了警告而产生的,有些则是出现错误而产生的。消息的不同优先级可以通过记录 级别 来产生。最常用的日志级别有:
Debug: 这个级别的消息中包含了非常广泛的上下文信息。通常用于问题诊断。
Info: 这些消息包含了一些有助于在产品环境中(粒度较粗)帮助跟踪执行过程的上下文消息。
Warning: 警告消息,说明系统中可能存在问题。例如,如果这个消息类别是有关安全性方面的,那么如果检测到字典攻击,就应该产生一条警告消息。
Error: 错误消息说明系统中出现了严重的问题。这种问题通常都是不可恢复的,需要人工进行干预。
标准的 Java Logging API 和 Apache Log4J 在此之外又提供了一些日志级别。日志级别的主要目标是帮助您过滤有用信息中的噪声。为了防止出现使用错误的级别以及降低日志消息的效用的情况,在开始编码之前,必须为开发人员提供一个清晰的指导方针。
日志消息的格式
一旦选定日志记录程序并建立起日志级别之后,就可以开始构建日志消息了。在这样做时,重要的是要包含尽可能多的上下文信息,例如用户提供的参数,其他应用程序的状态信息。记录日志对象的一种方法是将它们转换成 XML。第三方库,例如 XStream(请参阅 参考资料)可以自动将 Java 对象转换成 XML 。尽管这是一种非常强大的机制,但是我们必须要考虑在速度与详细程度之间达到一种平衡。除了典型的应用程序状态信息之外,还应该记录以下信息:
线程 ID: 企业级的应用程序通常都是在多线程的环境中运行的。使用线程 ID 信息,您就可以将多个请求区分开来。
调用程序的标识: 调用程序的标识也是非常重要的信息。由于不同的用户具有不同的特权,它们的执行路径也可能会有很大的不同。将用户的标识放到日志消息中,这对于对安全性敏感的应用程序是非常大的一个帮助。
时间戳: 通常来说,用户只能近似地知道问题发生的时间。如果没有时间戳,就很难让别人来判断问题的原因所在。
源代码信息: 这包括类名、方法名和行号。除非您非常关注安全性,否则我建议您保留调试标记(-g),即使在编译产品时也是如此。如果没有调试标记,Java 编译器就会删除所有的行号信息,从而极大地减少日志消息的可用性。
上面这些信息(除了调用程序标识)都是由日志实现自动获取的。为了将这些信息包含到消息中,您只需要为输出处理程序配置一个适当的 layout 模式即可。要捕获调用者的标识,您可以利用 Log4J 中的诊断上下文特性(更多信息请参阅 参考资料)。诊断上下文让您可以将上下文信息与当前正在运行的线程关联在一起。这些信息可以在为输出进行格式化的同时而包含到每条消息中。
在 J2EE Web 应用程序中,应用逻辑将用户标识保存到诊断上下文中最好的地方是在一个 servlet 过滤器中。清单 4 中显示了要实现这种功能的必要代码。它使用了 Log4J 1.3 alpha 中提供的映射诊断上下文类(MDC)。您可以使用 Log4J 1.2 中提供的嵌套诊断上下文(NDC)实现相同的功能。有关 servlet 过滤器的更多通用信息,请参阅 参考资料 中的信息。
清单 4. 在 servlet 过滤器中使用诊断上下文