Logback中文文档(二):体系结构

时间:2021-05-24 14:28:10

logback

Logback 的基本结构充分通用,可应用于各种不同环境。目前,logback 分为三个模块:Core、Classic 和 Access。

Core模块是其他两个模块的基础。Classic 模块扩展了core模块。Classic 模块相当于 log4j的显著改进版。Logback-classic 直接实现了 SLF4J API,因此你可以在 logback 与其他记录系统如 log4j 和 java.util.logging (JUL)之间轻松互相切换。Access 模块与 Servlet 容器集成,提供 HTTP 访问记录功能。本文不讲述 access 模块。

本文中,“logback”代表 logback-classic 模块。

Logger 、Appender 和 和 Layout

Logback 建立于三个主要类之上:Logger、Appender 和 Layout。这三种组件协同工作,使开发者可以按照消息类型和级别来记录消息,还可以在程序运行期内控制消息的输出格式和输出目的地。

Logger类是logback-classic 模块的一部分,而Appender和Layout接口来自logback-core。

作为一个多用途模块,logback-core 不包含任何 logger。

Logger 上下文

任何比System.out.println高级的记录API的第一个也是最重要的优点便是能够在禁用特定记录语句的同时却不妨碍输出其他语句。这种能力源自记录隔离(space)——即所有各种记录语句的隔离——是根据开发者选择的条件而进行分类的。在 logback-classic 里,这种分类是 logger 固有的。各个 logger 都被关联到一个 LoggerContext,LoggerContext 负责制造logger,也负责以树结构排列各 logger。

Logger 是命名了的实体。它们的名字大小写敏感且遵从下面的层次化的命名规则:

命名层次:

如果 logger 的名称带上一个点号后是另外一个 logger 的名称的前缀,那么, 前者 就被称为 后者的祖先。 如果 logger 与代 其后代 logger 之间没有其他祖先, 那么,前者就被称为子logger 之 父。

比如,名为“com.foo"”的 logger 是名为“com.foo.Bar”之父。同理, “java”是“java.util"”之父,也是“java.util.Vector”的祖先。

根 logger 位于 logger 等级的最顶端,它的特别之处是它是每个层次等级的共同始祖。

如同其他各 logger,根 logger 可以通过其名称取得,如下所示:

Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);

其他所有 logger 也通过 org.slf4j.LoggerFactory 类的静态方法 getLogger 取得。getLogger方法以 logger 名称为参数。Logger 接口的部分基本方法列举如下:

package org.slf4j;
public interface Logger {
// Printing methods:
public void trace(String message);
public void debug(String message);
public void info(String message);
public void warn(String message);
public void error(String message); }

有效 级别(Level ) 即 级别 继承

Logger 可以被分配级别。级别包括:TRACE、DEBUG、INFO、WARN 和 ERROR,定义于 ch.qos.logback.classic.Level 类。注意在 logback 里,Level 类是 final 的,不能被继承,Marker 对象提供了更灵活的方法。

如果 logger 没有被分配级别,那么它将从有被分配级别的最近的祖先那里继承级别。

更正式地说:logger L 的有效级别等于其层次等级里的第一个非 null级别,顺序是从 从 L 开始, 向上直至根 logger 。

为确保所有 logger 都能够最终继承一个级别,根 logger 总是有级别,默认情况下,这个级别是 DEBUG。

下面的四个例子包含各种分配级别值和根据级别继承规则得出的最终有效(继承)级别。

  • 例 1
Logger名 分配级别 有效级别
root DEBUG DEBUG
X none DEBUG
X.Y none DEBUG
X.Y.Z none DEBUG

例 1 里,仅根 logger 被分配了级别。级别值 DEBUG 被其他 logger X、X.Y 和 X.Y.Z 继

承。

  • 例 2
Logger名 分配级别 有效级别
root ERROR ERROR
X INFO INFO
X.Y DEBUG DEBUG
X.Y.Z WARN WARN

例 2 里,所有 logger 都被分配了级别。级别继承不发挥作用。

  • 例 3
Logger名 分配级别 有效级别
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z ERROR ERROR

例 3 里,根 logger、X 和 X.Y.Z 分别被分配了 DEBUG、INFO 和 ERROR 级别。X.Y 从

其父 X 继承级别。

  • 例 4
Logger名 分配级别 有效级别
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z none none

例 4 里,根 logger 和 X 分别被分配了 DEBUG 和 INFO 级别。X.Y 和 X.Y.Z 从其最近的

父 X 继承级别,因为 X 被分配了级别

打印方法和基本选择规则

根据定义,打印方法决定记录请求的级别。例如,如果 L 是一个 logger 实例,那么,语句 L.info("..")是一条级别为 INFO 的记录语句。

记录请求的级别在高于或等于其 logger 的有效级别时被称为被启用,否则,称为被禁用。如前所述,没有被分配级别的 logger 将从其最近的祖先继承级别。该规则总结如下:

基本选择规则

记录为 请求级别为 p ,其 logger 的有效级别为 为 q, , 只有当 则当 p>=q, 时, 该请求才会被执行。


该规则是 logback 的核心。级别排序为:TRACE < DEBUG < INFO < WARN < ERROR。

下表显示了选择规则是如何工作的。行头是记录请求的级别 p。列头是 logger 的有效级别 q。行(请求级别)与列(有效级别)的交叉部分是按照基本选择规则得出的布尔值。

请求级别 p 有效级别 q
TRACE DEBUG INFO WARN ERROR OFF
TRACE YES NO NO NO NO NO
DEBUG YES YES NO NO NO NO
INFO YES YES YES NO NO NO
WARN YES YES YES YES NO NO
ERROR YES YES YES YES YES NO
OFF YES NO NO NO NO NO

下面是基本选择规则的例子。

// 取得名为"com.foo"的 logger 实例
Logger logger = LoggerFactory.getLogger("com.foo");
// 设其级别为 INFO
logger.setLevel(Level.INFO);
Logger barlogger = LoggerFactory.getLogger("com.foo.Bar");
// 该请求有效,因为 WARN >= INFO
logger.warn("Low fuel level.");
// 该请求无效,因为 DEBUG < INFO.
logger.debug("Starting search for nearest gas station.");
// 名为"com.foo.Bar"的 logger 实例 barlogger, 从"com.foo"继承级别
// 因此下面的请求有效,因为 INFO >= INFO.
barlogger.info("Located nearest gas station.");
// 该请求无效,因为 DEBUG < INFO.
barlogger.debug("Exiting gas station search");

译者注:上例的 logger.setLevel(Level.INFO)无效。org.slf4j.Logger 没有 setLevel()方法,ch.qos.logback.classic.Logger 有此方法。

获取 Logger

用同一名字调用 LoggerFactory.getLogger 方法所得到的永远都是同一个 logger 对象的引用。

例如:

Logger x = LoggerFactory.getLogger("wombat");
Logger y = LoggerFactory.getLogger("wombat");

x 和 y 指向同一个 logger 对象。

因此,可以配置一个 logger,然后从其他地方取得同一个实例,不需要到处传递引用。

生物学里的父母总是先于其孩子,而 logback 不同,它可以以任何顺序创建和配置 logger。

特别的是,即使“父”logger 是在其后代初始化之后才初始化的,它仍将查找并链接到其后代们。

通常是在程序初始化时对 logback 环境进行配置。推荐用读配置文件类进行配置。稍后会讲这种方法。

Logback 简化了 logger 命名,方法是在每个类里初始化 logger,以类的全限定名作为

logger 名。这种定义 logger 的方法即有用又直观。由于记录输出里包含 logger 名,这种命名

方法很容易确定记录消息来源。Logback 不限制 logger 名,你可以随意命名 logger。

然而,目前已知最好的策略是以 logger 所在类的名字作为 logger 名称。

Appender 和 和 Layout

有选择性地启用或禁用记录请求仅仅是 logback 功能的冰山一角。Logback 允许打印记录请求到多个目的地。在 logback 里,一个输出目的地称为一个 appender。目前有控制台、文件、远程套接字服务器、MySQL、PostreSQL、Oracle和其他数据库、JMS和远程UNIX Syslog守护进程等多种 appender。

一个 logger 可以被关联多个 appender。

方法 addAppender 为指定的 logger 添加一个 appender。对于 logger 的每个启用了的记录请求,都将被发送到 logger 里的全部 ppender 及更高等级的 appender。换句话说,appender叠加性地继承了 logger 的层次等级。例如,如果根 logger 有一个控制台 appender,那么所有

启用了的请求都至少会被打印到控制台。如果 logger L 有额外的文件 appender,那么,L 和L后代的所有启用了的请求都将同时打印到控制台和文件。设置 logger 的 additivity 为 false,则可以取消这种默认的 appender 累积行为。

控制 appender 叠加性的规则总结如下。

Appender 叠加性

Logger L 的记录 语句的输出会发送给 L 及其祖先的全部appender。这就是“appender叠加性”的含义。

然而,果 如果 logger L 的某个祖先 P 设置叠加性标识为 false,那么,L 的输出会发送给L 与 与 P 之间(含 P )的所有 appender ,但不会发送给 P 的任何祖先的 appender。

Logger 的叠加性默认为 true。

示例:

Logger 名 关联的Appender 叠加性标识 输出目标 说明
root A1 不可用 A1 叠加性标识不适用于根 logger
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,所以appender 不累积。只有 A-sec
security.access none true A-sec 只有 security,因为 security 的叠 加 性 标 识 为false。

有些用户希望不仅可以定制输出目的地,还可以定制输出格式。这时为 appender 关联一个 layout 即可。Layout 负责根据用户意愿对记录请求进行格式化,appender 负责将格式化化后的输出发送到目的地。PatternLayout 是标准 logback 发行包的一部分,允许用户按照类似于 C 语言的 printf 函数的转换模式设置输出格式。

例如,转换模式"%-4relative [%thread] %-5level %logger{32} - %msg%n"在 PatternLayout里会输出形如:

176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.

第一个字段是自程序启动以来的逝去时间,单位是毫秒。

第二个地段发出记录请求的线程。

第三个字段是记录请求的级别。

第四个字段是与记录请求关联的 logger 的名称。

“-”之后是请求的消息文字。

参数化记录

因为 logback-classic 里的 logger 实现了 SLF4J 的 Logger 接口,某些打印方法可接受多个参数。这些不同的打印方法主要是为了在提高性能的同时尽量不影响代码可读性。

对于某个 Logger,下面的代码

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]));
}

当 logger 的 debug 级别被禁用时,这个方法可以避免参数构造带来的性能消耗。另一方面,如果 logger 的 DEBUG 级别被启用,那么会导致两次评估 logger 是否被启用:一次是isDebugEnabled 方法,一次是 debug 方法。在实践中,这种额外开销无关紧要,因为评估 logger所消耗的时间不足实际记录请求所用时间的 1%。

更好的替代方法

还有一种基于消息格式的方便的替代方法。假设 entry 是一个 object,你可以编写:

Object entry = new SomeObject();

logger.debug("The entry is {}.", entry);

在评估是否作记录后,仅当需要作记录时,logger 才会格式化消息,用 entry 的字符串值替换"{}"。换句话说,当记录语句被禁用时,这种方法不会产生参数构造所带来的性能消耗。

工作原理

介绍过 logback 的核心组件后,下面描述 logback 框架在用户调用 logger 的打印方法时

所做的事情。在本例中,用户调用名为 com.wombat 的 logger 的 info()方法。

  1. 取得过滤链(filter chain)的判定结果

    如果 TurboFilter 链存在,它将被调用。Turbo filters 能够设置一个上下文范围内的临界值,这个临界值或者表示过滤某些与信息有关(比如 Marker、级别、Logger、消息)的特定事件,或者表示与每个记录请求相关联的 Throwable。如果过滤链的结果是 FilterReply.DENY,则记录请求被抛弃。如果结果是 FilterReply.NEUTRAL,则继续下一步,也就是第二步。如果结果是 FilterReply.ACCEPT,则忽略过第二步,进入第三步。
  2. 应用基本选择规则

    Logback 对 logger 的有效级别与请求的级别进行比较。如果比较的结果是记录请求被禁用,logback 会直接抛弃请求,不做任何进一步处理。否则,继续下一步。
  3. 创建 LoggingEvent 对象

    记录请求到了这一步后,logback 会创建一个 ch.qos.logback.classic.LoggingEvent 对象,该对象包含所有与请求相关的参数,比如请求用的 logger、请求级别、消息、请求携带的异常、当前时间、当前线程、执行记录请求的类的各种数据,还有 MDC。

    注意有些成员是延迟初始化的,只有当它们真正被使用时才会被初始化。MDC 用来为记录请求添加额外的上下文信息。之后的章节会讨论 MDC。
  4. 调用 appender

    创建了 LoggingEvent 对象后,logback 将调用所有可用 appender 的 doAppend()方法,这就是说,appender 继承 logger 的上下文。

    所有 appender 都继承 AppenderBase 抽象类,AppenderBase 在一个同步块里实现了doAppend 方以确保线程安全。AppenderBase 的 doAppender()方法也调用 appender关联的自定义过滤器,如果它们存在的话。自定义过滤器能被动态地关联到任何appender,另有章节专门讲述它。
  5. 格式化输出

    那些被调用了的 appender 负责对记录事件(LoggingEvent)进行格式化。然而,有些但不是全部 appender 把格式化记录事件的工作委托给 layout。Layout 对LoggingEvent 实例进行格式化,然后把结果以字符串的形式返回。注意有些appender,比如 SocketAppender,把记录事件进行序列化而不是转换成字符串,所以它们不需要也没有 layout。
  6. 发送记录事件(LoggingEvent)

    记录事件被格式化后,被各个 appender 发送到各自的目的地。

    下图是整个流程的 UML 图。Logback中文文档(二):体系结构

性能

一个关于记录的常见争论是它的计算代价。这种关心很合理,因为即使是中等大小的应

用程序也会生成数以千计的记录请求。人们花了很多精力来测算和调整记录性能。尽管如此,

用户还是需要注意下面的性能问题。

  1. 记录被彻底关闭时的记录性能

    你可以将根 logger 的级别设为*的 Level.OFF,就可以彻底关闭记录。当记录被彻底关闭时,记录请求的消耗包括一次方法调用和一次整数比较。在 CPU 为3.2Ghz 的 Pentium D 电脑上,一般需要 20 纳秒。

    但是,任何方法调用都会涉及“隐藏的” 参数构造消耗,例如,对于 logger x,x.debug("Entry number: " + i + "is " + entry[i]);

    把整数 i 和 entry[i]都转换为字符串和连接各字符串会造成消息参数构造消耗,不管消息是否被记录。

    参数构造消耗可以变得非常高,同时也跟参数大小有关。利用 SLF4J 的参数化记录可以避免这种消耗。

    x.debug("Entry number: {} is {}", i, entry[i]);

    这种方式不会造成参数构造消耗。与前面的 debug()方法相比,这种方法快得多。

    只有当请求在被发送给 appender 时,消息才会被格式化。在格式化的时候,负责格式化消息的组件性能很高,不会对整个过程造成负面影响。格式化 1 个和 3 个参数分别需要 2 和 4 微妙。

    请注意,无论如何,应当避免在紧密循环里或者非常频繁地调用记录语句,因为很可能降低性能。即使记录被禁用,在紧密循环里作记录仍然会拖慢应用程序,如果记录被启用,就会产生大量(也是无用的)输出。
  2. 当记录启用时,判断是否进行记录的性能

    在 logback 中,logger 在被创建时就明确地知道其有效级别(已经考虑了级别继承)。

    当父 logger 的级别改变时,所有子 logger 都会得知这个改变。因此,在根据有效级别去接受或拒绝请求之前,logger 能够作出准即时判断,不需要咨询其祖先。
  3. 实际记录(格式化和写入输出设备)

    性能消耗包括格式化肌瘤输出和发送到目的地。我们努力使 layout(formatter)和appender都尽可能地快。记录到本地机器的文件里的耗时一般大约在 9至12微秒。

    如果目的地是远程服务器上的数据库时,会增加早几个毫秒。

    尽管功能丰富,logback 最首要的一项设计目标就是执行速度,重要程度仅排在可靠性之后。为提高性能,logback 的一些组件已经被多次重写。