实践证明,日志打印是在开发中非常重要的功能模块。一旦将日志打印的语句写入代码中,日志的输出就不需要人工干预,还可以将日志持久化存储到本地文件中、数据库中、远程主机上,便于后续研究代码的执行逻辑。适用于 Java 日志打印框架有很多,比如 SLF4J、Logback、Log4j 等,这里介绍一下应用最广的 Log4j。
为什么使用 Log4j 2?
Log4j 1.x 从 1999 年发布至今得到了非常广泛的应用,但经过这些年的发展它有些慢了。而且,由于需要与旧版的 JDK 兼容而变得难以维护,也存在严重的安全漏洞问题,所以 Log4j 1.x 在 2015 年 8 月变为了 End of Life。
SLF4J/Logback 也在框架上做出了许多必要的改进,但为什么还去操心 Log4j 2 呢?理由如下:
- Log4j 2 是一个可以被用作日志审计的框架。 Log4j 1.x 和 Logback 在重新配置后会丢失事件,而 Log4j 2 不会。在 Logback 中,Appenders(输出源)中的异常对应用来说是不可见的,Log4j 2 中的 Appenders 可以配置为允许异常对应用可见;
- Log4j 2 包含基于 LMAX Disruptor library 的下一代异步 Loggers。在多线程场景下,异步 Loggers 拥有 10 倍于 Log4j 1.x 和 Logback 的生产力和低延迟量级;
- Log4j 2 对于独立应用来说是 garbage free 的,对于稳定输出日志 web 应用来说产生的垃圾也是很少的。这就减轻了垃圾回收器的压力从而在响应时间上有更好的性能;
- Log4j 2 的插件系统使得通过添加新的 Appenders、 Filters、 Layouts、 Lookups 和 Pattern Converters 来扩展该框架变得非常容易,Log4j 2 本身不需要任何改变;
- 由于插件系统的配置更加简单,配置项已不需要指定类名;
- 可以通过代码或配置文件进行配置自定义的日志级别;
- 支持 Lambda 表达式,基于 Java 8 的代码可以使用 Lambda 表达式简洁的创建日志的 Message;
- 支持 Message 对象 。 我们可以自由地创建有趣、复杂的 Message 类型,编写自定义的 Layouts、 Filters 和 Lookups 来操作这些 Message 类型;
- Log4j 1.x 支持在 Appenders 上使用 Filters,Logback 也添加了 TurboFilters 来在事件还未被 Logger 处理之前先过滤事件,Log4j 2 支持在事件被 Logger 处理之前配置 Filters 来处理事件,就像事件通过 Logger 处理或在 Appenders 上处理一样;
- 多数 Logback Appenders 不支持使用 Layout 而仅支持固定格式的数据。 多数 Log4j 2 Appenders 可以使用 Layout 来输出任意期望格式的数据;
- Log4j 1.x 中的 Layouts 以及 Logback 返回的是一个字符串,这导致了一些编码问题。而 Log4j 2 的 Layouts 总返回一个字节数组,这意味着该字节数组可以应用在任何 Appender 上,不仅仅是写入 OutputStream 中;
- Syslog Appender 支持 TCP 和 UDP 协议,也支持 BSD syslog 和 RFC 5424 格式;
- Log4j 2 应用了 Java 5 的并发支持和最低限度的加锁。Log4j 1.x 有一些死锁问题,这些问题多数在 Logback 得到了修复,但 Logback 类依然需要相当高级别的同步。
Log4j 2 架构详解
Log4j 2 的架构图如下:
LogManager 通过 getLogger(final Class clazz)
静态方法将定位到合适的 LoggerContext,然后从中得到一个 Logger 对象,要创建 Logger 需要关联一个 LoggerConfig,该 LoggerConfig 对象在 Configuration 中关联着传送 LogEvents 的 Appenders。
Logger Hierarchy
Log4j 1.x 中的 Logger 层级由各 Loggers 之间的关系来维护,而 Log4j 2 中的 Logger 层级由 LoggerConfig 对象负责维护。Logger 和 LoggerConfig 都是命名的实体。Logger 的名称是大小写敏感的,遵循以下层级命名规则:
如果 LoggerConfig A 的名称后跟一个点号作为后代 LoggerConfig A.*.B 的名称前缀(*表示任意中间前缀),那么 LoggerConfig A 是 LoggerConfig A.*.B 的 ancestor(An ancestor is a parent or the parent of an antecedent);如果名称 A 和 B 之间没有其他内容,那么 LoggerConfig A 是 LoggerConfig A.B 的 parent。
类似的, java
包是 java.util
包的 parent,是 java.util.Vector
类的 ancestor。这种命名模式对大多数开发者来说都是熟悉的。下表显示了这种层级关系。
LoggerConfig | ROOT | com | com.foo | com.foo.Bar |
---|---|---|---|---|
Root | X | Child | descendant | descendant |
com | Parent | X | Child | descendant |
com.foo | Ancestor | Parent | X | Child |
com.foo.Bar | Ancestor | Ancestor | Parent | X |
root LoggerConfig 位于 LoggerConfig 层级的最顶级,它存在于在每个层级中。直接链接到 root LoggerConfig 的 Logger 可以这样获得:
Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
也可以更简单些:
Logger logger = LogManager.getRootLogger();
其他 Logger 可以通过调用 LogManager.getLogger(final String name)
静态方法并传入期望的 Logger 名来获取。更多获取 Logger 的内容请参考 Log4j 2 API 。
LoggerContext
LoggerContext 充当着日志系统的锚点。不同情况下一个应用可以有多个有效的 LoggerContext。
Configuration
每个 LoggerContext 都有一个有效的 Configuration,该 Configuration 包含了所有的 Appenders、上下文范围的 Filters,以及 LoggerConfig, 并包含了 StrSubstitutor 的引用。在重新配置时会存在两个 Configuration。一旦所有的 Logger 重定向到新的 Configuration,旧的 Configuration 就会被停止和禁用。
Logger
Logger 本身执行无指向的动作,它仅含有一个与 LoggerConfig 关联的名称,继承了 AbstractLogger 并实现了其必需的方法,当配置被修改了,Loggers 可能转而关联不同的 LoggerConfig 从而会改变其自身的行为。如果调用 LogManager.getLogger
方法时使用相同的名称参数,则总会返回同一个 Logger 对象。例如:
Logger x = LogManager.getLogger("wombat");
Logger y = LogManager.getLogger("wombat");
x
和y
参照的实际上是同一个 Logger 对象。通过将 Logger 命名为其所在类的全限定名可以使输出的日志更具可辨识性,这是目前最好的做法。这不是强制的,开发者可以为 Logger 起任意的期望名称。既然一般习惯使用所在类的全限定名命名 Logger,所以 LogManager.getLogger()
方法默认创建使用所在类全限定名命名的 Logger。
LoggerConfig
当 Logger 在配置文件中声明时,就创建了 LoggerConfig 对象。LoggerConfig 包含一些 Filter,这些 Filter 用于过滤传递给任意 Appender 的 LogEvent。它还包含了一些 Appender 的引用,这些 Appender 用来处理事件。
Log Levels
LoggerConfig 会被分配一个日志级别。内建的日志级别按优先级从高到低排序有:OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL。Log4j 2 也支持自定义日志级别,另一种更细粒度化的机制是使用 Markers 来替代。
Log4j 1.x 和 Logback 都有一个日志级别继承的概念。Log4j 2 中,Logger 和 LoggerConfig 是两个不同的对象,所以这个概念也有所不同。每个 Logger 引用着一个合适的 LoggerConfig,该 LoggerConfig 又可以反过来继承该 Logger 的 parent LoggerConfig 的日志级别。
下面列出几张表演示了日志级别的继承逻辑。注意,如果 root LoggerConfig 没有配置,则它会被分配一个默认的日志级别(默认为 ERROR)。
在下面的示例中,只有 root
Logger 通过与其名称匹配的 LoggerConfig 配置一个日志级别,所有其他 Logger 将引用 root LoggerConfig,并使用其日志级别。
Logger Name | Assigned LoggerConfig | LoggerConfig Level | Logger Level |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | root | DEBUG | DEBUG |
X.Y | root | DEBUG | DEBUG |
X.Y.Z | root | DEBUG | DEBUG |
在下面的示例中,所有 Logger 都配置了与各自名称匹配的 LoggerConfig 并从中获取日志级别。
Logger Name | Assigned LoggerConfig | LoggerConfig Level | Level |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X.Y | INFO | INFO |
X.Y.Z | X.Y.Z | WARN | WARN |
在下面的示例中,名为 root
、X
和 X.Y.Z
的 Logger 都配置了各自的 LoggerConfig,但名为 X.Y
的 Logger 并没有配置名称匹配的 LoggerConfig,它将使用名为 X
的 LoggerConfig 。因为 X
LoggerConfig 的名称是 X.Y
Logger 的名称开头的最长匹配,如果还有名为 W.X.Y
的 LoggerConfig,X.Y
Logger 将会使用 W.X.Y
LoggerConfig。
Logger Name | Assigned LoggerConfig | LoggerConfig Level | Level |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X | ERROR | ERROR |
X.Y.Z | X.Y.Z | WARN | WARN |
在下面的示例中,root
和 X
Logger 都配置了与各自名称匹配的 LoggerConfig,X.Y
和 X.Y.Z
Logger 没有匹配 LoggerConfig,所以从分配给它们的 X
LoggerConfig 中获取其日志级别。
Logger Name | Assigned LoggerConfig | LoggerConfig Level | level |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X | ERROR | ERROR |
X.Y.Z | X | ERROR | ERROR |
在下面的示例中,root
,X
和 X.Y
Logger 都配置了与其各自名称匹配的 LoggerConfig,但 X.YZ
Logger 没有配置 LoggerConfig,所以从分配给它的 X
LoggerConfig 中获取其日志级别。由此可见,如果一个 Logger 没有配置 LoggerConfig,那么它将会继承使用上一级 LoggerConfig。
Logger Name | Assigned LoggerConfig | LoggerConfig Level | level |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X.Y | INFO | INFO |
X.YZ | X | ERROR | ERROR |
在下面的示例中,X.Y
Logger 配置了与其名称匹配的 X.Y
LoggerConfig,但 X.Y
LoggerConfig 没有配置日志级别,所以,X.Y
LoggerConfig 从 X
LoggerConfig 获取其日志级别。X.Y.Z
Logger 没有配置与其名称匹配的 LoggerConfig,所以,X.Y.Z
Logger 将使用 X.Y
LoggerConfig,从而其级别也从 X
LoggerConfig 获得。如果一个 Logger 的 LoggerConfig 没有配置日志级别,那么该 LoggerConfig 将会继承使用上一级 LoggerConfig 的日志级别 。
Logger Name | Assigned LoggerConfig | LoggerConfig Level | Level |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X.Y | ERROR | |
X.Y.Z | X.Y | ERROR |
StrSubstitutor 和 StrLookup
StrSubstitutor 类和 StrLookup 接口是从 Apache Commons Lang 借鉴修改而来用以处理 LogEvents 的。另外,Interpolator 类是从 Apache Commons Configuration 借鉴修改而来从而使 StrSubstitutor 可以处理多个 StrLookups 中的变量,该类也经过修改可以支持处理 LogEvents。这些类一起让配置可以引用 System Properties、配置文件、ThreadContext Map 以及 LogEvent 的 StructuredData 中的变量。