Log4j简明手册
1. 概述
本文主要描述Log4j的API的唯一特性和它的设计原理。Log4j是一个基于许多作者的开放源码的项目。它允许开发员以任意的间隔来控制日志的输出。它通过设在外部的配置文件而达到运行时灵活的设置。最重要的是,Log4j有一个平稳的学习曲线。注意:根据来自用户的反馈判断,它很容易使人上瘾。
2. 导言
几乎所有的大型应用程序都包括它的自己的日志和跟踪API。顺应这个规则,E.U. SEMPER 项目决定写它自己的跟踪PAI。这是1996年初。在无数次加强,几次变形和许多工作后,那个API变成了如今的Log4j,一个流行的java日志包。这个包以Apache Software License协议发布,一个成熟的开放源吗协议。最新的Log4j版本,包括全部的源码,class文件和文档,你可以在http://jakarta.apache.org/Log4j/上找到。顺便,Log4j已经给C, C++, C#, Python, Ruby, and Eiffel 语言都提供了接口。
为了调试而插入日志输出到代码里是一个低技术成分的方法,但它可能也是唯一的方法,因为调试器并不是一直可用或者可以适应的,尤其对于多线程的分布使式的大型程序而言。
经验指出调试是软件开发周期中一个重要的组成部分。
Log4j拥有几个优点:
l 首先,它提供关于运行程序的准确的环境。一旦代码被插入,不需要人工干预就可以产生调试信息。
l 其次,日志输出可以被有计划的保存在永久媒体中以便日后研究。
l 另外,除了在开发周期中,一个充分详尽的日志包可以被用来作为以后的统计工具。
Log4j当然还有它的缺点,它可能减慢程序。如果太详细,它可能导致屏幕盲目滚动。排除这些情况,Log4j是可靠的,快速的,可以扩展的。因为日志很少是一个应用程序的主要目的, 设计者们正努力使得Log4j API学习和使用简单化。
3. 日志类别、输出源和布局
Log4j有三个主要的组件:日志类别(Loggers)、输出源( Appenders)和布局(Layouts)。这三种类型的组件一起工作使得开发员可以根据信息的类型和级别记录它们,并且在运行时控制这些信息的输出格式和位置。
3.1 日志类别的层次结构(Loggers)
Log4j首要的相对于简单的使用System.out.println()方法的优点是基于它的在禁止一些特定的信息输出的同时不妨碍其它信息的输出的能力。这个能力源自于日志命名空间,也就是说,所有日志声明的空间,它根据一些开发员选择的公式而分类。以前的观察引导我们选择类别作为包的中心概念。然而,自从Log4j的1.2版本,Logger类被Catalog类所取代,对于那些熟悉Log4j以前版本的人来说,Logger类可以被想象成仅仅是Category 类的别名。
Loggers 被指定为实体,Logger的名字是大小写敏感的,它们遵循以下的命名规则:
命名继承
如果类别的名称(后面加一个点)是其子类别名称的前缀,则它就是另一个类别的祖辈。
如果一个类别(Logger)和它的子类别之间没有其它的继承关系,我们就称之为parent与child的关系。
例如,类别"com.foo"是类别"com.foo.Bar"的parent。相似的,"java"是"java.util"的parent,是"java.util.Vector"的父辈。.这个命名规则应该被大多数的开发员所熟悉。
根(root) 类别位于logger继承结构的最上层。它有两种例外:
1.它一直存在
2.它不能根据名称而获得。
调用类的静态方法Logger.getRootLogger可以得到它。其它所有的Logger可以通过静态方法Logger.getLogger而得到它们自己的实例。这个方法取希望的Logger名作为参数。Logger的一些基本的方法示例如下:
package org.apache.Log4j;
public Logger class {
// 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);
// generic printing method:
public void log(Level l, Object message);
}
Loggers可以被分配的级别。所有级别的集合包括:
l DEBUG
l INFO
l WARN
l ERROR
l FATAL
它们被定义于org.apache.Log4j.Level 类。虽然我们不鼓励,但是你们可以通过继承Level类来定义你们自己的级别。我们随后将介绍一个比较好的方法。
如果一个Logger没有被分配一个级别,那么它将从一个被分配了级别的最接近它的ancestor哪里继承。
级别继承
对于一个给定的Logger C,它的继承的级别等于从C开始上溯到的第一个拥有非空级别的Logger的级别。
为了保证所有的Logger最终能够继承到一个级别,根Logger通常有一个已经定义了的级别。
以下四个表中的数据演示了根据以上规则得到的结果。
类别名 | 分配的级别 | 继承的级别 |
Root | Proot | Proot |
X | none | Proot |
X.Y | none | Proot |
X.Y.Z | none | Proot |
Example 1
在例子1中,只有根Logger定义了一个级别,它的级别的值--"Proot"被所有其它的Loggers X, X.Y, 和X.Y.Z所继承。
类别名 | 分配的级别 | 继承的级别 |
root | Proot | Proot |
X | Px | Px |
X.Y | Pxy | Pxy |
X.Y.Z | Pxyz | Pxyz |
Example 2
在例子2中,所有的Logger都有一个被分配的级别值,所以它们不需要级别继承。
类别名 | 分配的级别 | 继承的级别 |
root | Proot | Proot |
X | Px | Px |
X.Y | none | Px |
X.Y.Z | Pxyz | Pxyz |
Example 3
在例子3中,根Logger,以及X和X.Y.Z被分别分配了级别Proot,Px和Pxyz。Logger X.Y从它的parent X继承了级别值Px。
类别名 | 分配的级别 | 继承的级别 |
root | Proot | Proot |
X | Px | Px |
X.Y | none | Px |
X.Y.Z | none | Px |
Example 4
在例子4中,根Logger和X被分别分配了级别"Proot"和"Px",Logger X.Y 和 X.Y.Z从被分配了级别的最接近它们的ancestor X那里得到继承。
我们需要通过调用Logger的输出的实例方法之一来实现日志请求。这些输出的方法是debug, info, warn, error, fatal 和 log.
通过定义输出方法来区分日志的请求的级别。例如,如果c是一个Logger的实例,那么声明 c.info 就是一个INFO级别的日志请求。
如果一个日志的请求的级别高于或等于日志的级别那么它就能被启用。反之,将被禁用。一个没有被安排级别的Logger将从它的父辈中得到继承。这个规则总结如下。
基本的选择规则
假如在一个级别为q的Logger中发生一个级别为p的日志请求,如果p>=q,那么请求将被启用。
这是Log4j的核心原则。它假设级别是有序的。对于标准级别,我们定义DEBUG < 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 progamitcally. This is usually done
// in configuration files.
cat.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对象的实例。
例如,
Categoty x = Logger.getLogger("wombat");
Categoty y = Logger.getLogger("wombat");
x和y参照的是同一个Logger对象。
这样我们就可以先定义一个Logger,然后在代码的其它地方不需传参就可以重新得到我们已经定义了的Logger的实例.
同基本的生物学理论--父先于子相反,Log4j 的loggers可以以任何顺序创造和配置。特别是,一个后实例化的"parent"logger能够找到并且连接它的子logger。
配置Log4j的环境通常在一个应用程序被初始化的时候进行,最好的方法是通过读一个配置文件。
这个方法我们将简短介绍。
Log4j使得通过软件组件命名logger很容易。我们可以通过Logger的静态的初始化方法在每一个类里定义一个logger,令logger的名字等于类名的全局名,而实现logger的命名。这是一个实效的简单的定义一个logger的方法。因为日志输出带有产生日志的类的名字,这个命名策略使得我们更容易定位到一个日志信息的来源。虽然普通,但却是命名logger的常用策略之一。Log4j没有限制定义logger的可能。开发员可以自由的按照它们的意愿定义logger的名称。
然而,以类的所在位置来命名Logger好象是目前已知的最好方法。
3.2 输出源(Appenders)和布局(Layouts)
有选择的能用或者禁用日志请求仅仅是Log4j的一部分功能。Log4j允许日志请求被输出到多个输出源。用Log4j的话说,一个输出源被称做一个Appender 。Appender包括console(控制台), files(文件), GUI components(图形的组件), remote socket servers(socket 服务), JMS(java信息服务), NT Event Loggers(NT的事件日志), and remote UNIX Syslog daemons(远程UNIX的后台日志服务)。它也可以做到异步记录。
一个logger可以设置超过一个的appender。
用addAppender 方法添加一个appender到一个给定的logger。对于一个给定的logger它每个生效的日志请求都被转发到该logger所有的appender上和该logger的父辈logger的appender上。换句话说,appender自动从它的父辈获得继承。举例来说,如果一个根logger拥有一个console appender,那么所有生效的日志请求至少会被输出到console上。如果一个名为C的logger有一个file类型的appender,那么它就会对它自己以及所有它的子logger生效。我们也可以通过设置appender的additivity flag 为false,来重载appender的默认行为,以便继承的属性不在生效。
调节输出源(appender)添加性的规则如下。
输出源的可添加性(Appender Additivity )
一个名为C的logger的日志定义的输出将延续到它自身以及它的ancestor logger的appenders。这就是术语"appender additivity"的含义。
然而,logger C的一个ancestor logger P,它的附加标志被设为false,那么C的输出将被定位到所有C的appender,以及从它开始上溯到P的所有ancestor logger的appender。
Loggers的附加标记(additivity flag)默认为true。
下表是一个例子。
LoggerName | AddedAppenders | AdditivityFlag | Output Targets | Comment |
root | A1 | not applicable | A1 | The root logger is anonymous but can be accessed with the Logger.getRootLogger() method. There is no default appender attached to root. |
x | A-x1, A-x2 | true | A1, A-x1, A-x2 | Appenders of "x" and root. |
x.y | None | true | A1, A-x1, A-x2 | Appenders of "x" and root. |
x.y.z | A-xyz1 | true | A1, A-x1, A-x2, A-xyz1 | Appenders in "x.y.z", "x" and root |
security | A-sec | false | A-sec | No appender accumulation since the additivity flag is set to false.
|
security.access | None | true | A-sec | Only appenders of "security" because the additivity flag in "security" is set to false. |
经常,用户希望自定义不但输出源,而且定义输出格式。这个是通过在一个appender上附加一个layout来完成的。layout是负责根据用户的希望来格式化日志请求。而appender是负责发送格式化的输出到它的目的地。PatternLayout,作为Log4j标准版中的一部分,让用户指以类似C语言的printf方法的格式来指定日志的输出格式。
例如,转化模式为"%r [%t] %-5p %c - %m%n" 的PatternLayout 将输出类似如下的信息:
176 [main] INFO org.foo.Bar - Located nearest gas station.
第一个栏位是自从程序开始后消逝的毫秒数。
第二个栏位是做出日志的线程。
第三个栏位是log的级别。
第四个栏位是日志请求相关的logger的名字。而"-"后的文字是信息的表述。
Log4j将根据用户定义的公式来修饰日志信息的内容。例如,如果你经常需要记录Oranges,一个在你当前的项目被用到的对象类型,那么你可以注册一个OrangeRenderer ,它将在一个orange需要被记录时被调用。
对象渲染类似的类的结构继承。例如,假设oranges是fruits,如果你注册了一个FruitRenderer,所有的水果包括oranges将被FruitRenderer所渲染。除非你注册了一个orange。
对象渲染必须实现ObjectRenderer接口。
4. 配置
插入日志请求到应用程序的代码中需要大量的预先计划和最终努力。观察显示大约4%的代码是用来输出的。
因此,大小适度的程序都被嵌入有成千个日志输出语句。为了以无需手工的方式管理这些日志的输出状态,给日志输出以编号和规范变得势在必行。
Log4j在程序中有充分的可配置性。然而,用配置文件配置Log4j具有更大的弹性。目前,它的配置文件支持xml和java properties(key=value)文件两种格式。
让我们以一个例子来演示它是如何做的。假定有一个用了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以引入Log4j的相关类开始,接着它定义了一个静态logger变量,并给予值为"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的设置。它加入一
个ConsoleAppender到根logger。输出将被采用了"%-4r [%t] %-5p %c %x - %m%n"模式的PatternLayout所格式化。
注意,根logger默认被分配了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的子logger只连接到已经存在的它们的父代。特别的是,名为com.foo.bar的logger是直接连接到根logger,而不是围绕着没用的com或com.foologger。这显著的提高了程序性能并且减少的内存占用。
MyApp类配置Log4j是通过调用BasicConfigurator.configure 方法。其它的类仅仅需要引入org.apache.Log4j.Logger 类,找到它们希望用的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.
当logger com.foo.bar没有被分配一个级别,它将从com.foo继承,在配置文件中它被设置了WARN的级别。在Bar.doIt方法中定义的log为DEBUG级别,低于WARN,因此doIt() 方法的日志请求被禁用。
这里是另外一个配置文件,它使用了多个appenders.
Log4j.rootLogger=debug, stdout, R
Log4j.appender.stdout=org.apache.Log4j.ConsoleAppender
Log4j.appender.stdout.layout=org.apache.Log4j.PatternLayout
# Pattern to output the caller's file name and line number.
Log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n
Log4j.appender.R=org.apache.Log4j.RollingFileAppender
Log4j.appender.R.File=example.log
Log4j.appender.R.MaxFileSize=100KB
# Keep one backup file
Log4j.appender.R.MaxBackupIndex=1
Log4j.appender.R.layout=org.apache.Log4j.PatternLayout
Log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n
以这个配置文件调用加强了的MyApp类将输出如下信息.
INFO [main] (MyApp2.java:12) - Entering application.
DEBUG [main] (Bar.java:8) - Doing it again!
INFO [main] (MyApp2.java:15) - Exiting application.
另外,因为根logger有被分配第二个appender,所以输出也将被定向到example.log文件。这个文件大小达到100kb时将自动备份。备份时老版本的example.log文件自动被移到文件example.log.1中。
注意我们不需要重新编译代码就可以获得这些不同的日志行为。我们一样可以容易的使日志输出到UNIX Syslog daemon, 重定向所有的com.foo到NT Event logger,或者转发日志到一个远程的Log4j服务器,它根据本地server的策略来进行日志输出。例如转发日志事件到第二个Log4j服务器.
5. 默认的初始化过程
Log4j类库不对它的环境做任何假设。特别是没有默认的Log4j appender。在一些特别的有着良好定义的环境下,logger的静态inializer将尝试自动的配置Log4j。
java语言的特性保证类的静态initializer当且仅当装载类到内存之时只会被调用一次。 要记住的重要一点是,不同的类装载器可能装载同一个类的完全不同的拷贝。 这些同样类的拷贝被虚拟机认为是完全不相干的。
默认的initialization是非常有用的,特别是在一些应用程序所依靠的运行环境被准确的定位的情况下。例如,同一样的应用程序可以被用做一个标准的应用程序,或一个applet,或一个在web-server控制下的servlet。
准确的默认的initialization原理被定义如下:
1. 设置系统属性Log4j.defaultInitOverride为"false"以外的其它值,那么Log4j将跳过默认的initialization过程。
2. 设置资源变量字符串给系统属性Log4j.configuration。定义默认initialization文件的最好的方法是通过系统属性Log4j.configuration。万一系统属性Log4j.configuration没有被定义,那么设置字符串变量resource 给它的默认值Log4j.properties。
3. 尝试转换resource 变量为一个URL。
4. 如果变量resource的值不能被转换为一个URL,例如由于MalformedURLException违例,那么通过调用org.apache.Log4j.helpers.Loader.getResource(resource, Logger.class) 方法从classpath中搜索resource,它将返回一个URL,并通知"Log4j.properties"的值是一个错误的URL。看See Loader.getResource(java.lang.String) 查看搜索位置的列表。
5. 如果没有URL被发现,那么放弃默认的initialization。否则用URL配置Log4j。
PropertyConfigurator将用来解析URL,配置Log4j,除非URL以".xml"为结尾。在这种情况下的话DOMConfigurator将被调用。你可以有机会定义一个自定义的configurator。系统属性Log4j.configuratorClass 的值取自你的自定义的类名的全路径。你自定义的configurator必须实现configurator接口。
6. 配置范例
6.1 Tomcat下的初始化
默认的Log4j initialization典型的应用是在web-server 环境下。在tomcat3.x和tomcat4.x下,你应该将配置文件Log4j.properties放在你的web应用程序的WEB-INF/classes 目录下。Log4j将发现属性文件,并且以此初始化。这是使它工作的最容易的方法。
你也可以选择在运行tomcat前设置系统属性Log4j.configuration 。对于tomcat 3.x,TOMCAT_OPTS 系统变量是用来设置命令行的选项。对于tomcat4.0,用系统环境变量CATALINA_OPTS 代替了TOMCAT_OPTS。
Example 1
UNIX 命令行
export TOMCAT_OPTS="-DLog4j.configuration=foobar.txt"
告诉Log4j用文件foobar.txt作为默认的配置文件。这个文件应该放在WEB-INF/classes 目录下。这个文件将被PropertyConfigurator所读。每个web-application将用不同的默认配置文件,因为每个文件是和它的web-application 相关的。
Example 2
UNIX 命令行
export TOMCAT_OPTS="-DLog4j.debug -DLog4j.configuration=foobar.xml"
告诉Log4j输出Log4j-internal的调试信息,并且用foobar.xml作为默认的配置文件。这个文件应该放在你的web-application的WEB-INF/classes 目录下。因为有.xml的扩展名,它将被DOMConfigurator所读。每个web-application将用不同的默认配置文件。因为每个文件都和它所在的web-application 相关的。
Example 3
UNIX 命令行
set TOMCAT_OPTS=-DLog4j.configuration=foobar.lcf -DLog4j.configuratorClass=com.foo.BarConfigurator
告诉Log4j用文件foobar.lcf作为默认的配置文件。这个文件应该放在你的web-application的WEB-INF/classes 目录下。因为定义了Log4j.configuratorClass 系统属性,文件将用自定义的com.foo.barconfigurator类来解析。每个web-application将用不同的默认配置文件。因为每个文件都和它所在的web-application 相关的。
Example 4
UNIX 命令行
set TOMCAT_OPTS=-DLog4j.configuration=file:/c:/foobar.lcf
告诉Log4j用文件foobar.lcf作为默认的配置文件。这个配置文件用URL file:/c:/foobar.lcf定义了全路径名。这样同样的配置文件将被所有的web-application所用。不同的web-application将通过它们自己的类装载器来装载Log4j。这样,每个Log4j的环境将独立的运作,而没有任何的相互同步。例如:在多个web-application中定义了完全相同的输出源的FileAppenders将尝试写同样的文件。结果好象是缺乏安全性的。
你必须确保每个不同的web-application的Log4j配置没有用到同样的系统资源。
6.2 Servlet 的初始化
用一个特别的servlet来做Log4j的初始化也是可以的。如下是一个例子:
package com.foo;
import org.apache.Log4j.PropertyConfigurator;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.IOException;
public class Log4jInit extends HttpServlet {
public void init() {
String prefix = getServletContext().getRealPath("/");
String file = getInitParameter("Log4j-init-file");
// if the Log4j-init-file is not set, then no point in trying
if(file != null) {
PropertyConfigurator.configure(prefix+file);
}
}
public void doGet(HttpServletRequest req, HttpServletResponse res) {
}
}
在web.xml中定义随后的servlet为你的web-application。
<servlet>
<servlet-name>Log4j-init</servlet-name>
<servlet-class>com.foo.Log4jInit</servlet-class>
<init-param>
<param-name>Log4j-init-file</param-name>
<param-value>WEB-INF/classes/Log4j.lcf</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
写一个初始化的servlet是最有弹性的初始化Log4j的方法。代码中没有任何限制,你可以在servlet的init方法中定义它。
7. Nested Diagnostic Contexts
在现实世界中的系统经常不得不同时处理多个客户端请求。在这样的一个典型的多线程的系统中,不同的线程将处理不同的客户端。Logging特别能够适应这种复杂的分布式的应用程序的调试和跟踪。一个常见的区分每个客户端所输出的Logging的方法是为每个客户端实例化一个新的独立的Logger。这导致Logger的大量产生,管理的成本也超过了logging本身。
唯一标识每个log请求是一个轻量级的技术。Neil Harrison 在名为“Patterns for Logging Diagnostic Messages”的书中描述了这个方法in Pattern Languages of Program Design 3, edited by R. Martin, D. Riehle, and F. Buschmann (Addison-Wesley, 1997).
为了唯一标识每个请求,用户把上下文信息推入NDC(Nested Diagnostic Context)中。
NDC类示例如下:
public class NDC {
// Used when printing the diagnostic
public static String get();
// Remove the top of the context from the NDC.
public static String pop();
// Add diagnostic context for the current thread.
public static void push(String message);
// Remove the diagnostic context for this thread.
public static void remove();
}
NDC如同一个堆栈样管理每个线程。注意所有the org.apache.log4j.NDC 类的方法都是静态的。假设NDC输出被开启,每次一个log 请求被生成时,适当的log4j组件为将要输出log的线程包含完整的NDC堆栈。这是在没有用户的干预的情况下做到的,用户只负责在NDC中定位正确的信息,通过在代码中正确位置插入很少的push和pop方法就行了。相反的,在代码中per-client实现方法有着很大变化。
为了演示这个点,让我们以一个发送内容到匿名客户端的servlet为例。这个servlet可以在开始执行每个其他代码前的初始化时建立NDC。上下文信息可以是客户主机的名字和其他的请求中固有的信息。
典型的信息包括cookies。因此,即使servlet同时为多个客户同时提供服务,log 被同样的代码初始化,例如属于同一个logger,依然可以被区别,因为每个客户请求将有不同的NDC堆栈。与之相比,Contrast this with the complexity of passing a freshly instantiated logger to all code exercised during the client's request。
不过,一些诡异的程序,例如虚拟主机的web server记录日志,不是一般的依靠虚拟主机的上下文,还要依靠软件的组件发出请求。近来log4j的发布版本支持多层的树形结构。这个增强允许每个虚拟主机可以处理在树型结构中属于它自己的logger。
8. 优化
一个经常引用的依靠于logging的参数是可以计算的花费。这是一个合理的概念,一个适度的应用程序可能产生成千上万个日志请求。许多努力花在测量和调试logging的优化上。Log4j要求快速和弹性:速度最重要,弹性是其次。用户应该注意随后的优化建议。
1.日志为禁用时,日志的优化。
当日志被彻底的关闭,一个日志请求的花费等于一个方法的调用加上整数的比较时间。在233mhz的Pentium II 机器上这个花费通常在5-50纳秒之间。然而,方法调用包括参数构建的隐藏花费。
例如,对于logger cat,
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
引起了构建信息参数的花费,例如,转化整数i和entry[i]到一个string,并且连接中间字符串,不管信息是否被输出。这个参数的构建花费可能是很高,它主要决定于被调用的参数的大小。
避免参数构建的花费应如下,如果logger的debug被关闭这将不会招致参数构建的花费。另一方面,如果logger是debug的话,它将产生两次判断 logger是否能用的花费。一次是在debugenabled,一次是debug。这是无关紧要的,因为判断日志的能用 只占日志实际花费时间的约1%。
在Log4j里,日志请求在Logger 类的实例里。Logger 是一个类,而不是一个接口。这大量的减少了在方法调用上的弹性化的花费。
当然用户采用预处理或编译时间技术去编译出所有的日志声明。这将导致完美的执行成效。然而因为二进制应用程序不包括任何的日志声明的结果,日志不可能对那个二进制程序开启。以我的观点,以这种较大的代价来换取较小的性能优化是不值得的。
2。当日志状态为启用时,日志的优化。
这是本质上的优化logger的层次。当日志状态为开,Log4j依然需要比较请求的级别与logger的级别。然而, logger可能没有被安排一个级别;它们将从它们的father继承。这样,在继承之前,logger可能需要搜索它的ancestor。
这里有一个认真的努力使层次的搜索尽可能的快。例如,子logger仅仅连接到它的存在的father logger。
在先前展示的BasicConfigurator 例子中,名为com.foo.bar 的logger是连接到跟根logger,因此绕过 了不存在的logger com和com.foo。这将显著的改善执行的速度,特别是解析logger的层结构时。
典型的层次结构的解析的花费是logger彻底关闭时的三倍。
3.日志信息的输出时,日志的优化。
这是主要花费在日志输出的格式化和发送它到它的输出源上。这里我们再一次的付出努力以使格式化执行的尽可能快。同appender一样。实际上典型的花费大约是100-300毫秒。详情看org.apache.log4.performance.Logging。
虽然Log4j有许多特点,但是它的第一个设计目标还是速度。一些Log4j的组件已经被重写过很多次以改善性能。不过,投稿者经常提出了新的优化。你应该满意的知道,以SimpleLayout的配置执行测试已经展示了Log4j的输出同System.out.println一样快。
9. 总结
Log4j是一个用java写成的流行的日志包。一个它与众不同的特点是在logger中的继承的概念。用logger的继承可以以任意的间隔控制日志的状态输出。这个减少了体积和最小化日志的代价。
易管理性是Log4j API的优点之一。只要日志定义被加入到代码中,它们就可以用配置文件来控制。它们可以有选择的被禁用,并且发送到不同的多个输出源上,以用户选择好的格式。
Log4j的包被设计成,不需花费沉重的执行代价就可以保留它们在产品中。