log4j中文文档 中文详细教程
这篇文章描述了Log4j的API、独一无二的特色和设计原理。Log4j是一个聚集了许多作者劳动成果的开源软件项目。它允许开发人眼以任意的粒度输出日志描述信息。它利用外部的配置文件,在运行时是完全可配置的。最厉害的是,log4j有一条平滑的学习曲线。当心:从用户的反馈表明,它是很容易上瘾的。
介绍
几乎每个大型的应用程序都包含有自己的日志记录或跟踪API。与这个原则一致,E.U. 项目决定写自己的跟踪API。这事发生在1996年。在多次改进以后,经过几次演化和大量的工作使之逐渐变成了log4j,一个流行java日志包。这个软件包是在apache软件许可证的保护下发布的,开源组织主动性保证了这是一个完整的开源许可证。最新的log4j版本包含了源代码、类文件和可以在找到的文档。顺便说一下,log4j已经被发展到了C, C++, C#, Perl, Python, Ruby,和Eiffel语言。
在代码里插入日志描述代码是一种低级的调试方法。由于调试器并不总是可用的或者可应用的,因此这可能是唯一的方法。这对多线程应用和分布式应用来说是非常普遍的现象。
经验表明日志是开发环节中一个重要的组件。它提供了好多的有点。对一个正在运行的应用程序而言,它可以提供准确的环境信息。一旦插入了代码,日志输出就不需要认为的干涉。还有,日志输出可以保存在永久的媒体中,供以后研究。包括它在开发环节的作用,一个高效的功能丰富的日志包可以被看作一款审计工具。
就像Brian W. Kernighan和Rob Pike在他们的扛鼎之作《编程实践》中写下的
鉴于每个人的选择,我们不提倡使用调试器,除非为了跟踪堆栈或者获得一个变量的值。一个原因是在复杂的数据结构和控制流中是很容易丢失细节的;第二个原因是,我们发现单步跟踪一个程序与仔细思考并在关键的地方添加代码输出描述与自我检查相比是没有效率的。查看所有的描述信息比扫描正确地方输出的信息将花费更多时间。决定在关键放置输出打印语句比单步跟踪更省时间,即使我们知道那在什么地方。跟重要的是,调试语句是和程序放在一起的;而调试会话是暂时的。
日志代码有它自己的缺点。它可能会导致应用程序运行变慢。假如输出太详细,可能会导致屏幕闪动(scrolling blindness)。为了减轻这些影响,log4j被设计为可依赖的,更快的和可扩展的。由于日志很少是应用程序关注的焦点,所以log4j API力争做到简单并易于理解和使用。
记录器(Loggers),输出源(Appenders)和布局器(Layouts)
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继承它们的级别值,这个父有一个指定的级别值…
通过调用一个记录器实例的打印方法来处理日志请求。这些打印方法是, , , , 和.
通过定义,打印方法决定一个日志请求的级别。例如,假如c是一个记录器实例,语句c.info("..")是一个带有INFo级别的日志请求。
若日志请求的级等于或者大于日志记录器的级别,那么这个日志请求就是可行的,相反,请求将不能输出。一个没有被指定级别的日志记录器将从层次(hierarchy)继承。这些规则在下面总结。
基本的选择规则 在一个具有q级别的日志记录器中(指定和继承都是合适的)有一个具有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 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组件、远程套接字服务器( servers)、JMS、NT事件记录器()和远程UNIX Syslog守护进程(remote UNIX 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, 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.
In addition, as the root logger has been allocated a second appender, output will also be directed to the example.log
file. This file will be rolled over when it reaches 100KB. When roll-over occurs, the old version of example.log
is automatically moved to example.log.1
.
另外,由于根日志记录器被分配给第二个输出源,输出将定向到example.log文件。当这个文件增展100KB时,将会翻转(
rolled over)这个文件,当翻转发生时,老版本的example.log将被自动移到example.log.1文件
注意,为获得这些不同的日志行为,我们不必重编译代码。我们可以很简单记录UNIX Syslog进程,重定向com.foo
所有的输出到一个NT事件记录器,或者定向日志事件到一个远程log4j服务器,这些都可以依照一个本地服务器规则记录日志,例如,可以通过定向一个日志事件到第二个log4j服务器。
默认的初始化进程
Log4j类库没有对它的环境作任何假设。特别是,log4j没有默认的输出源。然而,在某些定义明确的环境下,日志记录器类的静态的初始化器将尝试自动配置log4j。java语言保证在往内存中装载类时,类的静态初始化器仅仅可以被调用一次。不同类装载器可能装载相同类的不同拷贝,记住这是很重要的。Java虚拟机认为这些相同类的拷贝是完全不相关的。
在依赖运行环境的应用程序的正确入口处,默认的初始化是非常有用的。例如,在web服务器(web-server)的控制下,相同的应用程序可以被当作一个独立的应用程序、applet或者servlet。
下面定义的是确切的默认注视化算法:
1. 设定log4j.defaultInitO覆盖系统属性的为任何其它值,“false”将导致log4j忽略默认的初始化过程(这个过程)。
2. 设定资源字符串变量为log4j.configuration的系统属性值。指定默认初始化文件的最好方法是通过log4j.configuration的系统属性。万一系统属性log4j.configuration没有定义,可以设定字符串变量资源到它默认值“log4j.properties”。
3. 尝试转换资源变量为URL
4. 假如资源变量不能转换为URL,例如由于一个MalformedURLException异常,然后通过调用返回值为URL的org.apache.log4j.helpers.Loader.getResource(resource, Logger.class)方法在classpath中查找资源。注意,字符串"log4j.properties"包含(constitutes)一个丑陋的URL。
参考Loader.getResource(java.lang.String)方法,获得查找路径的列表。
5. 假如没有URL,忽略默认的初始化。其它,通过URL来配置log4j。
常常使用PropertyConfigurator类解析URL来配置log4j,若URL以“.xml”后缀名结束,将使用DOMConfigurator来解释。你可以有选择指定自定义的配置器。log4j.configuratorClass的系统属性值被当作你自定义配置器的完整类名。你所指定的自定义配置器必须实现Configurator接口。
配置实例
在Tomcat下的默认配置
在web服务器环境中,Log4j默认的初始化是特别有用的。在Tomcat 3.x 和 4.x下,你应该把log4j.properties放置在你web应用程序的WEB-INF/classes目录下。Log4会发现这个属性文件并自己进行初始化。这是很容易做的。
在tomcat启动之前,你也可以选择设置系统属性log4j.configuration。Tomcat 3.x的环境变量TOMCAT_OPTS被用来设置命令行选项。Tomcat 4.0设置CATALINA_OPTS环境变量替代TOMCAT_OPTS。
实例 1
Unix shell命令
输出TOMCAT_OPTS="-Dlog4j.configuration=foobar.txt"告诉log4j用文件foobar.txt作为默认的配置文件。这个文件应该被放置在你应用程序WEB-INF/classes的目录下。每个web应用程序将用一个不同的默认配置文件,因为每个文件都是和相对web应用程序的
实例 2
Unix shell命令
输出TOMCAT_OPTS="-Dlog4j.debug -Dlog4j.configuration=foobar.xml"告诉log4j输出log4j的内部调试信息,并用文件foobar.xml作为默认的配置文件。这个文件应该被放置在你应用程序WEB-INF/classes的目录下。由于文件是以.xml后缀结束的,所以它将使用DOMConfigurator来读文件。每个web应用程序将用一个不同的默认配置文件,因为每个文件都是和相对web应用程序的
实例 3
Windows shell命令
设置TOMCAT_OPTS=-Dlog4j.configuration=foobar.lcf -Dlog4j.configuratorClass=com.foo.BarConfigurator告诉log4j用文件foobar.lcf
作为默认的配置文件。由于log4j.configuratorClass这个系统属性的定义,文件将用com.foo.BarConfigurator定义的配置器(
configurator)来读取。每个web应用程序将用一个不同的默认配置文件,因为每个文件都是和相对web应用程序的
实例 4
Windows shell命令
设置TOMCAT_OPTS=-Dlog4j.configuration=file:/c:/foobar.lcf告诉log4j用文件c:foobar.lcf作为默认的配置文件。配置文件被完整指定通过用URL文件:/c:/foobar.lcf。因此所有的应用程序都使用相同的配置文件。
不同的web应用程序将通过他们各自的类加载器(classloaderss)来加载log4j类。因此,log4j环境的每个映象(image)是独立执行的,并且不需要任何相互同步。例如,FileAppenders在多个web应用程序配置中定义相同的方法,这将全部写入相同的文件。这个结果很可能是不太让人满意的。你必须确保不同web应用程序的log4j配置不用相同的潜在系统资源。
初始化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应用程序的web.xml文件中定义下面servlet
|
写一个初始化的servlet是初始化log4j的最灵活方法
嵌套诊断环境
大多数的现实世界的系统必须能并发地处理多个客户端。在这样一个典型的多线程系统中,不同线程将处理不同客户端。记录日志是特别适于跟踪和调试复杂的分布式应用程序。从一个客户端区分出另一个客户端的日志输出的简单方法是给每个客户端实例化以日志记录器。这将导致日志记录器的增加,并且增加了整个日志的维护工作。
一个简单的技术是对每个从相同客户端交互发出日志请求都有独一无二的时间戳。Neil Harrison在《Patterns for Logging Diagnostic Messages》书的Pattern Languages of Program Design 3(edited by R. Martin, D. Riehle, and F. Buschmann (Addison-Wesley, 1997))中描述了这个方法。
每个请求都有唯一的时间戳,使用者把环境信息压进NDC,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当作环境信息堆栈来管理。注意,org.apache.log4j.NDC类的所有的方法都是静态的。假定NDC打印是被证明了的,每次一个日志请求被处理,在日志输出信息中,适当的log4j组件将包含当前线程的完整NDC堆栈。这个在没有用户感受的情况下完成,这个用户负责的仅仅是通过在代码的某些定义明确的点使用push和pop方法,在NDC中放置正确的信息,
为了说明这一点,让我们举一个例子,这个例子是servlet分发内容到许多客户端。在执行其他代码之前Servlet可以在请求开始的时候编译NDC。环境信息可能是客户端主机的名字和请求内在的其他信息,典型的是包含cookie的信息。因此,即使sevlet在同时服务多个客户端,日志也会通过相同的代码开始记录,也就是说,即使依附于相同的日志记录器,由于每个客户顿请求包含不同的NDC堆栈,日志仍然是可以区分的。
然而,一些健壮的应用程序,例如虚拟主机web服务器,必须根据不同的虚拟主机环境和请求中的软件组件,记录不同不同信息。最近log4j发布的版本开始支持多层次树。这个提高允许每个虚拟主机拥有它自己的日志记录器层次的拷贝
性能
经常提到的放对日志记录的是计算消耗。这是一个合理的关注,即使适度大小的应用程序也可能产生数以千计的日志请求。大量工作被花费在调节和提高日志的性能上。Log4j主张快和灵活:速度第一,灵活性第二。
The user should be aware of the following performance issues.
用户应该注意下面的性能问题
1. 当关闭日志时的日志性能。
当日志被完全关掉的时候,或者恰恰设置为一个级别,日志请求的消耗包括一个方法调用和一个整型比较。在以233 MHz Pentium II的机子上,消耗在5到50纳秒范围内。
然后,方法调用隐含了参数构造的“隐形”消耗。
例如,一些日志记录器,如下,
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
构造消息参数导致消耗,也就是,转换整型I和数组entry[i]为字符串,还有连接媒介字符串,不论消息是否记录。这个参数转换的消耗可能时非常高的,并且依赖于转换参数的大小。
避免构造参数消耗的写法:
if(logger.isDebugEnabled() {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}
这个将不会导致构造参数的消耗,假如调试器是不可用的。另一方面,假如日志记录器是可调试的,无论日志记录器是否可用,它将导致两次计算消耗。这是一个无关紧要的费用(overhead),因为评估(evaluating)一个日志记录器的消耗大约是日志记录器真正消耗的1%。
在log4j里面,日志请求是由日志记录器实例处理的。日志记录器是一个类而不是一个接口。这就显著地减少方法调用的消耗,当然这是以灵活性为代价的(This measurably reduces the cost of method invocation at the cost of some flexibility)。
某些用户利用预处理或者实时编译技术编译所有的日志语句。这将导向优秀的性能效率而不影响日志记录。然而,由于应用程序二进制不包含任何日志语句,日志记录不可能变成二进制(logging cannot be turned on for that binary)。我的观点,这是一个不成比例的代价,为获得小性能的提高。
2. 当日志记录开始启动,决定是否记录日志的性能
跨越日志记录器层次的性能是潜在的。当日志记录开始时,log4j将需要比较日志请求和日志请求处理器的级别。然而,日志记录器不可以有指定的级别;他们可以从日志记录器层次继承级别。因此。在继承一个级别前,日志记录器可能需要搜索它的祖先。
已经花费大量努力让跨越这个层次尽可能的快。例如,子日志记录器仅仅连接到他们存在的祖先。在早先展示的BasicConfigurator实例中,名为com.foo.Bar的日志记录器被直接连接到根日志记录器,因此回避了com和com.foo的不存在。这显著地提高了跨越地速度,特别是在“稀疏的”层次关系
跨越层次的典型消耗比日志记录被完全关闭的时候大约慢3倍。
3. 真正地输出信息
这是格式化日志输出并把它发向目标的消耗。在这儿,大量的努力再次被花费在让布局器(格式化器)执行尽可能地快。这是和输出源相同地。真正地记录日志的典型消耗大约从100到300微秒。参考org.apache.log4.performance.Logging获得准确数字。
尽管log4j有许多特点,但是它的首要目标是速度。为了提高性能,一些log4j组件已经被重写了好多次了。然而,贡献者还在不断地提出新地优化。你应该很高兴,当你知道使用SimpleLayout配置记录日志时,测试显示log4j和System.out.println地速度一样快。
结论
Log4j是一个用java写的优秀日志软件包。它与众不同地特色之一是继承日志记录器概念。用用日志记录器层次,使按任意的粒度控制日志语句输出成为可能。这有助于减少日志输出量,并最小化日志消耗。
Log4j API的特色之一是它的易管理性。一旦日志语句被插入代码,他们可以用配置文件来控制。他们可以有选择的激活或不激活。Log4j包被设计以便于日志语句可以以漂码(in shipped code)的方式保存,而不会致使大量的性能消耗。
感谢
非常感谢N. Asokan审阅了这篇文章。他logger概念的创立者之一。感谢Nelson Minar鼓励我写这片文章。他也对这篇文章提出了许多有用的建议和修正。Log4j是集体努力的结果。谢谢所有对这个项目作出贡献的作者。毫无例外,在包中最好的特色已经在用户社群中广泛创建了。