2003 年 9 月 29 日
日志记录不仅是开发和测试周期中的一个重要元素――提供关键调试信息,而且对于系统已部署到生产环境之后调试错误也是很有用的――提供修复错误所需的准确上下文信息。在本文中,Orange Soft 公司(这是一家专业从事面向对象技术、服务器端Java 平台和 Web 可访问性的西班牙公司)的共同创办人 Ruth Zamorano 和 Rafael Luque 阐述了如何利用 log4j 的扩展能力,使得分布式 Java 应用程序能够通过即时消息传送(instant messaging,IM)来监视。
不管您编写多少设计良好的测试用例,即使是最小的应用程序也会在部署到生产环境之后隐藏着一个或多个错误。虽然测试驱动的开发和 QA 手段可以提高代码质量并增强对应用程序的信心,但是当某个系统失败时,开发人员和系统管理员需要了解系统的相关执行上下文信息。有了适当的信息,他们就能确定问题的本质并快速解决问题,从而节省时间和金钱。
监视分布式应用程序要求能够对远程资源进行日志记录――通常是一台中央日志服务器或者系统管理员的计算机。log4j 环境提供一组适用于远程日志记录的 appender,比如 SocketAppender
、 JMSAppender
和SMTPAppender
。在本文中,我们将向您展示一种新的远程类(remote-class)appender: IMAppender
。
让我们首先简要回顾一下 log4j ,然后再深入研究 appender。自然地,理解 appender 的最好方式就是试着编写一个 appender,因此我们将在最后一节实现一个例子 IM(即时消息传送)appender,以说明 AppenderSkeleton
类的工作原理。
读者应该熟悉 log4j 框架。关于 log4j 的更多信息,请参见本文后面的 参考资料 。
log4j 概述
log4j 框架是用 Java 语言编写的事实上的标准日志记录框架。作为 Jakarta 项目的一部分,它在 Apache 软件许可证(Apache Software License)下分发,Apache 软件许可证是由开放源代码促进会(Open Source Initiative ,OSI)认证的一种流行的开放源代码许可证。log4j 环境是完全可配置的,或者通过编程方式完成,或者通过属性中的配置文件或者 XML 格式的配置文件完成。此外,它还允许开发人员无需修改源代码就可以选择性地筛选出日志记录请求。
log4j 环境包括三个主要组件:
- logger(日志记录器):控制要启用或禁用哪些日志记录语句。可以对日志记录器指定如下级别:
ALL
、DEBUG
、INFO
、WARN
、ERROR
,FATA或
OFF
。 - layout(布局):根据用户的愿望格式化日志记录请求。
- appender:向目的地发送格式化的输出。
理解 appender
log4j 框架允许向任何日志记录器附加多个 appender。可以在任何时候对某个日子记录器添加(或删除)appender。附随 log4j 分发的 appender 有多个,包括:
ConsoleAppender
FileAppender
SMTPAppender
JDBCAppender
JMSAppender
NTEventLogAppender
SyslogAppender
也可以创建自己的自定义 appender。
log4j 最主要的特性之一就是它的灵活性。遗憾的是,没有多少现存文档说明了如何编写自己的 appender。学习编写 appender 的方式之一就是分析可用的源代码,然后尝试推断 appender 是如何工作的――本文将帮助 您完成这个任务。
揭开面纱
所有的 appender 都必须扩展 org.apache.log4j.AppenderSkeleton
类,这是一个抽象类,它实现了 org.apache.log4j.Appender
和 org.apache.log4j.spi.OptionHandler
接口。 AppenderSkeleton
类的 UML 类图看起来如图1所示:
图 1. AppenderSkeleton 的 UML 类图
下面让我们研究一下 AppenderSkeleton
类所实现的 Appender
接口的方法。如清单1所示, Appender
接口中的几乎所有方法都是 setter 方法和 getter 方法:
|
这些方法处理 appender 的如下属性:
- name:Appender 是命名的实体,因此有一个针对其名称的 setter/getter。
- layout: Appender 可以具有关联的 Layout,因此还有另一个针对 layout 的setter/getter 方法。注意我们说的是“可以”而不是“必须”。这是因为有些 appender 不需要 layout。lauout 管理格式输出――也就是说,它返回
LoggingEvent
的String
表示形式。另一方面,JMSAppender
发送的事件是 串行化的,因此您不需要对它附加 layout。如果自定义的 appender 不需要 layout,那么requiresLayout()
方法必须返回false
,以避免 log4j 抱怨说丢失了 layout 信息。 errorHandler
: 另一个 setter/getter 方法是为ErrorHandler
而存在的。appender 可能把它们的错误处理委托给一个ErrorHandler
对象――即org.apache.log4j.spi
包中的一个接口。实现类有两个:OnlyOnceErrorHandler
和FallbackErrorHandler
。OnlyOnceErrorHandle
实现 log4j 的默认错误处理策略,它发送出第一个错误的消息并忽略其余的所有错误。错误消息将输出到System.err
。FallbackErrorHandler
实现ErrorHandler
接口,以便能够指定一个辅助的 appender。如果主 appender 失败,辅助 appender 将接管工作。错误消息将输出到System.err
,然后登录到新的辅助 appender。
还有管理过滤器的其他方法(比如 ddFilter()
、 clearFilters()
和 getFilter()
方法 )。尽管 log4j 具有过滤日志请求的多种内置方法(比如知识库范围级、日志记录器级和 appender 阈值级),但它使用自定义过滤器方法的能力也是非常强大的。
一个 appender 可以包含多个过滤器。自定义过滤器必须扩展 org.apache.log4j.spi.Filter
抽象类。这个抽象类要求把过滤器组织为线性链。 对每个过滤器的 decide(LoggingEvent)
方法的调用要按照过滤器被添加到链中的顺序来进行。自定义过滤器基于三元逻辑。 decide()
方法必须返回 DENY
、 NEUTRAL
或者 ACCEPT
这三个整型常量值之一。
除了 setter/getter 方法以及和过滤器相关的方法外,还有另外两个方法: close()
和 doAppend()
。 close()
方法释放 appender 中分配的任何资源,比如文件句柄、网络连接,等等。在编写自定义 appender 代码时,务必要实现这个方法,以便当您的 appender 关闭时,它的 closed 字段将被设置为 true
。
如清单2所示的 doAppend()
方法遵循“四人组模板方法(Gang of Four Template Method )”设计模式(参见 参考资料)。这个方法提供了一个算法框架,它把某些步骤推迟到子类中来实现。
|
如清单2所示,该算法:
- 检查 appender 是否关闭。附加关闭的 appender 是一个编程错误。
- 检查正在记录日志的事件是否处于 appender 的阈值之下。
- 检查是否有过滤器附加到 appender,如果有,则拒绝请求。
- 调用 appender 的
append()
方法。这个步骤被委托给每个子类。
我们已经介绍了 AppenderSkeleton
从 Appender
继承来的方法和属性。下面让我们看看“为什么” AppenderSkeleton
要实现 OptionHandler
接口。 OptionHandler
仅包含一个方法: activateOptions()
。这个方法在对属性调用 setter 方法之后由一个配置器类调用。有些属性彼此依赖,因此它们在全部加载完成之前是无法激活的,比如在 activateOptions()
方法中就是这样。这个方法是开发人员在 appender 变为激活和就绪之前用来执行任何必要任务的机制。
除了上面提到的所有方法,让我们再回头观察一下 图1。注意 AppenderSkeleton
提供了一个新的抽象方法( append()
方法)和一个新的 JavaBean 属性( threshold
)。 threshold
属性由 appender 用来过滤日志记录请求,只有超过阈值的请求才会得到处理。我们在谈到 doAppend()
方法之前就提到了 append()
方法。它是自定义 appender 必须实现的一个抽象方法,因为框架在 doAppend()
方法内调用 append()
方法。 append()
方法是框架的钩子(hook)之一。
现在我们已经看到了 AppenderSkeleton
类中的所有可用方法,下面让我们看看幕后发生的事情。图2演示了 log4j 中的一个 appender 对象的 生命周期。
让我们逐步地研究一下这个图表:
- appender 实例不存在。或许框架还没有配置好。
- 框架实例化了一个新的 appender。这发生在配置器类分析配置脚本中的一个 appender 声明的时候。配置器类调用
Class.newInstance(YourCustomAppender.class)
,这等价于动态调用new YourCustomAppender()
。框架这样做是为了避免被硬编码为任何特定的 appender 名称;框架是通用的,适用于任何 appender。 - 框架判断 appender 是否需要 layout。如果该 appender 不需要 layout,配置器就不会尝试从配置脚本中加载 layout 信息。
- Log4j 配置器调用 setter 方法。在所有属性都已设置好之后,框架就会调用这个方法。程序员可以在这里激活必须同时激活的属性。
- 配置器调用 activateOptions() 方法。在所有属性都已设置好之后,框架就会调用这个方法。程序员可以在这里激活必须同时激活的属性。
- Appender 准备就绪。 此刻,框架可以调用 append() 方法来处理日志记录请求。这个方法由 AppenderSkeleton.doAppend() 方法调用。
- 最后,关闭appender。 当框架即将要删除您的自定义 appender 实例时,它会调用您的 appender 的
close()
方法。close()
是一个清理方法,意味着 您需要释放已分配的所有资源。它是一个必需的方法,并且不接受任何参数。它必须把closed
字段设置为true
,并在有人尝试使用关闭的 appender 时向框架发出警报。
现在我们已经回顾了与建立自己的 appender 相关的概念,下面让我们考虑一个包括真实例子appender 的完整案例研究。
|
编写基于 IM 的 appender
本文给出的代码说明了如何扩展 log4j 框架以集成 IM 特性。它被设计来使得 log4j 相容的应用程序能够把输出记录到 IM 网络上。IM appender 实际上充当一个自定义的 客户机。然而,它不是把 System.out
、文件或者 TCP 套接字当作底层输出设备,而是把 IM 网络当作底层输出设备。
为了提供 IM 支持,我们不需要在开发特定解决方案时完全重新开始。相反,我们将利用一个我们认为是该类别中最好的工具:Jabber。Jabber 是一种用于即时消息传送和展示的基于 XML 的开放协议,它由 Jabber 社区开发,非 营利性的 Jabber 软件基金会(Jabber Software Foundation)对它提供技术支持。
我们之所以选择 Jabber 而没有选择其他 IM 系统,是因为 Jabber 提供了广泛的好处,包括它的:
- 开放性质: 不像其他的专有系统,Jabber 的规范和源代码是可以免费获得的,从而允许任何人无成本地创建 Jabber 实现。
- 简单性:Jabber 使用基于 XML 的简单协议作为它的标准数据格式,并且遵循大多数人都理解的客户机/服务器 架构。
- 与其他 IM 系统的互操作性:Jabber 传输模块(transport module)使得 Jabber 用户访问诸如 AIM、Yahoo!Messager 和 ICQ 等其他即时消息传送系统成为可能。
- 资源敏感:Jabber 对多客户机访问提供明确的支持。同一个用户可以通过不同的客户机(或者说 资源)同时地连接到Jabber 服务器,消息将被恰当地路由到可用的最佳资源。
为什么要把日志记录到 IM 网络?
日志记录是开发人员必须养成的良好编码习惯,就像编写单元测试、处理异常或者编写 Javadoc 注释一样。插入到代码中明确位置的日志记录语句起着审核工具的功能,提供了关于应用程序内部状态的有用信息。与主流意见相反,我们认为在许多情况下,将日志语句保留在生产代码中是方便的。如果您担心计算成本,就必须考虑从应用程序中删除日志记录功能所带来的少量性能提升是否值得。此外,log4j 的灵活性允许您声明式地控制日志记录行为。您可以建立严格的日志记录策略来降低日志的累赘性并改进性能。
图3显示了 IMAppender
的一个使用场景:一个配置为使用 IMAppender
的 log4j 应用程序记录 它的被包装为 IM 消息的调试数据。即时消息通过 Jabber 公司网络被路由到系统管理员的Jabber 地址(注意,公开可用的 Jabber 服务器对生产应用可能不足够可靠)。因而,无论何时系统管理员需要检查应用程序的状态,他们只需加载最喜欢的 Jabber 客户机,然后连接到Jabber 服务器。如图3所示,管理员可以通过不同的设备来访问。他可以使用办公室的 PC 来登录服务器,或者当他离开办公桌时,可以使用运行在手持设备上的 Jabber 客户机来检查消息。
但是为什么需要 IM appender 呢?因为向 IM 服务器发送消息将允许您通过自由选择的工具(比如Jabber客户机)来更容易地监视应用程序行为。
IMAppender
提供了多个优点:
- 获得实时通知――我们称之为“即时日志记录”。
- 支持一对一(聊天)和一对多(小组聊天)模式。
- Jabber 不只是用于台式计算机。用于诸如 PDA 和移动电话等无线设备的客户机也正在开发之中。
- 要进入或退出应用程序正在向其转发日志数据的聊天室很容易。而要订阅和取销订阅由
SMTPAppender
发送的电子邮件则很困难。 - 通过安全套接字层(SSL)上的隧道来保证安全很容易。当然,您可以加密电子邮件,但是 SSL上 的 Jabber 既方便又快捷。
每个消息中传送的日志记录事件的数量是由缓冲区的大小决定的。循环缓冲区仅保留最后的 bufferSize
个日志记录事件,当它装满时就会溢出并丢弃较旧的事件。
为了连接到 Jabber 服务器, IMAppender
需要依赖 Jive Software 公司的 Smack API。Smack 是一个开放源代码的高级库,它处理与 Jabber 服务器通信的协议细节。这样, 您无需任何特别的 Jabber 或者 XML 专业经验就能理解代码。
IMAppender
的属性总结在表 1中:
属性 | 说明 | 类型 | 是否必需 |
host | 服务器的主机名称 | String | 是 |
port | Jabber服务器的端口号 | int | 否,默认为 5222 |
username | 应用程序的Jabber帐户用户名 | String | 是 |
password | 应用程序的Jabber帐户密码 | String | 是 |
recipient | 接收方的Jabber地址。Jabber地址也称为Jabber ID,它在一个@字符后面指定用户的Jabber 域,就像电子邮件地址一样 这个属性可以保存任何聊天地址或者聊天室地址。例如,您可以指定这样的聊天地址:[email protected];或者您可能希望向 [email protected] 小组聊天服务器上名为"java-apps"的某个聊天小组发送日志记录消息(例如, [email protected] ) | String | 是 |
chatroom | 接受一个布尔值。如果为 true , recipient 值将被接受为小组聊天地址。如果要设置这个选项,还应该设置 nickname 选项。默认情况下,recipient 值被解释为一个聊天地址 | boolean | 否,默认为 false |
nickname | 仅当设置了 chatroom 属性时才会考虑这个属性。否则,它将被忽略 用户可以选择 appender 使用的任意小组聊天昵称来加入小组聊天。昵称不一定要和 Jabber用户名有关 | String | 否 |
SSL | 用于保护与 Jabber 服务器的连接 | boolean | 否,默认为 false |
bufferSize | 可以保留在循环缓冲区中的日志记录事件的最大数量 | int | 否,默认为 16 |
evaluatorClass | 这个属性的值被当作一个类的完全限定名称的字符串表示形式,该类实现了 org.apache.log4j.spi. TriggeringEventEvaluator 接口(换句话说,也就是一个包含自定义触发逻辑的类,它覆盖了默认的触发逻辑)。如果没有指定这个选项, IMAppender 将使用 DefaultEvaluator 类的一个实例,这个类根据被指定为 ERROR 或更高级别的事件触发响应 | String | 否,默认为 DefaultEvaluator |
现在让我们进一步观察代码。IMAppender 类遵循清单3所示的结构:
|
请注意关于我们的 appender 的如下几个方面:
IMAppender
类扩展org.apache.log4j.AppenderSkeleton
,这是所有自定义 appender 都必须要做的。IMAppender
从AppenderSkeleton
继承诸如 appender 阈值和自定义过滤之类的公共功能。- 我们的 appender 的第一部分很简单。每个 appender 都有字段和 set/get 方法。属性和方法签名遵守 JavaBeans 命名约定。因而,log4j 能够通过反射来分析 appender,透明地处理 appender 配置。为节省篇幅,上述代码片断仅显示了
setHost()
和getHost()
方法。 - 为了完成我们的 appender,我们必须实现 log4j 框架调用来管理我们的 appender 的回调方法:
requiresLayout()
、activateOptions()
、append()
和close()
。
log4j 框架调用 requiresLayout()
方法来判断自定义 appender 是否需要 layout。注意,有些appender 使用内置格式或者根本就不格式化事件,因此它们不需要 Layout 对象。 IMAppender
需要 layout,因而该方法返回 true
,如 清单4所示:
|
注意, AppenderSkeleton
实现了 org.apache.log4j.spi.OptionHandler
接口(参见 图 1 )。 AppenderSkeleton
把这个接口的单个方法 activateOptions()
实现为一个空方法。我们的 IMAppender
需要这个方法是由于其属性之间的相互依赖性。例如,与 Jabber 服务器的连接依赖 Host
、 Port
和 SSL
属性,因此 IMAppender
在这三个属性被初始化之前无法建立连接。log4j 框架调用 activateOptions()
方法来通知 appender 所有属性都已设置就绪。
IMAppender.activateOptions()
方法激活指定的属性(比如 Jabber 主机、端口、 bufferSize
,等等),所采取的方式是实例化依赖这些属性值的更高级对象,如清单5所示:
清单 5. 只有在调用 activateOptions() 方法之后,属性才会被激活且变得有效
|
activateOptions()
方法完成以下任务:
- 建立
bufferSize
个事件的最大循环缓冲区。我们使用了org.apache.log4j.helpers.CyclicBuffer
的一个实例,org.apache.log4j.helpers.CyclicBuffer
是 log4j 附带的一个辅助类,它提供了缓冲区的逻辑。 - Smack 的
XMPPConnection
类创建了一个到 XMPP (Jabber) 服务器的连接,这个服务器是通过host
和port
属性来指定的。为了创建一个 SSL 连接,我们要使用SSLXMPPConnection
子类。 - 大多数服务器都要求您在执行其他任务之前首先登录,因此我们使用由
username
和password
属性所定义的 Jabber 帐户来登录,同时调用XMPPConnection.login()
方法。 - 在登录之后,我们创建一个
Chat
或者GroupChat
对象,具体视chatroom
值而定。
在 activateOptions()
方法返回之后,appender 就准备好处理日志记录请求了。如 清单6所示,由 AppenderSkeleton.doAppend()
调用的 append()
方法将执行大多数实际的日志附加工作。
|
append()
方法中的第一个语句判断进行附加尝试是否有意义。 checkEntryConditions()
方法检查是否有可用于附加到输出的 Chat
或者 GroupChat
对象,以及是否有用于格式化传入 event
对象的 Layout
对象。如果这些前提条件得不到满足,那么 append()
将输出一条警告消息并返回,从而不会继续进行输出操作。下一个语句把事件添加到循环缓冲区实例 cb
。然后, if
语句把日志记录事件提交给 evaluator
,这是一个 TriggeringEventEvaluator
实例。如果 evaluator
返回 true
,这意味着该事件与触发条件匹配, sendBuffer()
就会被调用。
清单7显示了 sendBuffer()
方法的代码 :
|
sendBuffer()
方法把缓冲区的内容作为IM消息来发送。此方法逐项遍历保留在缓冲区中的事件,同时调用 layout 对象的 format()
方法来格式化每个事件。事件的字符串表示形式被附加到 StringBuffer
对象。最后, sendBuffer()
调用 chat
或者 groupchat
对象的 sendMessage()
方法,把消息发送出去。
请注意以下几点:
AppenderSkeleton.doAppend()
方法(它调用append()
)是经过同步的,因此sendBuffer()
已经拥有 appender 的监视器。这使得我们不必在cb
上执行同步操作。- 异常提供了极其有用的信息。由于这个原因,如果指定的 layout 忽略了包含在
LoggingEvent
对象中的可抛出对象,自定义 appender 的开发人员必须输出包括在事件中的异常信息。如果 layout 忽略了可抛出的对象,那么 layout 的ignoresThrowable()
方法应该返回true
,并且sendBuffer()
可以使用LoggingEvent.getThrowableStrRep()
方法来检索包含在该事件中的可抛出信息的String[]
表示形式。
下载源代码 |
把全部内容组合起来
下面将通过展示 IMAppender
的实际工作效果来结束本文的讨论。我们将使用一个相当简单的名为 com.orangesoft.logging.example.EventCounter
的应用程序,如 清单8所示。这个示例应用程序在命令行接受两个参数。第一个参数是一个整数,对应于要产生的日志记录事件的数量。第二个参数必须是以属性的格式提供的一个 log4j 配置文件名。这个应用程序总是以 ERROR 事件结束,该事件将触发一次 IM 消息传送。
|
我们可以使用类似清单9所示的配置文件:
|
上面的配置文件脚本把 IMAppender
添加到根日志记录器(root logger),这样所接收到的每个日志记录请求都将被分派到我们的 appender。
在试验这个示例应用程序之前,请确保将 host
、 username
、 password
和 recipient
属性设置为 您所在环境中的适当值。下面的命令将运行 EventCounter
应用程序:
|
当运行时, EventCounter
将根据 eventcounter.properties
所设置的策略记录 100 个事件。然后一个 IM 消息将从接收方的屏幕上弹出来。图4、5、6 显示了不同平台上的 Jabber 客户机接收到的结果消息:
图 4. Windows (Rhymbox)上的 Jabber 客户机接收到的消息的屏幕快照
图 5. Linux (PSI)上的 Jabber 客户机接收到的消息的屏幕快照
图 6. Pocket PC (imov)上的 Jabber 客户机接收到的消息的屏幕快照
注意 EventCounter
产生了 100 个事件。然而,由于 IMAppender
缓冲区的默认大小为 16,接收方应该收到仅包含最后 16 个事件的 IM 消息。可以看到,包含在最后一个事件(消息和堆栈跟踪)中的异常信息已经被正确地传送了。
这个例子应用程序只展示了 IMAppender
的一个非常小的用途,因此继续探索它吧,您会找到很多乐趣的!
结束语
log4j 网络 appender, SocketAppender
、 JMSAppender
和 SMTPAppender
已经提供了监视 Java 分布式应用程序的机制。然而,多个因素使得 IM 成为用于实时远程日志记录的合适技术。在本文中,我们介绍了通过自定义 appender 来扩展 log4j 的基础知识,并看到了一个基本 IMAppender
的逐步实现过程。许多开发人员和系统管理员都可以从 appender 的使用中获益。
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
- 下载 IMAppender 类的 源代码 、例子应用程序和必需的库。 您可以根据需要随便使用和扩展这些源代码。
- 从 log4j项目首页 获得最新的 log4j 版本 ――包括完整的源代码、类文件和文档。欲了解更多信息,请在 log4j官方文档页 查看文章和演示材料。
- 如果您的开发工作需要超出本文档范围的内容,可以参考 Ceki Gülcü 编著的一本优秀参考书 log4j完全参考手册 (QOS.ch, 2003),该书详细介绍了 log4j 的基本特性和高级特性。
- developerWorks Web services 专区提供了 LogKit 作为“每周内容”(component of the week)” (2001年8月)。LogKit 是 Jakarta 的 Avalon项目 的日志记录组件。
- Sun 已经完成了一个名为 JSR 47 的社区过程(community process),它定义了 Java 平台的日志记录 API。JSR 47 API和 log4j 在体系结构层次上相当类似,不过 log4j 具有许多 JSR 47 所没有的特性。
- 在“ Merlin的魔力:异常和日志记录”( developerWorks,2001年12月)中,John Zukowski展示了新的 JDK 1.4 日志记录 API 是如何工作的。
- Thomas E. Davis 撰写的 JavaWorld 文章 “ Logging on to Internet Relay Chat (IRC)” 介绍了一种允许应用程序把输出写到 IRC 的简单工具。
- Gerhard Poul 的文章“ Jabber” ( developerWorks,2002年5月)说明了 Jabber 如何适合于当今的电子商务 基础设施。
- Smack 是 Jive Software 公司提供的一个开放源代码库,用于与 Jabber 服务器通信以执行即时消息传送和聊天。
- Iain Shigeoka 的文章 用 Java 实现即时消息传送 (Manning, 2002) 深入分析了各种 Jabber 协议。
- “模板方法(Template Method)设计模式”出自 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 所著的 设计模式 一书(Addison-Wesley出版, 1995年),这四位作者也被称为“四人组(Gang of Four,GoF)。
- 也可以在线阅读 模板方法设计模式 的相关报道。
- 在 developerWorks Java技术专区 可以找到数百篇关于 Java 编程的各个方面的文章。
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文.