日志记录不仅是开发和测试周期中的一个重要元素——提供关键调试信息,而且对于系统已部署到生产环境之后调试错误也是很有用的——提供修复错误所需的准确上下文信息。在本文中,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 环境包括三个主要组件:
ALL
、DEBUG
、INFO
、WARN
、ERROR
, FATA或
OFF
。理解 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 的如下属性:
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所示,该算法:
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 对象的 生命周期。
图 2. appender 的生命周期图
让我们逐步地研究一下这个图表:
Class.newInstance(YourCustomAppender.class)
,这等价于动态调用 new YourCustomAppender()
。框架这样做是为了避免被硬编码为任何特定的 appender 名称;框架是通用的,适用于任何 appender。close()
方法。close()
是一个清理方法,意味着 您需要释放已分配的所有资源。它是一个必需的方法,并且不接受任何参数。它必须把 closed
字段设置为 true
,并在有人尝试使用关闭的 appender 时向框架发出警报。 现在我们已经回顾了与建立自己的 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 提供了广泛的好处,包括它的:
为什么要把日志记录到 IM 网络?
日志记录是开发人员必须养成的良好编码习惯,就像编写单元测试、处理异常或者编写 Javadoc 注释一样。插入到代码中明确位置的日志记录语句起着审核工具的功能,提供了关于应用程序内部状态的有用信息。与主流意见相反,我们认为在许多情况下,将日志语句保留在生产代码中是方便的。如果 您担心计算成本,就必须考虑从应用程序中删除日志记录功能所带来的少量性能提升是否值得。此外,log4j 的灵活性允许您声明式地控制日志记录行为。您可以建立严格的日志记录策略来降低日志的累赘性并改进性能。
图3显示了 IMAppender
的一个使用场景:一个配置为使用 IMAppender
的 log4j 应用程序记录 它的被包装为 IM 消息的调试数据。即时消息通过 Jabber 公司网络被路由到系统管理员的Jabber 地址(注意,公开可用的 Jabber 服务器对生产应用可能不足够可靠)。因而,无论何时系统管理员需要检查应用程序的状态,他们只需加载最喜欢的 Jabber 客户机,然后连接到Jabber 服务器。如图3所示,管理员可以通过不同的设备来访问。他可以使用办公室的 PC 来登录服务器,或者当他离开办公桌时,可以使用运行在手持设备上的 Jabber 客户机来检查消息。
图 3. IMAppender 使用场景
但是为什么需要 IM appender 呢?因为向 IM 服务器发送消息将允许您通过自由选择的工具(比如Jabber客户机)来更容易地监视应用程序行为。
IMAppender
提供了多个优点:
SMTPAppender
发送的电子邮件则很困难。进阶IMAppender
模仿随 log4j 一起分发的 SMTPAppender
的日志记录策略。IMAppender
把日志记录事件存储在一个内部循环缓冲区(cyclic buffer)中,并且仅当所接收到的日志记录请求触发了某个用户指定的条件时,才把这些事件作为即时消息来发送。或者,用户也可以提供一个触发事件鉴别器类(triggering event evaluator class)。然而在默认情况下,消息传送是由指定为 ERROR
或者更高级别的事件所触发的。
每个消息中传送的日志记录事件的数量是由缓冲区的大小决定的。循环缓冲区仅保留最后的 bufferSize
个日志记录事件,当它装满时就会溢出并丢弃较旧的事件。
为了连接到 Jabber 服务器,IMAppender
需要依赖 Jive Software 公司的 Smack API。Smack 是一个开放源代码的高级库,它处理与 Jabber 服务器通信的协议细节。这样, 您无需任何特别的 Jabber 或者 XML 专业经验就能理解代码。
IMAppender
的属性总结在表 1中:
表 1. IMAppender 属性
属性 | 说明 | 类型 | 是否必需 |
host | 服务器的主机名称 | String |
是 |
port | Jabber服务器的端口号 | int |
否,默认为 5222 |
username | 应用程序的Jabber帐户用户名 | String |
是 |
password | 应用程序的Jabber帐户密码 | String |
是 |
recipient | 接收方的Jabber地址。Jabber地址也称为Jabber ID,它在一个@字符后面指定用户的Jabber 域,就像电子邮件地址一样 这个属性可以保存任何聊天地址或者聊天室地址。例如,您可以指定这样的聊天地址:[email protected];或者 您可能希望向 |
String |
是 |
chatroom | 接受一个布尔值。如果为 true ,recipient 值将被接受为小组聊天地址。如果要设置这个选项,还应该设置 nickname 选项。默认情况下,recipient 值被解释为一个聊天地址 |
boolean |
否,默认为 false |
nickname | 仅当设置了chatroom 属性时才会考虑这个属性。否则,它将被忽略用户可以选择 appender 使用的任意小组聊天昵称来加入小组聊天。昵称不一定要和 Jabber用户名有关 |
String |
否 |
SSL | 用于保护与 Jabber 服务器的连接 | boolean |
否,默认为false |
bufferSize | 可以保留在循环缓冲区中的日志记录事件的最大数量 | int |
否,默认为16 |
evaluatorClass | 这个属性的值被当作一个类的完全限定名称的字符串表示形式,该类实现了 |
String |
否,默认为 DefaultEvaluator |
现在让我们进一步观察代码。IMAppender 类遵循清单3所示的结构:
清单 3. IMAppender 类的总体结构 |
请注意关于我们的 appender 的如下几个方面:
IMAppender
类扩展 org.apache.log4j.AppenderSkeleton
,这是所有自定义 appender 都必须要做的。IMAppender
从 AppenderSkeleton
继承诸如 appender 阈值和自定义过滤之类的公共功能。setHost()
和 getHost()
方法。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所示:
|
activateOptions()
方法完成以下任务:
bufferSize
个事件的最大循环缓冲区。我们使用了 org.apache.log4j.helpers.CyclicBuffer
的一个实例,org.apache.log4j.helpers.CyclicBuffer
是 log4j 附带的一个辅助类,它提供了缓冲区的逻辑。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
上执行同步操作。LoggingEvent
对象中的可抛出对象,自定义 appender 的开发人员必须输出包括在事件中的异常信息。如果 layout 忽略了可抛出的对象,那么 layout 的 ignoresThrowable()
方法应该返回 true
,并且 sendBuffer()
可以使用 LoggingEvent.getThrowableStrRep()
方法来检索包含在该事件中的可抛出信息的 String[]
表示形式。
下载源代码 |
把全部内容组合起来
下面将通过展示 IMAppender
的实际工作效果来结束本文的讨论。我们将使用一个相当简单的名为 com.orangesoft.logging.example.EventCounter
的应用程序,如 清单8所示。这个示例应用程序在命令行接受两个参数。第一个参数是一个整数,对应于要产生的日志记录事件的数量。第二个参数必须是以属性的格式提供的一个 log4j 配置文件名。这个应用程序总是以 ERROR 事件结束,该事件将触发一次 IM 消息传送。
|
我们可以使用类似清单9所示的配置文件:
清单 9. 示例 IMAppender 配置文件 |
上面的配置文件脚本把 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 的使用中获益。
参考资料