摘要
这个文档资料描述了log4j API,它的独特的特性和设计原理。Log4j是由许多作者共同参与的开放源代码项目。它允许开发人员以任意的精细程度控制哪些日志说明被输出。通过使用外部的配置文件,可以在运行时配置它。最好的是,log4j 开发包很容易上手。注意,它也可能会使一些开发人员着迷。
简 介
几乎每个大的应用程序都有它自己的日志和跟踪程序的API。顺应这一规则,E.U. SEMPER项目组决定编写它自己的程序跟踪API(tracing API)。这开始于1996年早期。经过无数的工作,更改和性能加强,这个API终于成为一个十分受欢迎的Java日志软件包,那就是log4j。这个软件包的发行遵守open source动议认证的Apache Software License。最新的log4j版本包括全部的源代码,类文件和文档资料,可以在 http://logging.apache.org/log4j/找到它们。另外,log4j已经被转换成 C, C++, C#, Perl, Python, Ruby, 和 Eiffel 语言。
把log statements插入到你的代码中是一种排错的低技能办法。这也许是唯一的方法,因为排错工具并不总是可以被使用或者适用于你的程序。对于多线程的应用程序和多数发行的应用程序,通常就是这样的情形。
经验告诉我们logging是开发过程中重要的一环。它具有多种优点。首先,它能精确地提供运行时的上下文(context)。一旦在程序中加入了Log 代码,它就能自动的生成并输出logging信息而不需要人为的干预。另外,log信息的输出可以被保存到一个固定的地方,以备以后研究。除了在开发过程中发挥它的作用外,一个性能丰富的日志记录软件包能当作一个审计工具(audit tool)使用。
Brian W. Kernighan 和 Rob Pike 在他们的"The Practice of Programming" 书中这样写到: "The Practice of Programming"
作为个人的选择,除了得到一大堆程序跟踪信息或一两个变量值以外,我们倾 向於不使用排错器。一个原因是在详细而复杂的数据结构和控制流程中很容易 迷失;我们发现认真思考并在关键处加入自我检查代码和输出指令,比起一步 步看程序要效率高。在日志说明里查找比在明智地放置自我检查代码后的输出 里查找要费时。而决定在哪里放置打印指令要比在日志说明里一步步找到关键 的代码要省时间。更重要的是,自我检查的排错指令和程序并存;而排错 sessions是暂时的。
Logging确实也有它的缺陷。它降低了程序运行的速度。它太冗长,查看时很容易错过。为了减少这些负面影响,log4j 被设计得可靠,高效和灵活。因为,记录日志很少是一个应用程序的主要焦点,log4j API 尽量做到容易被理解和使用。
Loggers, Appenders and Layouts
Log4j 有三个主要组件:loggers, appenders和layouts。这三类组件一起应用,可以让开发人员能够根据日志的类型和级别进行记录,并且能在程序运行时控制log信息输出的格式和往什么地方输出信息。
Logger hierarchy
任何logging API 与简单的System.out.println
输出调试信息方法比较,最主要的优点在于它能够关闭一些调试信息输出而不影响其他人的调试。这种能力的实现是假设这些logging空间,也就是所有的可能发生的日志说明空间,可以根据程序开发人员选择的标准进行分类。这一观察以前使得我们选择了category作为这个软件包的中心概念。但是,在log4j 1.2版本以后,
类取代了Logger
类。对于那些熟悉早先版本的log4j的开发人员来说,Logger类只不过是Category类的一个别名。 Category
Loggers是被命名的实体。Logger的名字大小写有区别(case-sensitive),并且它们遵守阶层式的命名规则:
|
例如,叫做"com.foo"的logger是叫做 "com.foo.Bar"的logger的父辈 。同样地,"java"是"java.util" 的父辈,是"java.util.Vector"的前辈。大多数开发人员都熟悉这种命名方法。 "com.foo"
"com.foo.Bar"
"java"
"java.util"
"java.util.Vector"
根(root)logger 位于logger 阶层的最上层。它在两个方面很特别:
- 它总是存在的,
- 不能通过使用它的名字直接得到它。
通过这个类的静态方法Logger.getRootLogger得到它(指RootLogger)。所有其他的loggers是通过静态方法Logger.getLogger来实例化并获取的。这个方法Logger.getLogger把所想要的logger的名字作为参数。 Logger类的一些其它基本方法在下面列出:
package org.apache.log4j; public class Logger { // Creation and 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); } |
Loggers可以被指派优先级别。Level.html#DEBUG">DEBUG, INFO, WARN, ERROR 和FATAL这组级别在org.apache.log4j.Level
类中有定义。你也可以通过Level类的子类去定义你自己的优先级别,尽管我们不鼓励你这样做。在后面我们会讲到一个更好的方法。
如果一个logger没有被指定优先级别,它将继承最接近的祖先所被指定的优先级别。下面是更多关于优先级别的信息:
|
要保证所有的loggers最终都继承一个优先级别,root logger总是有一个被指派的优先级。
下面是具有各种指派优先级别值的四个表格,以及根据上面的规则所得出的继承优先级别。
Logger name(名称) |
指派 级别 |
继承 级别 |
---|---|---|
根 | Proot | Proot |
X | none | Proot |
X.Y | none | Proot |
X.Y.Z | none | Proot |
在上面的示例1中,只有root logger被指派了级别。这个级别的值,Proot
,被其它的loggers X, X.Y
和 X.Y.Z
继承了。
Logger name(名称) |
指派 级别 |
继承 级别 |
---|---|---|
根 | Proot | Proot |
X | Px | Px |
X.Y | Pxy | Pxy |
X.Y.Z | Pxyz | Pxyz |
在上面的示例2中,所有的loggers都有一个指派的级别值。不需要级别继承。
Logger name(名称) |
指派 级别 |
继承 级别 |
---|---|---|
根 | Proot | Proot |
X | Px | Px |
X.Y | none | Px |
X.Y.Z | Pxyz | Pxyz |
在示例3中,loggers root
, X 和
分别被指派级别值X.Y
.ZProot
, Px
和Pxyz
。Logger X.Y 从它的父辈X那里继承它的级别值。
Logger name(名称) |
指派 级别 |
继承 级别 |
---|---|---|
根 | Proot | Proot |
X | Px | Px |
X.Y | none | Px |
X.Y.Z | none | Px |
在示例4中,loggers root
和X 分别被指派级别值Proot
和Px
。Logger X.Y
和X.Y.Z
继承它们最接近的父辈X的被指派的级别值。
日志请求是通过调用一个日志实例的打印方法(之一)而产生的。这些打印方法是 log
4j/Logger.html#debug(java.lang.Object)">debug, info, warn, error, fatal 和 log。
根据定义,打印方法决定一个日志请求的级别。例如,如果c是一个日志实例,那么语句c.info("..") 就是级别为INFO的一个日志请求。 c.info("..")
只有一个日志请求(A logging request)的级别高于或等于它的logger级别的时候才能够被执行。否则,则被认为这个日志请求不能被执行。一个没有被定义优先级别的logger将从层次关系中的前辈那里继承优先级别。这个规则总结如下:
|
这个规则是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
方法,返回的reference总是指向完全相同的logger对象。
例如,在这里:
Logger x = Logger.getLogger("wombat"); Logger y = Logger.getLogger("wombat"); |
因此,通过这种方式可以配置一个logger,而不需要传递references就能在其他地方得到相同的实例。在生物的父子关系中父母总是排放在孩子们前面, log4j loggers与此有相互矛盾的地方,那就是log4j loggers可以以任何顺序被产生和配置。特别的是,一个"parent" logger 会找到并连接他的后代,即使他是在他们之后被定义。
Log4j环境通常是在程序被初始化的时候被配置的。最好的方式是通过阅读一个配置文件去配置。我们会马上讨论到这方面的内容。
Log4j使得通过软件组件的名称去定义loggers的名字很容易。这可以通过在每个类中静态地instantiating一个logger,让logger的名字与这个合格的java类文件名相同来完成。这是一种有用并且直观的定义loggers的方式。因为日志的输出带有产生它们的logger的名字,这种命名策略使我们能够很方便地识别这些log信息的来源。不过,尽管这是通用的一种loggers命名策略,Log4j没有限制怎样对loggers进行命名。开发程序员可以根据自己的喜好随意定义 loggers。 software component
当然,至今所知的最好的命名策略还是以它们所在的类的名称来命名 loggers。
Appenders and Layouts
基于自身的logger选择性地使用或不使用日志请求(logging requests )的能力仅仅整个Log4j能力的一部分。Log4j允许将log信息输出到许多不同的输出设备中。用log4j的语言来说,一个log信息输出目的地就叫做一个appender。目前,log4j 的appenders可以将log信息输出到console,files,GUI components,remote socket servers, JMS,NT Event Loggers,和 remote UNIX Syslog daemons。它还可以同时将log信息输出到多个输出设备中。 NT Event Loggers
多个appenders可以和一个logger连接在一起。
使用addAppender方法把一个appender加入到给定的logger上。一个给定的 logger的每一个被允许的日志请求都会被传递给这个logger的所有appenders,以及阶层中高级别的appenders。换句话说appenders是从logger阶层中不断添加地被继承的。例如,一个 console appender加给了root logger,那么,这个root logger所有被允许输出的日志信息将被输出到console。如果你又给一个名字为C的logger添加了一个 file appender,那么C 以及C的子辈的所有被允许的日志信息将被同时输出到 file appender和console appender。可以通过把additivity flag设置为false
来覆盖这个默认的行为从而使appender的继承关系不再是添加性的。 Each enabled logging request for a given logger will be forwarded to all the appenders in that logger as well as the appenders higher in the hierarchy. setting the additivity flag
支配appender添加性的规则总结如下:
|
下面的表格显示一个示例:
Logger name(名称) |
添加的 Appenders |
Additivity 旗标 |
输出目标 | 注释 |
---|---|---|---|---|
根 | A1 | not applicable | A1 | Root logger是无名的,但是可以通过Logger.getRootLogger() 来访问。Root logger没有附带默认的appender。 |
x | A-x1, A-x2 | true | A1, A-x1, A-x2 | "x" 和root logger里的Appenders。 |
x.y | none | true | A1, A-x1, A-x2 | "x" 和root logger里的Appenders。 |
x.y.z | A-xyz1 | true | A1, A-x1, A-x2, A-xyz1 | "x.y.z", "x" 和root logger里的Appenders。 |
安全 | A-sec | false | A-sec | 因为additivity flag被设置为 false ,所以没有appender继承积累。 |
security.access | none | true | A-sec | 因为"security" logger里的additivity flag被设置为false ,所以仅仅只有"security" logger的appenders。 |
通常,用户不仅希望自己指定log信息的输出目的地,而且,他们还希望指定 log信息的输出格式。这可以通过和appender相关的layout实现。Layout负责根据用户的需要去格式化log信息的输出,而appender负责将一个格式化过的 log信息输出到它的目的地。PatternLayout 是标准log4j发行包中的一部分,它让用户根据和C语言中的printf
方法相似的转换模式指定输出格式。
例如,具有"%r [%t] %-5p %c - %m%n" 转换格式的PatternLayout 将输出以下的式样:
176 [main] INFO org.foo.Bar - Located nearest gas station.
第一个区域是从程序开始运行到输出日志信息所用的毫秒数。第二个区域是产生日志请求的线程。第三个区域是这个log语句的优先级别。第四个区域是和日志请求相关联的logger名字。在'-' 之后的文字是这个log信息的内容。
同样重要的是,log4j 将根据用户指定的标准来表达log信息的内容。例如,如果你经常需要日志记录Oranges
,Oranges是你当前项目中使用的一个对象类型,那么你可以注册一个OrangeRenderer
,这样每当需要日志记录一个 orange时,OrangeRenderer就会被调用。
对象的表达遵照类阶层(class hierarchy)形式。例如,假设oranges是 fruits,你注册了一个
,那么,包括oranges在内的所有的fruits 都将由FruitRenderer来表达,除非你自己为orange注册了一个特定的 FruitRenderer
OrangeRenderer
。
Object renderers必须实施ObjectRenderer界面。
配 置
在程序代码中插入这些日志请求需要相当大的工作量。调查显示,大约%4左右的代码是logging。因此,即便是中等大小的应用程序也需要在它们的代码中至少包含有几千行的log语句。就从这个数目来看,管理这些log语句而不用人工地去修改它们是十分重要的。
Log4j环境是完全能够通过编程来配置的。但是使用配置文件去配置则更灵活。目前,Log4j的配置文件是以XML格式和JAVA properties (key=value) 格式编写的。
假设我们有个叫MyApp
的程序使用log4j,让我们来看看这是怎样做到的:
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."); } } |
类首先引入log4j的相关类,然后定义一个命名为MyApp的静态logger变量,而这个名字恰好和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的设置。这个方法将一个 ConsoleAppender添加到root logger,从而让log信息输出到 console。通过把PatternLayout设置为 %-4r [%t] %-5p %c %x - %m%n来确定输出格式。
注意,默认的root 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 的子代loggers只和它们现有的前辈链接。在这里,名字叫
的logger直接和com
.foo.Barroot
logger链接,因此绕过了没有被使用的com 或com.foo
loggers。这样极大地提高了log4j的性能并减少了内存(memory)的使用。
通过调用BasicConfigurator.configure
方法来配置MyApp
类。其它的类只需要引入org.apache.log4j.Logger
类,获取它们想要使用的loggers,就可以输出 log。
先前的例子总是输出同样的log信息。幸运的是,很容易修改MyApp
程序就可以在程序运行时对log输出进行控制。下面是略加修改后的版本:
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
方法去解读配置文件并设置相应的logging 。
这里是一个配置文件的示例,这个配置文件产生和前面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中继承优先级别,而com.foo的优先级别在配置文件中被设置为WARN。 com.foo
.BarBar.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会把下列输出信息输出到控制台(console)上。
INFO [main] (MyApp2.java:12) - Entering application. DEBUG [main] (Bar.java:8) - Doing it again! INFO [main] (MyApp2.java:15) - Exiting application.
另外,当root logger增加了第二个appender时,log信息将同时也被输出到
文件中。当example.log文件达到100KB 后,example.log文件将被rolled over。当roll-over 发生时,example.log 的老版本将自动被移到 example.log
example.log.1
中去。
注意,要获得这些不同的logging行为并不需要重新编译代码。我们还可以简单地通过修改log配置文件把log信息输出到UNIX Syslog daemon中,把所有 com.foo
的日志输出转指向NT Event logger 中,或者把log事件输出到远程 log4j服务器中,当然它要根据局部服务器规则进行log,例如可以把log事件输出到第二个log4j服务器中去。
默认的初始化过程
Log4j库没有对它的环境作任何假设。特别是,没有默认的log4j appenders。不过在一些精细定义过的情况下,这个Logger
类的静态的initializer会试图自动配置log4j。 Java语言确保一个类的静态的initializer在这个类被装载到内存里时被调用一次,而且仅仅一次。这点很重要,要记住不同的classloaders会装载同一个类的不同复制版。这些同一个类的不同复制版在JVM看来是完全不相关的。
默认的初始化在这样的环境中很有用处,那就是同一个程序依据运行时的环境作不同用处。例如,同样一个程序可以在web-server的控制下作为单独的程序,作为一个applet,或者作为一个servlet被使用。
默认的初始化运算法则定义如下:
- 把log4j.defaultInitOverride的系统属性设置为 "false"以外的任何值将会造成 log4j跳过默认的初始化过程。
- 把
这个string变量设置为log4j.configuration系统属性的值。最好的方法指定默认初始化文件是通过log4j.configuration系统属性来指定。在log4j.configuration系统属性没有被定义的情况下,把resource这个string变量设置成它的默认值"log4j.properties"。resource
- 把
resource
变量转换为一个URL。 - 如果这个
resource
变量不能转换为一个URL,例如,因为MalformedURLException
的缘故,那么就通过调用 org.apache.log4j.helpers.Loader.getResource(resource, Logger.class)在 classpath上搜寻resource,它会返回一个URL。注意, string "log4j.properties"是一个不合式的URL。org.apache.log4j.helpers.Loader.getResource(resource, Logger.class)
有关搜寻地址列单,请参看Loader.getResource(java.lang.String)。
- 如果不能找到URL,那就放弃默认的初始化。否则,从URL配置log4j 。
Configurator.html">PropertyConfigurator将被用于解读URL来配置log4j,除非这个URL以".xml"扩展符结束,若这个URL以".xml"扩展符结束,DOMConfigurator则被使用。你可以选择性地指定一个客户自己的configurator。log4j.configuratorClass系统属性的值就是你客户自己的configurator的类名。你指定的客户configurator必须 实施Configurator接口。
配置示例
Tomcat下默认的初始化
默认的log4j初始化在web-server环境中特别有用。在Tomcat 3.x and 4.x下,你应该把log4j.properties
放置在你的网络程序的WEB-INF/classes
目录下面。 Log4j自己会去找到属性文件并初始化。这样做又简单又有效。
你可以选择在Tomcat启动之前设置系统属性log4j.configuration 。对于 Tomcat 3.x ,
环境变量被用来设置命令行选项。对于 Tomcat 4.0,使用TOMCAT_OPTS
CATALINA_OPTS
环境变量而不是TOMCAT_OPTS 。
例子
Unix shell 命令
export TOMCAT_OPTS="-Dlog4j.configuration=foobar.txt"告诉log4j 使用文件
foobar.txt
作为默认的配置文件。这个文件应该被放置在你的网络应用程序的
WEB-INF/classes
目录下面。文件将通过 PropertyConfigurator被读取。每个网络应用程序使用不同的默认配置文件,因为每个文件都是和每个网络应用程序相关的。
例子
Unix shell 命令
export TOMCAT_OPTS="-Dlog4j.debug -Dlog4j.configuration=foobar.xml"告诉log4j输出log4j-内部排错信息,并使用文件
foobar.xml
作为默认的配置文件。这个文件应该被放置在你的网络应用程序的
WEB-INF/classes
目录下面。因为文件以.xml扩展符结尾,将使用 DOMConfigurator来读取。每个网络应用程序使用不同的默认配置文件,因为每个文件都是和每个网络应用程序相关的。
例子
Windows shell 命令
set TOMCAT_OPTS=-Dlog4j.configuration=foobar.lcf -Dlog4j.configuratorClass=com.foo.BarConfigurator告诉log4j使用文件
foobar.lcf
作为默认的配置文件。这个文件应该被放置在你的网络应用程序的
WEB-INF/classes
目录下面。根据
log4j.configuratorClass 系统属性的定义 ,文件将通过将使用客户自己的configurator——
com.foo.BarConfigurator
被读取。每个网络应用程序使用不同的默认配置文件,因为每个文件都是和一个网络应用程序相关的。
例子
Windows shell 命令
set TOMCAT_OPTS=-Dlog4j.configuration=file:/c:/foobar.lcf告诉log4j使用文件c:\foobar.lcf 作为默认的配置文件。这个配置文件完全由 URL
file:/c:/foobar.lcf
指定。因此,这个相同的配置文件将被所有网络应用程序使用。
c:\foobar.lcf
不同的网络应用程序通过它们各自的classloaders装载log4j的类。因此,每个 log4j环境的image会独自地,没有任何相互协调地行动。例如,在多个网络应用程序的配置中,FileAppenders
若定义得完全相同,它们就会编写相同的文件。这样的结果就不那么令人满意。你必须保证不同的网络应用程序的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.xml文件里为你的网络应用程序定义下面的servlet。
<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> |
编写一个initialization servlet 是最灵活的方式来初始化log4j。不受任何限制,你可以在这个servlet的init()
方法里放入任何代码。
Nested Diagnostic Contexts
实际情况下的大多数系统都需要同时处理多个客户端问题。在这种系统的典型的多线程实施中,通常是不同的线程去分别处理不同的客户需求。Logging特别适合于复杂的程序跟踪和排错。一个通常的处理办法是通过给每个客户产生一个新的分离开的logger来达到把不同的客户的日志输出信息区分开来。但这促进了loggers的增殖,加大了logging的管理负担。
一个更简洁的技术是独特地标记来自于同一个客户的每一个日志请求。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) 对这个方法进行了描述。 Pattern Languages of Program Design 3
要独特地标记每个日志请求,用户把上下文信息送入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类是作为一个保存线程上下文的stack来独个线程(per thread) 管理的。注意,org.apache.log4j.NDC
类中所有的方法都是静态的。假设NDC打印功能被打开,每一次若有日志请求,相应的log4j组件就把这个当前线程的整个 NDC stack包括在日志输出中打印出来。这样做不需要用户干预,用户只需要在代码中明确指定的几点通过push
和pop
方法将正确的信息放到NDC中就行了。相反,per-client logger方法需要在代码中作很多更改。
为了说明这一点,我们举个有关一个servlet把信息内容发送到多个客户的例子。这个Servlet程序在开始接到客户端的请求,执行其它代码之前,首先创建一个NDC。该上下文信息可能是客户端的主机名,以及其他请求中固有的信息,通常是包含在cookies中的信息。因此即便这个Servlet程序可能同时要服务于多个客户,由相同的代码启动的这些logs,比如属于同一个logger,它们仍然能够被区分开来,因为不同的客户端请求具有不同的NDC stack。这与在客户请求期间把一个实例化的logger传递给所有要被执行的代码的复杂性形成了反差。
然而,一些复杂的应用程序,比如虚拟网络服务器,必须依据虚拟主机的上下文语言环境,以及发布请求的软体组件来作不同的log。最近的log4j发行版支持多阶层树。这一功能的加强允许每个虚拟主机拥有它自己的logger阶层版本。
性能
一个经常提出的争议就是logging的运算开销。这种关注是有道理的,因为即便是一个中等大小的应用程序至少也会产生几千个log输出。许多工作都花费在测量和改进logging性能上。Log4j声明它是快速和灵活的:速度第一,灵活性第二。
用户需要清楚地了解下面这些与性能相关的问题:
- Logging performance when logging is turned off.
当logging被完全关闭或只是set of levels被关闭,日志请求的开销是方法的调用和整数的比较。在一个233 MHz Pentium II机器上,这种开销通常在5 to 50 毫微秒范围内。 set of levels
不过,方法的调用包含有参数的建造上的“隐闭”开销。
例如下面的logger
cat
程序段中:logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
不管message被日志记录与否,构造message参数的开销还是有的,比如说,把整数i 和数组entry[i]
转化为String,连接中间字串。参数构造的这种开销可能很高,它依赖于所介入的参数数量有多少。为了避免这种参数构造开销,把以上的代码段改写为:
if(logger.isDebugEnabled() { logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i])); }
如果排错功能不被使用,就不会有参数构造上的开销。但是,另一方面,如果 logger的排错功能被起用,就会有俩倍的开销用于评估logger是否被起用:一次是判断
,一次是判断debug是否被启用。但这不是极重的负担,因为评估logger的时间只有整个log语句执行时间的1%debug
Enabled在log4j中,把日志请求作为Logger类的实例。Logger是类而不是接口,这主要是为了减少程序调用的开销,但牺牲了接口所能带来的灵活性。
有些用户使用预处理或compile-time技术来编译所有log语句。这样logging方面的性能是很好。但是,因为resulting application binary没有包含任何log语句,你不能对这个二进制程序起用logging。在我看来,这是为了小的性能增加而付出大的代价。
- The performance of deciding whether to log or not to log when logging is turned on.
本质上影响性能的因素是logger的层次关系。当logging功能被打开时,log4j仍然需要把log请求的级别去与request logger的级别作比较。不过,有些loggers 并没有指派的优先级别,但它可以从它的上一层logger那里继承优先级别。因此在继承优先级之前,logger可能需要搜索它的ancestors。
Log4j在这方面做了很大的努力,以便使这种阶层的优先级别搜寻(hierarchy walk )尽可能的快速。例如,子代loggers仅仅只和它们现有的ancestors链接。在前面的
BasicConfigurator
示例中,叫做
的logger 直接与 root logger链接,绕过了不存在的com或com
.foo.Barcom.foo
loggers。这极大地提高了优先级别搜寻的速度。阶层的优先级搜寻(walking the hierarchy )的开销在于它比logging完全关闭时要慢三倍。
- Actually outputting log messages
这里讲的是log输出的格式化和把log信息发送到目标所在地的开销。Log4j在这方面也下了大力气让格式化能尽快执行。对appenders也是一样。通常情况下,格式化语句的开销可能是100到300微秒的处理时间。确切数字请参看 org.apache.log4.performance.Logging 。
尽管log4j具有许多功能特性,但速度是第一设计目标。为了提高性能,一些 log4j的部件曾经被重写过许多次。即使这样,log4j的贡献者们不断提出新的优化办法。你应该很惊喜地发现当以SimpleLayout来配置时,性能测试显示使用 log4j日志和使用System.out.println
日志同样快。
结论
Log4j是用Java编写的一个非常流行的logging开发包。它的一个显著特性之一是在loggers里运用了继承的概念。使用这种logger的层次关系,就可能准确地控制每一个log语句的输出。这样减少了log信息的输出量并降低了logging的开销。
Log4j API的优点之一是它的可管理性。一旦log语句被插入到代码中,他们就能被配置文件控制而无需重新编译源代码。Log信息的输出能够有选择地被起用或关闭,用户能够按照自己选择的格式将这些log信息输出到许多不同的输出设备中。Log4j软件包的设计是在代码中保留log语句的同时不造成很大的性能损失。