2015 年 8 月 5 日,Logging 服务项目管理委员会宣布 Log4j 1.x 已终止生命周期。
公告全文请参阅
Apache 博客 。建议 Log4j 1 用户升级至 Apache Log4j 2。
版权所有 © 2000-2002 阿帕奇软件基金会。保留所有权利。本软件根据阿帕奇软件许可证 2.0 版条款发布,该许可证副本已随 log4j 发行包附带的 LICENSE 文件中包含。本文档基于 2000 年 11 月期《JavaWorld》杂志发表的 "Log4j 实现日志记录控制" 一文编写。但当前文章包含更详细和最新的信息。本简短手册还引用了同一作者(即本人)所著 "log4j 完全手册 " 中的部分文字。
本文档介绍了 log4j API 的独特功能与设计原理。log4j 是一个基于众多开发者贡献的开源项目,它允许开发人员以任意粒度控制日志语句的输出,并支持通过外部配置文件在运行时进行完全配置。最重要的是,log4j 学习曲线平缓。但请注意:根据用户反馈,它还具有令人着迷的特性。
几乎每个大型应用程序都包含自己的日志记录或跟踪 API。遵循这一惯例,欧盟 SEMPER 项目决定开发自己的跟踪 API。这发生在 1996 年初。经过无数次改进、多个版本迭代和大量工作后,该 API 最终演变为 log4j——一个流行的 Java 日志记录工具包。该软件包基于 Apache 软件许可证分发,这是一个经开源倡议认证的完整开源协议。最新版 log4j(包含完整源代码、类文件和文档)可在 http://logging.apache.org/log4j/ 获取。值得一提的是,log4j 已被移植到 C、C++、C#、Perl、Python、Ruby 和 Eiffel 等语言。
在代码中插入日志语句是一种低技术含量的调试方法。这也可能是唯一可行的方法,因为调试器并非总是可用或适用。这种情况通常出现在多线程应用和大规模分布式应用中。
经验表明,日志记录是开发周期中的重要组成部分。
它具有多项优势。它能提供应用程序运行的精确
上下文信息。一旦将日志代码插入程序中,生成日志输出就无需人工干预。此外,日志输出可以保存在持久化介质中供后续分析。除了在开发周期中使用外,功能足够丰富的日志记录包还可以被视为审计工具。
正如 Brian W. Kernighan 和 Rob Pike 在他们极好的著作《程序设计实践》中所言
As personal choice, we tend not to use debuggers beyond getting a stack trace or the value of a variable or two. One reason is that it is easy to get lost in details of complicated data structures and control flow; we find stepping through a program less productive than thinking harder and adding output statements and self-checking code at critical places. Clicking over statements takes longer than scanning the output of judiciously-placed displays. It takes less time to decide where to put print statements than to single-step to the critical section of code, even assuming we know where that is. More important, debugging statements stay with the program; debugging sessions are transient.
日志记录确实有其缺点。它可能拖慢应用程序速度。如果过于冗长,会导致滚动盲视。为了缓解这些问题,log4j 被设计为可靠、快速且可扩展的。由于日志记录很少是应用程序的主要关注点,log4j API 力求简单易懂、易于使用。
Log4j 包含三个主要组件:日志记录器、
附加器和布局器 。这三种组件协同工作,使开发者能够根据消息类型和级别进行日志记录,并在运行时控制这些消息的格式化方式及其输出位置。
任何日志 API 相较于普通日志的首要优势
System.out.println 在于其能够禁用特定日志语句,同时允许其他日志不受阻碍地输出。这一功能的前提是:整个日志空间(即所有可能的日志语句集合)能够按照开发者设定的标准进行分类。这一特性曾促使我们选择分类作为该包的核心概念。但从 log4j 1.2 版本开始, Logger 类已取代 Category 类。对于熟悉 log4j 早期版本的用户, Logger 类可视为 Category 类的别名。
日志记录器是具名实体。其名称区分大小写,并遵循层次化命名规则:
|
例如,名为 "com.foo" 的日志记录器是名为 "com.foo.Bar" 的日志记录器的父级。同样地,
"java" 是 "java.util" 的父级,也是 "java.util.Vector" 的祖先级。这种命名方案对大多数开发者来说应该很熟悉。
根日志记录器位于日志记录器层次结构的顶端。它在两个方面具有特殊性:
调用类的静态方法 Logger.getRootLogger
可以获取它。所有其他日志记录器都是通过类的静态方法
Logger.getLogger 实例化并获取的
该方法以所需记录器的名称作为
参数。Logger 类中的一些基本方法列在
下方。
package org.apache.log4j; public class Logger { // Creation & retrieval methods: public static Logger getRootLogger(); public static Logger getLogger(String name); // printing methods: public void trace(Object message); 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); } |
日志记录器可以被分配级别。可能的级别集合包括:
TRACE,
DEBUG,
INFO,
警告 ,
错误以及
致命错误
这些级别定义在 org.apache.log4j.Level 类中。虽然我们不鼓励这样做,但您可以通过继承 Level 类来定义自己的级别。稍后将解释可能更好的方法。
如果某个记录器未被分配级别,则它会继承其最近被分配级别的祖先的级别。更正式地说:
|
为确保所有记录器最终都能继承一个级别,根记录器始终具有一个已分配的级别。
以下是四张表格,展示了根据上述规则分配的不同级别值及其对应的继承级别结果。
示例 1
日志记录器 名称 | 已分配 级别 |
继承 级别 |
---|---|---|
根记录器 | 根 | Proot |
X | 无 | Proot |
X.Y | 无 | Proot |
X.Y.Z | 无 | Proot |
在上面的示例1中,仅根记录器被分配了级别。这个级别值 Proot 会被其他记录器 X 、 X.Y 继承
X.Y.Z .
示例2
日志记录器 名称 | 已分配 级别 |
继承 级别 |
---|---|---|
根记录器 | 根 | Proot |
X | 像素 | 像素 |
X.Y | 像素 | 像素 |
X.Y.Z | 像素 xyz | Pxyz |
在示例2中,所有记录器都分配了级别值,因此无需进行级别继承。
日志记录器 名称 | 已分配 级别 |
继承 级别 |
---|---|---|
根记录器 | 根 | Proot |
X | 像素 | 像素 |
X.Y | 无 | 像素 |
X.Y.Z | 像素 xyz | Pxyz |
在示例3中,日志记录器 root 、 X 和
X.Y.Z 分别被分配了级别 Proot 、
Px 和 Pxyz 。日志记录器
X.Y 从其父级继承了级别值
X .
日志记录器 名称 | 已分配 级别 |
继承 级别 |
---|---|---|
根记录器 | 根 | Proot |
X | 像素 | 像素 |
X.Y | 无 | 像素 |
X.Y.Z | 无 | 像素 |
在示例4中,日志记录器 root 和 X
被分别分配了级别 Proot 和 Px
分别是。记录器 X.Y 和 X.Y.Z
继承自它们最近的父级 X
所设置的级别值。
日志记录请求通过调用打印方法之一来发起
日志记录器实例的打印方法。这些打印方法
debug,
info,
warn,
error,
fatal
and log .
根据定义,打印方法决定了日志请求的级别。例如,如果 c 是一个日志记录器实例,那么语句 c.info("..") 就是一个 INFO 级别的日志请求。
如果日志请求的级别高于或等于其日志记录器的级别,则该请求被称为启用 。否则,该请求被称为禁用 。未分配级别的日志记录器将从层次结构中继承一个级别。这一规则总结如下。
|
该规则是 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"); |
因此,可以先配置一个日志记录器,然后在代码的其他位置检索同一实例,而无需传递引用。与生物学上的父子关系(父母总是先于子女存在)根本不同的是,log4j 日志记录器可以按任意顺序创建和配置。特别是,"父级"日志记录器即使在其子级之后实例化,也能找到并链接到它们。
log4j 环境的配置通常在应用程序初始化时完成。首选方式是通过读取配置文件,这种方法稍后将进行讨论。
Log4j 通过软件组件命名日志记录器非常简单。可以在每个类中静态实例化一个日志记录器,并将记录器名称设置为该类的完全限定名。这是定义日志记录器的一种实用且直接的方法。由于日志输出会携带生成记录器的名称,这种命名策略可以轻松识别日志消息的来源。然而,这只是命名日志记录器的一种可能策略(尽管很常见)。Log4j 并不限制记录器的命名方式,开发者可以自由地按需命名日志记录器。
尽管如此,以所在类命名日志记录器似乎是目前已知的最佳策略。
根据日志记录器选择性启用或禁用日志请求的能力只是其中一部分。Log4j 允许将日志请求输出到多个目标。在 log4j 术语中,输出目标被称为附加器 。目前支持的附加器包括控制台 、 文件 、GUI 组件、 远程套接字
服务器、JMS、
NT 事件日志记录器 ,以及远程 UNIX 系统日志
守护进程。还可以实现异步日志记录。
一个记录器可以附加多个追加器。
addAppender
方法将追加器添加到指定的记录器。
对于给定记录器的每个启用的日志记录请求,都将转发到该记录器中的所有追加器以及层次结构中更高层级的追加器。
换句话说,附加器是从记录器层次结构中叠加继承的
例如,如果向根记录器添加了控制台附加器
那么所有启用的日志请求至少会在控制台打印
如果再向某个记录器添加文件附加器
C,然后启用 C 的日志记录请求
C 的子级日志将同时输出到文件和控制台。通过将可加性标志设置为 false ,可以覆盖此默认行为,使追加器不再具有累积性。
控制追加器可加性的规则总结如下。
|
下表展示了一个示例:
日志记录器 名称 | 已添加 附加器 | 可叠加性 标志 | 输出目标 | 注释 |
---|---|---|---|---|
根记录器 | A1 | 不适用 | A1 | 根记录器是匿名的,但可以通过 Logger.getRootLogger()方法访问。根记录器默认没有附加任何输出源。 |
x | A-x1, A-x2 | 真 | A1, A-x1, A-x2 | "x"的附加器和根记录器 |
x.y | 无 | 真 | A1, A-x1, A-x2 | "x"的附加器和根记录器 |
x.y.z | A-xyz1 | true | A1、A-x1、A-x2、A-xyz1 |
"x.y.z"、"x"及根记录器中的追加器 |
安全 | A-sec | false | A-sec | 由于可加性标志被设置,无附加器累积 false . |
安全访问 | 无 | 真 | A 级安全 | 仅限“security”的附加器,因为“security”中的可加性标志设置为 false 。 |
用户通常不仅希望自定义输出目的地,还希望自定义输出格式。这通过将布局与附加器关联来实现。布局负责根据用户需求格式化日志请求,而附加器则负责将格式化后的输出发送到目标位置。
例如,使用转换模式"%r [%t] %-5p %c - %m%n"的 PatternLayout 将输出类似于:
176 [main] INFO org.foo.Bar - Located nearest gas station.
第一个字段是程序启动后经过的毫秒数。第二个字段是发出日志请求的线程。第三个字段是日志语句的级别。第四个字段是与日志请求关联的记录器名称。短横线'-'后的文本是该语句的具体消息内容。
同样重要的是,log4j 会根据用户指定的标准来呈现日志消息的内容。例如,如果您经常需要记录 Oranges (一种用于
您当前的项目,那么您可以注册一个
每当需要记录橙色信息时就会被调用的方法。
对象渲染遵循类层次结构。例如,假设橙子是水果,如果你注册了一个 FruitRenderer ,那么所有
包括橙子在内的水果都将由
FruitRenderer 渲染,除非你注册了特定于橙子的 OrangeRenderer 。
对象渲染器需要实现
ObjectRenderer
接口。
将日志请求插入应用程序代码需要相当多的规划与努力。观察表明,约4%的代码专用于日志记录。因此,即使是中等规模的应用程序,其代码中也会嵌入数千条日志语句。鉴于其数量之多,必须能够管理这些日志语句而无需手动修改它们。
log4j 环境完全可以通过编程方式配置。然而,使用配置文件配置 log4j 要灵活得多。目前,配置文件可以用 XML 或 Java 属性(键=值)格式编写。
让我们通过一个虚构的应用程序 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."); } } |
MyApp 首先导入与 log4j 相关的类。它
随后定义了一个名为 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。输出将使用设置为模式"%-4r [%t] %-5p %c %x - %m%n"的 PatternLayout 进行格式化。
请注意,默认情况下根记录器被分配给
Level.DEBUG .
MyApp 的输出结果为:
0 [main] INFO MyApp - Entering application. 36 [main] DEBUG com.foo.Bar - Did it again! 51 [main] INFO MyApp - Exiting application.
下图展示了 MyApp 的对象关系图
刚刚调用了 BasicConfigurator.configure 之后
方法。
顺便提一下,在 log4j 中子记录器仅链接
到其现有的祖先记录器。具体来说,名为
com.foo.Bar 直接链接到 root
记录器,从而绕过了未使用的 com 或
com.foo 记录器。这显著提高了性能并减少了 log4j 的内存占用。
MyApp 类通过调用方法来配置 log4j
BasicConfigurator.configure 方法。其他类只需导入 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 继承其级别,
在配置文件中设置为 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.
此外,由于根记录器被分配了第二个附加器,输出内容也将被定向到 example.log
文件中。当文件大小达到 100KB 时,该文件将进行滚动更新。当
当发生滚动更新时,旧版本的 example.log 会自动移至 example.log.1 。
请注意,要实现这些不同的日志记录行为,我们无需重新编译代码。同样可以轻松地将日志记录到 UNIX 系统日志守护进程、将所有 com.foo 输出重定向到 NT 事件日志记录器,或将日志事件转发到远程 log4j 服务器——后者会根据本地服务器策略进行记录,例如将日志事件转发到第二个 log4j 服务器。
log4j 库不会对其运行环境做任何预设。特别是,log4j 没有默认的附加器。但在某些明确定义的情况下, Logger 类的静态初始化器会尝试自动配置 log4j。Java 语言保证类的静态初始化器在类加载到内存时只会被调用一次。需要特别注意的是,不同的类加载器可能会加载同一个类的不同副本,这些副本会被 JVM 视为完全无关的类。
默认初始化在应用程序入口点取决于运行时环境的情况下非常有用。例如,同一个应用程序可以作为独立应用程序、小程序或由 Web 服务器控制的 servlet 使用。
确切的默认初始化算法定义如下:
请参阅 Loader.getResource(java.lang.String)
用于搜索位置列表。
PropertyConfigurator
将用于解析 URL 以配置 log4j,除非 URL 以".xml"扩展名结尾,
此时将使用 DOMConfigurator
将使用默认配置。您可以选择指定一个自定义配置器。
系统属性 log4j.configuratorClass 的值将被视为您自定义配置器的完全限定类名。您指定的自定义配置器必须实现 Configurator
接口。
默认的 log4j 初始化在 Web 服务器环境中特别有用。在 Tomcat 3.x 和 4.x 下,您应该将 log4j.properties 放置在
WEB-INF/classes 您 Web 应用程序的目录下。Log4j 会找到该属性文件并自行初始化。这种方法简单易行且有效。
您也可以选择设置系统属性
在启动 Tomcat 之前设置 log4j.configuration。对于 Tomcat 3.x 版本
使用 TOMCAT_OPTS 环境变量来设置命令行选项。对于 Tomcat 4.0,应设置 CATALINA_OPTS
环境变量而非 TOMCAT_OPTS 。
示例 1
Unix shell 命令
export TOMCAT_OPTS="-Dlog4j.configuration=foobar.txt"
示例2
Unix shell 命令
export TOMCAT_OPTS="-Dlog4j.debug -Dlog4j.configuration=foobar.xml"
示例3
Windows shell 命令
set TOMCAT_OPTS=-Dlog4j.configuration=foobar.lcf -Dlog4j.configuratorClass=com.foo.BarConfigurator
示例4
Windows shell 命令
set TOMCAT_OPTS=-Dlog4j.configuration=file:/c:/foobar.lcf
不同的 Web 应用程序将通过各自的类加载器加载 log4j 类。因此,每个 log4j 环境的实例将独立运行且无需相互同步。例如,在多个 Web 应用程序配置中以完全相同方式定义的 FileAppenders 都会尝试写入同一个文件。结果很可能不尽如人意。您必须确保不同 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> <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() 方法中可以放置任何代码,没有任何限制。
大多数现实世界的系统需要同时处理多个客户端。在这种系统的典型多线程实现中,不同的线程会处理不同的客户端。日志记录特别适合跟踪和调试复杂的分布式应用程序。区分不同客户端日志输出的常见方法是为每个客户端实例化一个新的独立记录器。这会促使记录器激增,并增加日志记录的管理开销。
一种更轻量级的技术是为来自同一客户端交互的每个日志请求打上唯一标记。Neil Harrison 在《记录诊断消息的模式》一书中描述了这种方法,该书收录于 R. Martin、D. Riehle 和 F. Buschmann 编辑的 《程序设计模式语言 3》(Addison-Wesley,1997 年出版)。
为了给每个请求打上唯一标记,
用户将上下文信息推入 NDC,即
嵌套诊断上下文的缩写。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 中)。因此,即使 servlet 同时为多个客户端提供服务,由相同代码(即属于同一记录器)发起的日志仍然可以区分,因为每个客户端请求将拥有不同的 NDC 堆栈。这与在客户端请求期间向所有执行代码传递新实例化记录器的复杂性形成鲜明对比。
然而,一些复杂的应用程序(如虚拟主机网络服务器)需要根据虚拟主机上下文以及发出请求的软件组件进行不同的日志记录。最近的 log4j 版本支持多层级树结构,这一增强功能使每个虚拟主机都能拥有自己的日志记录器层次结构副本。
反对日志记录的一个常见理由是它的计算成本。这确实是个合理的问题,因为即使是中等规模的应用程序也可能产生数千条日志请求。人们花费了大量精力来测量和调整日志记录的性能。Log4j 声称既快速又灵活:速度优先,灵活性其次。
用户应当注意以下性能问题。
当日志记录被完全关闭或仅针对一组级别关闭时,日志请求的开销仅包含一次方法调用和一个整数比较。在 233 MHz 的奔腾 II 机器上,这一开销通常在 5 到 50 纳秒范围内。
然而,方法调用还涉及参数构造的"隐性"成本。
例如,对于某个记录器 cat 来说,写入时
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
为避免参数构造的开销,可以这样写:
if(logger.isDebugEnabled() { logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i])); }
如果调试功能被禁用,这种方式不会产生参数构造的成本。反之,若日志器启用了调试模式,则会双倍消耗判断日志器是否启用的开销:一次在 debugEnabled 处,另一次在
debug 处。这个额外开销微不足道,因为判断日志器状态的时间仅占实际记录日志所需时间的1%左右。
在 log4j 中,日志记录请求是通过 Logger 类的实例发出的。Logger 是一个类而非接口,这种设计显著降低了方法调用的成本,但牺牲了部分灵活性。
部分用户采用预处理或编译时技术来编译掉所有日志语句。这能实现日志记录方面的完美性能效率。但由于生成的应用程序二进制文件中不包含任何日志语句,因此无法为该二进制文件开启日志功能。在我看来,为换取微小的性能提升而付出这种代价是不成比例的。
这本质上涉及遍历日志记录器层次结构的性能。当日志功能开启时,log4j 仍需将日志请求的级别与请求记录器的级别进行比较。然而,记录器可能没有分配级别,它们可以从日志记录器层次结构中继承级别。因此,在继承级别之前,记录器可能需要搜索其祖先记录器。
已付出重大努力使这种层次结构遍历尽可能快速。例如,子记录器仅链接到其现有的祖先记录器。在 BasicConfigurator
如前面示例所示,名为 com.foo.Bar 的记录器直接链接到根记录器,从而绕过了不存在的 com 或 com.foo 记录器。这显著提高了遍历速度,尤其是在"稀疏"层级结构中。
遍历层级的典型开销通常比完全关闭日志记录慢3倍。
这是格式化日志输出并将其发送到目标位置的开销。同样地,我们付出了巨大努力使布局(格式化器)尽可能高效运行。对于附加器也是如此。实际记录日志的典型开销约为100到300微秒。
尽管 log4j 功能丰富,但其首要设计目标是速度。部分 log4j 组件经过多次重写以提升性能。然而,贡献者们仍不断提出新的优化方案。值得欣慰的是,当配置使用 SimpleLayout 时,
性能测试表明 log4j 的日志记录速度堪比
System.out.println .