架构
主要组件
Log4j使用如下图所示的类:
应用程序要使用Log4j 2的API,需要从LogManager中获取一个有明确名称的Logger。LogManager将会定位到一个合适的LoggerContext并且从中获取Logger。如果Logger必须被创建,那么它会和包含这些信息的LogConfig相关联:a)与Logger相同的名称;b)父包的名称;c)根LoggerConfig。LoggerConfig对象根据配置中的Logger声明而创建。LoggerConfig与实际处理LogEvent事件的Appender关联。
日志层级
使用所有基于System.out.println
的日志API的首要条件,在于当允许其他无障碍打印时禁用明确的日志声明的能力。这项能力要求日志空间,也就是包含所有可能日志声明的空间,是根据某些开发者选择的规范而组织起来的。
在Log4j 1.x中,层级是由Logger之间的关系维护的。在Log4j 2中,这种关系不再存在。取而代之地,层级由LoggerConfig对象之间的关系维护。
Logger和LoggerConfig都是命名的实体。Logger的名称大小写敏感,且遵循层级命名规则:
命名层级
如果一个LoggerConfig的名称加上句点符号被作为前缀,那它就是另一个 后代 LoggerConfig的 祖先 。如果在它自身和后代LoggerConfig之间没有其他的祖先,那么前者就是 父 LoggerConfig,后者为 子 LoggerConfig。
例如,被命名为com.foo
的LoggerConfig是被命名为com.foo.Bar
的LoggerConfig的父。类似地,java
也是java.util
和java.util.Vector
的父。这种命名模式应该对很多开发者来说都很熟悉。
根LoggerConfig位于LoggerConfig层级的顶层。这意味着它总是存在的,并且是每一个层级的一部分。一个Logger可以通过下面的方式直接导向根LoggerConfig:
Logger logger = LogManager.getLogger(LogManager.ROOT_LOGGER_NAME);
或者更简单地:
Logger logger = LogManager.getRootLogger();
所有其他的Logger都可以通过静态方法LogManager.getLogger传入对应的名称而获取。更多关于日志API的信息可以参考Log4j 2 API。
LoggerContext
LoggerContext就像是日至系统中的一个锚点。然而视情况,一个应用程序中可能有多个活跃的LoggerContext。更多关于LoggerContext的细节在日志分离一节中。
Configuration
每一个LoggerContext都有一个活跃的Configuration。这个Configuration包含所有的Appender、上下文范围的Filter、LoggerConfig以及StrSubstitutor的引用。当重新配置时,将会有两个Configuration对象存在。一旦所有的Logger都已经被导向到新的Configuration上时,旧的Configuration就会被停止且丢弃。
Logger
此前的状态,Logger是由调用LogManager.getLogger方法被创建的。Logger本身并不做转向的动作。它只是有一个名字并且和LoggerConfig关联。它继承了AbstractLogger并且实现了所需的方法。当配置被修改时,Logger可能变为与一个不同的LoggerConfig关联,这样会导致他们的表现被改变。
获取Logger
用相同的名称调用LogManager.getLogger
方法总是会得到同一个完全相同的Logger对象。例如:
Logger x = LogManager.getLogger("wombat");
Logger y = LogManager.getLogger("wombat");
x
和y
是完全相同的Logger对象。
Log4j环境的配置是在应用初始化时一次性完成的。一种更倾向使用的方式是读取配置文件。这将在配置中讨论。
Log4j可以很容易地通过 软件组件 来命名Logger。这可以通过在每一个类中用于类名完全一样的名称来定义一个Logger来达到。这是定义Logger简单实用的方法。由于日志输出中承载了生成Logger的名称,这种命名策略使得在日志信息定位源头很简单。然而,这只是一种可能的、惯用的Logger命名策略。Log4j并不限制Logger的可能性。开发者可以按想要的方式自由命名Logger。
由于根据当前类来命名Logger特别常用,所以提供一个简便方法LogManager.getLogger()
来自动使用当前类的完整名称以命名Logger。
尽管如此,通过当前类来命名Logger看上去都是一个目前最好的策略。
LoggerConfig
LoggerConfig对象在日志配置中声明Logger时被创建。LoggerConfig包含一个Filter集合,以允许LogEvent在到达任何Appender之前被传递。它包含一个Appender集合的引用,用于处理事件。
日志级别
LoggerConfig会分配一个日志级别。内建的级别包含TRACE、DEBUG、INFO、WARN、ERROR和FATAL。Log4j 2也支持自定义日志级别。另一个获取更细粒度的机制是用Markers作为替代。
Log4j 1.x和Logback都有“级别继承”的概念。在Log4j 2中,Logger和LoggerConfig是两个不同的对象,所以这个概念是用不同方式实现的。每一个Logger都引用一个合适的、可间接引用其父对象的LoggerConfig,这样以达到相同的效果。
下面六张表格展示了许多分配的级别取值与每一个Logger被分配的结果级别。注意在这些举例中,如果根LoggerConfig没有被配置,一个默认的级别将会被分配。
示例1:
Logger名称 | 分配的LoggerConfig | LoggerConfig级别 | Logger级别 |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | root | DEBUG | DEBUG |
X.Y | root | DEBUG | DEBUG |
X.Y.Z | root | DEBUG | DEBUG |
在上面的示例1中,只有根logger被配置且有日志级别。所有其他的Logger都引用根LoggerConfig且使用它的级别。
示例2:
Logger名称 | 分配的LoggerConfig | LoggerConfig级别 | Logger级别 |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X.Y | INFO | INFO |
X.Y.Z | X.Y.Z | WARN | WARN |
在示例2中,所有的logger都配置了一个LoggerConfig,并从中获取级别。
示例3:
Logger名称 | 分配的LoggerConfig | LoggerConfig级别 | Logger级别 |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X | ERROR | ERROR |
X.Y.Z | X.Y.Z | WARN | WARN |
在示例3中,logger root
、X
和X.Y.Z
都配置了相同名称的LoggerConfig。Logger X.Y
没有配置匹配名称的LoggerConfig,因而使用X
的LogConfig,因为那是最长前缀匹配的LoggerConfig。
示例4:
Logger名称 | 分配的LoggerConfig | LoggerConfig级别 | Logger级别 |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X | ERROR | ERROR |
X.Y.Z. | X | ERROR | ERROR |
在示例4中,logger root
和X
都配置了相同名称的LoggerConfig。而X.Y
和X.Y.Z
都没有配置LoggerConfig,所以它们的日志级别都将从X
中获取,因为那是最长前缀匹配的LoggerConfig。
示例5:
Logger名称 | 分配的LoggerConfig | LoggerConfig级别 | Logger级别 |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X.Y | INFO | INFO |
X.YZ | X | ERROR | ERROR |
在示例5中,logger root
、X
和X.Y
都配置了相同名称的LogConfig。但X.YZ
没有配置,所以它的日志级别将从X
中获取,因为那是最长前缀匹配的LoggerConfig。它并不匹配X.Y
,因为匹配的段非常严格。
示例6:
Logger名称 | 分配的LoggerConfig | LoggerConfig级别 | Logger级别 |
---|---|---|---|
root | root | DEBUG | DEBUG |
X | X | ERROR | ERROR |
X.Y | X.Y | ERROR | |
X.Y.Z | X.Y | ERROR |
在示例6中,LoggerConfig X.Y
没有配置级别,所以将从X
中获取级别。而Logger X.Y.Z
使用LoggerConfig X.Y
,因为它没有配置名称完全匹配的LoggerConfig。继而也是从LoggerConfig X
中继承日志级别。
下面的表格展示了级别过滤是如何工作的。在表格中,垂直列为LogEvent的级别,水平列为从合适的LoggerConfig中分配到的级别。二者的交点处标识了LogEvent是否会被通过并传递给下一步处理,是(YES)或否(NO)。
事件级别 | TRACE | DEBUG | INFO | WARN | ERROR | FATAL | OFF |
---|---|---|---|---|---|---|---|
ALL | YES | YES | YES | YES | YES | YES | YES |
TRACE | YES | NO | NO | NO | NO | NO | NO |
DEBUG | YES | YES | NO | NO | NO | NO | NO |
INFO | YES | YES | YES | NO | NO | NO | NO |
WARN | YES | YES | YES | YES | NO | NO | NO |
ERROR | YES | YES | YES | YES | YES | NO | NO |
FATAL | YES | YES | YES | YES | YES | YES | NO |
OFF | NO | NO | NO | NO | NO | NO | NO |
Filter
作为上一节中对自动日志级别过滤的补充,Log4j提供Filter并可应用于:控制被传递到任何LoggerConfig之前、控制被传递到达一个LoggerConfig但在调用任何Appender之前、控制被传递到一个LoggerConfig单在调用一个指定的Appender和每一个Appender之前。与防火墙过滤的方式类似,每一个Filter都将返回三个结果之一:Accept
(接受)、Deny
(拒绝)或Neutral
(中立)。响应Accept
意味着其他的Filter都不应该再被调用,而事件应该被处理。响应Deny
意味着事件应该被立即忽略,且将控制讲给调用处。响应Neutral
代表事件应该被传递给其他的Filter。如果没有其他Filter,则事件将被处理。
尽管一个事件可能被Filter接受,但事件仍然可能不被记录。这种情况会发生于事件被LoggerConfig之前的Filter接受,但被LoggerConfig的Filter拒绝或者被所有的Appender拒绝。
Appender
基于Logger可选地启用或禁用日志请求只是一部分而已。Log4j允许将日志请求打印到多个目标。用Log4j的说法,一个输出的目标位置被称为Appender。目前,Appender存在几种:控制台、文件、远程Socket服务器、Apache Flume、JMS、远程UNIX系统日志后台以及好几种数据库API。请查看Appender获取更多关于多种可用类型的细节。一个Logger上可以装配多个Appender。
调用当前配置的addLoggerAppender方法可以将一个Appender添加到Logger上。如果名称匹配的LoggerConfig不存在,将会创建一个,Appender将会被装配上去,然后所有的Logger都将被通知去更新他们的LoggerConfig引用。
在一个给定的Logger上,所有启用的日志请求都将被传递到该Logger对应的LoggerConfig上所有的Appender中,也同样作用于它的所有父LoggerConfig上的Appender。换句话说,Appender是根据LoggerConfig的层级继承的。举个例子,如果一个控制台Appender被添加到根Logger上,那么所有启用的日志请求都至少会被打印到控制台。如果一个文件Appender被添加到名为 C 的LoggerConfig上,那么与 C 和 C 的所有孩子对应的启用的日志请求,都将被打印到文件以及控制台中。可以覆盖这种默认的表现,只要在配置文件中声明Logger时设置additivity="false"
,就可以让Appender不再自动累加。
Appender累加的治理规则总结如下:
Apender累加
日志声明的Logger L 的输出,将会到达与 L 相关联的LoggerConfig上的所有Appender,以及所有该LoggerConfig的祖先。这就是“Appender累加”的意思。
然而,如果一个与Logger L 相关联的名为 P 的祖先LoggerConfig,已经把累加标识设置为
false
,那么 L 的输出将会被导向到所有 L 以及所有低于 P (包含 P )层级的多个祖先对应的LoggerConfig上的Appender,但不会到达 P 的多个祖先。Logger的累加标识是默认设置为
true
的。
下面的表格展示了一个例子:
Logger名称 | 添加的Appender | 累加标识 | 输出目标 | 注释 |
---|---|---|---|---|
root | A1 | 不可用 | A1 | 根Logger没有父亲,所以累加标识对它不可用。 |
x | A-x1, A-x2 | true | A1, A-x1, A-x2 | “x”和root的Appender。 |
x.y | none | true | A1, A-x1, A-x2 | “x”和root的Appender。配置一个没有Appender的Logger是非典型的做法。 |
x.y.z | A-xyz1 | true | A1, A-x1, A-x2, A-xyz1 | “x.y.z”、“x”和root的Appender。 |
security | A-sec | false | A-sec | 没有Appender被累加,因为累加标识被设置为了false 。 |
security.access | none | true | A-sec | 只有“security”的Appender,因为“security”的累加标识被设置为false 。 |
Layout
屡见不鲜,用户不仅希望自定义输出的目的位置,也希望自定义输出格式。这可以通过将一个Layout与Appender关联来实现。Layout负责根据用户的希望来格式化LogEvent,然而是Appender负责将格式化的内容输出到目的位置。PatternLayout,Log4j中的一部分,让用户根据C语言printf
函数的方式来具体化输出格式。
例如,使用转换模式“%r [%t] %-5p %c - %m%n”的PatternLayout将会输出类似于下面的内容:
176 [main] INFO org.foo.Bar - Located nearest gas station.
第一个字段是程序启动以来锁经过的毫秒时间。第二个字段是发出日志请求的线程。第三个字段是日志声明的级别。第四个字段是与日志请求相关联的Logger名称。在“-”之后的文本是日志的消息内容。
Log4j带有很多不同的Layout以支持诸如JSON、XML、HTML和Syslog(包括新的RFC 5424版本)等用例。其他如数据库连接器的Appender等,由一些具体指定的字段填充,以替代特殊的文本布局。
重要的是,Log4j会让日志文本符合用户具体的规范。例如,如果你经常需要记录Oranges
(一个你当前项目中使用的对象类型),那么你可以创建一个OrangeMessage来接受一个Orange实例并将它传递给Log4j,这样Orange对象将在需要时被格式化为一个合适的byte数组。
StrSubstitutor与StrLookup
StrSubstitutor类和StrLookup接口是从Apache Commons Lang中借过来并修改以支持LogEvent变量的。添加的Interpolator类从Apache Commons Confuguration中借过来以允许StrSubstitutor支持多个StrLookup的变量,它同样也进行了修改以支持LogEvent变量。这些共同提供了一个机制以允许配置去引用系统属性变量、配置文件、LogEvent中的ThreadContext Map和StructuredData等。这些变量既可以在执行配置时,也可以在执行每一个事件时被处理。请查看Lookup以获取更多信息。