这篇文章描述了Log4j的API、独一无二的特色和设计原理。Log4j是一个聚集了许多作者劳动成果的开源软件项目。它允许开发人眼以任意的粒度输出日志描述信息。它利用外部的配置文件,在运行时是完全可配置的。最厉害的是,log4j有一条平滑的学习曲线。当心:从用户的反馈表明,它是很容易上瘾的。
几乎每个大型的应用程序都包含有自己的日志记录或跟踪API。与这个原则一致,E.U. SEMPER项目决定写自己的跟踪API。这事发生在1996年。在多次改进以后,经过几次演化和大量的工作使之逐渐变成了log4j,一个流行java日志包。这个软件包是在apache软件许可证的保护下发布的,开源组织主动性保证了这是一个完整的开源许可证。最新的log4j版本包含了源代码、类文件和可以在http://jakarta.apache.org/log4j/找到的文档。顺便说一下,log4j已经被发展到了C, C++, C#, Perl, Python, Ruby,和Eiffel语言。
在代码里插入日志描述代码是一种低级的调试方法。由于调试器并不总是可用的或者可应用的,因此这可能是唯一的方法。这对多线程应用和分布式应用来说是非常普遍的现象。
经验表明日志是开发环节中一个重要的组件。它提供了好多的有点。对一个正在运行的应用程序而言,它可以提供准确的环境信息。一旦插入了代码,日志输出就不需要认为的干涉。还有,日志输出可以保存在永久的媒体中,供以后研究。包括它在开发环节的作用,一个高效的功能丰富的日志包可以被看作一款审计工具。
就像Brian W. Kernighan和Rob Pike在他们的扛鼎之作《编程实践》中写下的
鉴于每个人的选择,我们不提倡使用调试器,除非为了跟踪堆栈或者获得一个变量的值。一个原因是在复杂的数据结构和控制流中是很容易丢失细节的;第二个原因是,我们发现单步跟踪一个程序与仔细思考并在关键的地方添加代码输出描述与自我检查相比是没有效率的。查看所有的描述信息比扫描正确地方输出的信息将花费更多时间。决定在关键放置输出打印语句比单步跟踪更省时间,即使我们知道那在什么地方。跟重要的是,调试语句是和程序放在一起的;而调试会话是暂时的。
日志代码有它自己的缺点。它可能会导致应用程序运行变慢。假如输出太详细,可能会导致屏幕闪动(scrolling blindness)。为了减轻这些影响,log4j被设计为可依赖的,更快的和可扩展的。由于日志很少是应用程序关注的焦点,所以log4j API力争做到简单并易于理解和使用。
Log4j包含三个首要组件:记录器,输出源和布局器。这三类组件一起工作使开发者可以按消息的类别和等级来输出消息,并且控制在运行时这些消息怎么格式化和在哪里输出这些信息。
任意一个log4j API最大的优点是平滑了System.out.println固有的能力,当允许其他人不受妨碍的打印时使某些日志语句不起作用。这个能力假定日志空间,也就是所有的可能的日志语句的空间,是可以按照开发者的标准来分类的。这个观察资料以前已经引导我们选择类别作为包的中心概念。然而,自从 log4j的1.2版本,记录器(Logger
)类已经取代了范围(Category
)类,对那些熟悉log4j早期版本的人来说,记录器(Logger
)类可以被认为仅仅是范围(Category
)类的别名(alias)。
记录器被命名为实体(Loggers are named entities),记录器(Logger)的命名是事件敏感的(case-sensitive),并且他们遵循层次的(hierarchical)命名规则:
按层次命名 假如一个记录器的名称后面跟着一个被认为是子记录器前缀的“.”号,那么它就被认为是另一个记录器的祖先 |
例如,名称为“com.foo
”的记录器是名称为“com.foo.Bar
”的父。相似的是,“java”是“java.util”的父,是“java.util.Vector”的祖先。这个命名规则对大多数的开发人员来说应该是很熟悉的
根记录器(root logger)处于记录器层次的顶端.在两种情况下,它是意外的。
1. 它总是存在
2. 它不可以通过名称获得
调用类的静态方法Logger.getRootLogger获得根类.所有其他类都被实例化,并且用类的静态方法Logger.getLogger获得这些实例。这个方法用期望的记录器作为参数。记录器类的一些基本方法如下:
package org.apache.log4j; public class Logger { // Creation & retrieval methods: public static Logger getRootLogger(); public static Logger getLogger(String name); // printing methods: public void debug(Object message); public void info(Object message); public void warn(Object message); public void error(Object message); public void fatal(Object message); // generic printing method: public void log(Level l, Object message); } |
记录器可以被设置级别。可能的级别包括DEBUG, INFO, WARN, ERROR和FATAL,这些级别被定义在org.apache.log4j.Level类中。尽管我们不鼓励,但是你还是可以通过子类化级别类来定义你自己的级别。一个更好的方法将在后面介绍
假如一个给定的记录器没有被设置级别,它可以集成一个最近的带有指定级别的祖先。更正式地:
Level Inheritance 级别继承 继承的级别被指定给记录器类C,在记录器层次中它是和第一个非空级别相等的 |
为了保证所有的记录器最终可以继承一个级别,根记录器总是有一个被指定的记录器。
下面是四个表,这些表带有不同指定级别值和参照上面规则的继承级别的结果
记录器名称 |
指定的级别 |
继承的级别 |
root |
Proot |
Proot |
X |
none |
Proot |
X.Y |
none |
Proot |
X.Y.Z |
none |
Proot |
范例 1 |
在上面的范例1中,仅仅根记录器被指定了级别。这个级别的值是Proot,它被其它的记录器X, X.Y和X.Y.Z继承
记录器名称 |
指定的级别 |
继承的级别 |
root |
Proot |
Proot |
X |
Px |
Px |
X.Y |
Pxy |
Pxy |
X.Y.Z |
Pxyz |
Pxyz |
范例 2 |
在范例2中所有的记录器都有一个指定的级别值,这就没有必要继承级别值了。
记录器名称 |
指定的级别 |
继承的级别 |
root |
Proot |
Proot |
X |
Px |
Px |
X.Y |
none |
Px |
X.Y.Z |
Pxyz |
Pxyz |
范例 3 |
在范例3中,所有的记录器,包括X 和 X.Y.Z都被分别指定记录器值为Proot、Px和Pxyz。记录器X.Y从它的父X继承它的级别值
记录器名称 |
指定的级别 |
继承的级别 |
root |
Proot |
Proot |
X |
Px |
Px |
X.Y |
none |
Px |
X.Y.Z |
none |
Px |
范例 4 |
在范例4中,记录器root和X分别被指定级别值为Proot和Px。记录器X.Y
和X.Y.Z从最接近它们的父X继承它们的级别值,这个父有一个指定的级别值…
通过调用一个记录器实例的打印方法来处理日志请求。这些打印方法是debug, info, warn, error, fatal和log.
通过定义,打印方法决定一个日志请求的级别。例如,假如c是一个记录器实例,语句c.info("..")是一个带有INFo级别的日志请求。
若日志请求的级等于或者大于日志记录器的级别,那么这个日志请求就是可行的,相反,请求将不能输出。一个没有被指定级别的日志记录器将从层次(hierarchy)继承。这些规则在下面总结。
基本的选择规则 在一个具有q级别的日志记录器中(指定和继承都是合适的)有一个具有p级别的日志请求,若p>=q,则这个日志请求是可以输出的。 |
这个规则是log4j的核心。假定级别是排序的,对标准的级别来说,我们设定DEBUG < INFO < WARN < ERROR < FATAL。 这就是说如果级别定为ERROR,在这个logger上输出了info,error,结果只能打印error的信息 相反如果级别定为INFO,那么warn error fatal的都可以输出到文件上
// get a logger instance named "com.foo" Logger logger = Logger.getLogger("com.foo"); // Now set its level. Normally you do not need to set the // level of a logger programmatically. This is usually done // in configuration files. logger.setLevel(Level.INFO); Logger barlogger = Logger.getLogger("com.foo.Bar"); // This request is enabled, because WARN >= INFO. logger.warn("Low fuel level."); // This request is disabled, because DEBUG < INFO. logger.debug("Starting search for nearest gas station."); // The logger instance barlogger, named "com.foo.Bar", // will inherit its level from the logger named // "com.foo" Thus, the following request is enabled // because INFO >= INFO. barlogger.info("Located nearest gas station."); // This request is disabled, because DEBUG < INFO. barlogger.debug("Exiting gas station search"); |
用同一个名称调用getLogger方法将返回一个指向同一个记录器对象的引用
例如,
Logger x = Logger.getLogger("wombat"); Logger y = Logger.getLogger("wombat"); |
x
和y
指向同一个日志记录器对象
因此,配置一个日志记录器,不用在代码里转换引用就可以获得相同的实例是可能的。在生物学父时代的基本矛盾里面,总是可以preceed他们的孩子,log4j的日志记录器可以按一定的规则创建和配置。尤其是,即使一个“父”日志记录器在它的子孙后面被实例化,它仍然可以发现并连接到它的子孙
在应用程序初始化的时候,Log4j的配置被执行。最好的方法是通过读取一个配置文件。很快就会讨论这个方法
通过使用软件组件,log4j很容易命名日志记录器。这可以通过在每个类中静态实例化日志记录器来完成,日志记录器名是和完整的类名相同的。这是一个直截了当地定义日志记录器的有用方法。由于日志输出带有产生该日志的日志记录器的名称,命名策略让辨认产生日志消息的源头很容易。然而,这仅仅是是一个可能,虽然命名日后子记录器的策略很普通(However, this is only one possible, albeit common, strategy for naming loggers)。Log4j没有约束日志记录器可能趋势(Log4j does not restrict the possible set of loggers)。开发者可以很自由的根据需要命名日志记录器。
不过,在类后面命名日志记录器好像是目前所知最好的策略
基于日志记录器有选择地让日志请求是否起作用地能力仅仅描述(picture)地一部分.log4j允许日志请求输出到多个目标。在log4j地声明中,输出目的地被成为输出源(appender).最近,输出源包括控制然台、文件、GUI组件、远程套接字服务器(remote socket servers)、JMS、NT事件记录器(NT Event Loggers)和远程UNIX Syslog守护进程(remote UNIX Syslog daemons)。它也可以异步地记录日志。
一个日志记录器可以有多个输出源。
AddAppender方法加一个输出源到给定的日志记录器。对应给定的日志记录器每个激活的日志请求都将被转向到所有的输出源,因为这些输出源是和层次 (hierarchy) 中更高级别的输出源一样的。换句话说,输出源是被从日志记录器的层次附加继承的(appenders are inherited additively from the logger hierarchy)。例如,假如有一个控制台输出源被加到一个根日志记录器(root logger),最后所有被激活的日志请求都将打印在控制台上,另外,假如一个文件输出源被加到日志记录器,叫做C,然后激活到C和C的孩子的日志请求将输出到一个文件和控制台。覆盖这个默认的行为是可能,以便于通过设定附加标识为假,输出源的聚集不再是附加的。
管理输出源附加行为的规则将在下面总结。
输出源的附加特性 日志记录器C的日志语句的输出将定向到C和它的祖先中的所有的输出源。这是条款“输出源的附加特性(appender additivity)”的意图. 然而,假如有一个日志记录器C的祖先,叫做P,有一个附加标识被设置为false,然后C的输出将被定向到C和直到C的祖先P(包括P)中的所有的输出源,但是不包括P的祖先的中的任何输出源。 日志记录器有它自己附加特性,该特性被默认设置为true |
下表展示了一个例子:
日志记录器 |
添加的输出源 |
附加特性标识 |
输出目标 |
评论 |
root |
A1 |
not applicable |
A1 |
根日志记录器是匿名的,但是可以用Logger.getRootLogger()方法来存取。根日志记录器没有默认输出源。 |
x |
A-x1, A-x2 |
true |
A1, A-x1, A-x2 |
x和根的输出源 |
x.y |
none |
true |
A1, A-x1, A-x2 |
x和根的输出源 |
x.y.z |
A-xyz1 |
true |
A1, A-x1, A-x2, A-xyz1 |
x.y.z 、x和根的输出源 |
security |
A-sec |
false |
A-sec |
由于附加标识被设置为false,没有输出源聚集 |
security.access |
none |
true |
A-sec |
由于安全中附加标识被设置为false,所以仅仅有安全的输出源。 |
时常,用户不仅希望自定义目的地,而且包括输出格式的自定义。这是通过给输出源设定一个布局器(layout)来到达目的地。
例如,带有"%r [%t] %-5p %c - %m%n"转换格式的PatternLayout布局器将输出和下面的内容类似。
176 [main] INFO org.foo.Bar - Located nearest gas station.
第一个字段是自从程序开始到目前花费的时间。第二个字段是发出日志请求的线程。第三个字段是日志语句的级别。第四个是和该日志请求关联的日志记录器的名称。紧接着“-”符号后面的内容是日志语句的消息。
正像这样重要,log4j将按用户指定的标准修饰(render,这样翻译,不知是否合适)日志信息的内容。例如,假如你经常需要记录Oranges,这是一个在你当前项目中使用的对象类别,你可以注册一个OrangeRenderer类,在某个orang需要记录日志的时候将调用OrangeRenderer类
对象的修饰(Object rendering)遵循类层次。例如,假定oranges是水果,若你注册了一个FruitRenderer类,包括所有的oranges水果都将被FruitRenderer类修饰,除非你给orange指定一个OrangeRenderer。
renderer对象必须实现ObjectRenderer接口
插入应用程序代码的日志请求需要相当大的准备和努力。观察表明大约有4%的代码是用来输出日志的。结果,即使适度大小的应用程序也有数以千计的日志语句被嵌在代码中。给定他们数字,管理这些语句变成了急迫的事情,而不需要没有手工修改的。
Log4j的环境是完全参数化的配置。然而,用配置文件配置log4j是非常灵活的。目前,配置文件可以使用XML或者java属性文件(键值)格式
然我们尝试一下怎样用log4j 配置一个虚构的应用程序MyApp
。
import com.foo.Bar; // Import log4j classes. import org.apache.log4j.Logger; import org.apache.log4j.BasicConfigurator; public class MyApp { // Define a static logger variable so that it references the // Logger instance named "MyApp". static Logger logger = Logger.getLogger(MyApp.class); public static void main(String[] args) { // Set up a simple configuration that logs on the console. BasicConfigurator.configure(); logger.info("Entering application."); Bar bar = new Bar(); bar.doIt(); logger.info("Exiting application."); } } |
MyApp从导入相关类开始。它然后使用MyApp定义了一个静态的日志记录器变量,这个MyApp恰好是一个完整的类名。
MyApp用到了定义在com.foo包中的Bar类
package com.foo; import org.apache.log4j.Logger; public class Bar { static Logger logger = Logger.getLogger(Bar.class); public void doIt() { logger.debug("Did it again!"); } } |
BasicConfigurator.configure方法的调用创建了一个比较简单的log4j设置。这个方法是硬连线(hardwired)地添加到根日志记录器的ConsoleAppender输出源。输出将使用布局器PatternLayout来格式化,布局器PatternLayout被设定为"%-4r [%t] %-5p %c %x - %m%n"的格式。
注意默认值,根日志记录器被设定为Level.DEBUG级别。
MyApp的输出是:
0 [main] INFO MyApp - Entering application.
36 [main] DEBUG com.foo.Bar - Did it again!
51 [main] INFO MyApp - Exiting application.
下面这个图描述了在调用BasicConfigurator.configure 方后之后MyApp的对象图
作为一个侧面的注意点,我要提及的是log4j中的子类仅仅连接到他们存在的祖先。特别的,名为com.foo.Bar的日志记录器被直接连接到根日志记录器,因而围绕在未使用的com或者com.foo日志记录器。这个显著地提高了性能,并且减少了log4j地内存消耗(footprint)
MyApp类通过调用BasicConfigurator.configure方法来配置log4j。其他类仅仅需要导入org.apache.log4j.Logger,取回他们想要的日志记录器,并且在远处记录。
前面的例子总是输出相同的日志信息。幸运的是,很容易修改MyApp,以便可以在运行是控制日志输出。下面是一个稍微修改的版本
import com.foo.Bar; import org.apache.log4j.Logger; import org.apache.log4j.PropertyConfigurator; public class MyApp { static Logger logger = Logger.getLogger(MyApp.class.getName()); public static void main(String[] args) { // BasicConfigurator replaced with PropertyConfigurator. PropertyConfigurator.configure(args[0]); logger.info("Entering application."); Bar bar = new Bar(); bar.doIt(); logger.info("Exiting application."); } } |
这个版本的MyApp构造了PropertyConfigurator类来解析一个配置文件,因此建立日志
下面是一个配置文件的实例,这个配置文件导致输出和前面也基于这个实例的BasicConfigurator类的输出完全相同的。
# Set root logger level to DEBUG and its only appender to A1. log4j.rootLogger=DEBUG, A1 # A1 is set to be a ConsoleAppender. log4j.appender.A1=org.apache.log4j.ConsoleAppender # A1 uses PatternLayout. log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n |
假定我们不再对com.foo包中任何组件的输出感兴趣。下面的配置文件展示了一个可能方法,利用这个方法可以完成这个任务。
log4j.rootLogger=DEBUG, A1 log4j.appender.A1=org.apache.log4j.ConsoleAppender log4j.appender.A1.layout=org.apache.log4j.PatternLayout # Print the date in ISO 8601 format log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n # Print only messages of level WARN or above in the package com.foo. log4j.logger.com.foo=WARN |
用这个文件配置的MyApp的输出如下所示。
2000-09-07 14:07:41,508 [main] INFO MyApp - Entering application.
2000-09-07 14:07:41,529 [main] INFO MyApp - Exiting application.
由于日志记录器com.foo.Bar没有给定级别,它从com.foo继承它的级别,在配置文件中com.foo被设定为WARN。Bar.doIt方法的日志语句有DEBUG级别,这比日志记录器的级别的WARN低。因此doIt()方法的日志请求被禁止
下面是有多个输出源的配置文件。
log4j.rootLogger=debug,
|
评论