第二章:架构

**所有真实分类都是家谱.

-查尔斯·达尔文,《物种起源》

如果不是不可能,那么任何人都很难仅通过阅读一门学科来学习该科目,而没有将信息应用到特定问题上,从而迫使他自己去思考所读的内容。此外,我们都最好地了解自己发现的东西.

—DONALD KNUTH,计算机编程的艺术**

Logback's architecture

Logback 的基本体系结构足够通用,可以在不同情况下应用。目前,logback 分为三个模块:logback-core,logback-classic 和 logback-access。

在本文档的其余部分,我们将编写“ logback”来引用经典的 logback 模块。

Logger,添加程序和布局

Logback 构建在三个主要类上:LoggerAppenderLayout。这三种类型的组件协同工作,使开发人员能够根据消息类型和级别记录消息,并在运行时控制如何格式化这些消息以及在何处报告它们。

Logger类是 logback-classic 模块的一部分。另一方面,AppenderLayout接口是 logback-core 的一部分。作为通用模块,logback-core 没有 Logger 的概念。

Logger context

与普通的System.out.println相比,任何日志记录 API 的首要优势都在于它能够禁用某些日志语句,同时允许其他语句不受阻碍地进行打印。此功能假定已根据开发人员选择的标准对日志记录空间(即所有可能的日志记录语句的空间)进行了分类。在经典的 logback 中,此分类是 Logger 的固有部分。每个 Logger 都附加到LoggerContext,后者负责制造 Logger 并将它们排列成树状层次结构。

Logger 是命名实体。它们的名称区分大小写,并且遵循分层命名规则:

Named Hierarchy

如果一个 Logger 的名称后跟一个点,则该 Logger 是另一个 Logger 的祖先,该后跟点的名称是该子 Logger 名称的前缀。如果 Logger 与子 Logger 之间没有祖先,则称该 Logger 为子 Logger 的父项。

例如,名为"com.foo"的 Logger 是名为"com.foo.Bar"的 Logger 的父级。类似地,"java""java.util"的父级和"java.util.Vector"的祖先。大多数开发人员都应该熟悉这种命名方案。

根 Logger 位于 Logger 层次结构的顶部。这是一个 exception,因为它从一开始就是每个层次结构的一部分。像每个 Logger 一样,可以按其名称检索它,如下所示:

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

还可以使用在org.slf4j.LoggerFactory类中找到的静态getLogger类来检索所有其他 Logger。此方法将所需 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); 
}

有效级别(又称级别继承)

可以为 Logger 分配级别。 ch.qos.logback.classic.Level类中定义了一组可能的级别(TRACE,DEBUG,INFO,WARN 和 ERROR)。请注意,在 logback 中,Level类是最终的,不能被子类化,因为存在更灵活的方法,以Marker对象的形式存在。

如果未为给定的 Logger 分配一个级别,则它将从其最接近的祖先那里继承一个已分配的级别。更正式地:

给定 Logger* L 的有效级别等于其层次结构中的第一个非空级别,从 L *本身开始,然后在层次结构中向上扩展到根 Logger。

为了确保所有 Logger 最终都可以继承级别,根 Logger 始终具有分配的级别。默认情况下,此级别是 DEBUG。

下面是四个示例,这些示例具有各种分配的级别值以及根据级别继承规则得出的有效(继承)级别。

Example 1

Logger name Assigned level Effective level
root DEBUG DEBUG
X none DEBUG
X.Y none DEBUG
X.Y.Z none DEBUG

在上面的示例 1 中,仅为根 Logger 分配了一个级别。该级别值DEBUG由其他 LoggerXX.YX.Y.Z继承。

Example 2

Logger name Assigned level Effective level
root ERROR ERROR
X INFO INFO
X.Y DEBUG DEBUG
X.Y.Z WARN WARN

在上面的示例 2 中,所有 Logger 都有一个分配的级别值。级别继承不起作用。

Example 3

Logger name Assigned level Effective level
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z ERROR ERROR

在上面的示例 3 中,分别为 LoggerrootXX.Y.Z分配了级别DEBUGINFOERROR。LoggerX.Y从其父级X继承其级别值。

Example 4

Logger name Assigned level Effective level
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z none INFO

在上面的示例 4 中,分别为 LoggerrootXDEBUGINFO分配了级别。LoggerX.YX.Y.Z从其最近的父级X继承其级别值,该父级已分配级别。

打印方法和基本选择规则

根据定义,打印方法确定记录请求的级别。例如,如果L是 Logger 实例,则语句L.info("..")是 INFO 级别的记录语句。

如果日志记录请求的级别高于或等于 Logger 的有效级别,则称为“启用”。否则,该请求被称为* disabled *。如前所述,没有分配级别的 Logger 将从其最近的祖先那里继承一个。该规则总结如下。

基本选择规则

如果* p> = q ,则向具有有效级别 q 的 Logger 发出级别为 p *的日志请求。

此规则是注销的核心。假定级别按以下 Sequences 排序:TRACE < DEBUG < INFO < WARN < ERROR

以更图形化的方式,这是选择规则的工作方式。在下表中,垂直标题显示记录请求的级别,由* p 表示,而水平标题显示 Logger 的有效级别,由 q *表示。行(级别请求)和列(有效级别)的交集是根据基本选择规则得出的布尔值。

level of
要求* 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

这是基本选择规则的示例。

import ch.qos.logback.classic.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
....

// get a logger instance named "com.foo". Let us further assume that the
// logger is of type  ch.qos.logback.classic.Logger so that we can
// set its level
ch.qos.logback.classic.Logger logger = 
        (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foo");
//set its Level to INFO. The setLevel() method requires a logback logger
logger.setLevel(Level. INFO);

Logger barlogger = LoggerFactory.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");

Retrieving Loggers

用相同的名称调用LoggerFactory.getLogger方法将始终返回对完全相同的 Logger 对象的引用。

例如,在

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

xy完全相同地指的是 Logger 对象。

因此,可以配置 Logger,然后在代码中其他地方检索相同实例,而无需传递引用。与亲生 parent 制(parent 总是先于子女)的根本矛盾是,可以以任何 Sequences 创建和配置logback Logger。特别是,“父”Logger 将找到并链接到其后代,即使在其后代实例化之后也是如此。

通常在应用程序初始化时完成对 logback 环境的配置。首选方法是读取配置文件。稍后将讨论这种方法。

使用 Logback 可以通过* software component *轻松命名 Logger。这可以通过实例化每个类中的 Logger 来实现,Logger 名称等于类的完全限定名称。这是定义 Logger 的有用且直接的方法。由于日志输出带有生成 Logger 的名称,因此这种命名策略使识别日志消息的来源变得容易。但是,这只是命名 Logger 的一种可能的策略,尽管很常见。 Logback 不限制可能的 Logger 集。作为开发人员,您可以随意命名 Logger。

但是,在记录员所在的类之后命名 Logger 似乎是迄今为止已知的最佳常规策略。

Appender 和布局

根据 Logger 有选择地启用或禁用日志记录请求的功能只是图片的一部分。 Logback 允许日志记录请求打印到多个目标。用 logback 说,输出目标称为追加器。当前,对于控制台,文件,远程套接字服务器,MySQL,PostgreSQL,Oracle 和其他数据库,JMS 和远程 UNIX Syslog 守护程序,存在附加程序。

一个 Logger 可以附加多个附加程序。

addAppender方法将附加器添加到给定的 Logger。给定 Logger 的每个启用的日志记录请求都将转发给该 Logger 中的所有附加程序以及层次结构中较高的附加程序。换句话说,追加程序是从 Logger 层次结构中累加继承的。例如,如果将控制台附加程序添加到根 Logger,则所有启用的记录请求将至少在控制台上打印。如果另外将文件追加器添加到 Logger 中,例如说* L ,则对 L L 的子级启用的记录请求将打印在控制台上的文件和*上。通过将 Logger 的可加性标志设置为 false,可以覆盖此默认行为,以便不再增加附加器累积。

下面概述了控制追加程序可加性的规则。

Appender Additivity

Logger* L 的 log 语句的输出将到达 L *及其祖先的所有追加程序。这就是术语“附加剂可加性”的含义。

但是,如果 Logger* L 的祖先(例如 P )的可加性标志设置为 false,则 L 的输出将定向到 L 中的所有追加程序及其祖先,直到 P ,但不是 P *的任何祖先中的附加项。

Logger 默认将其可加性标志设置为 true。

下表显示了一个示例:

Logger Name Attached Appenders Additivity Flag Output Targets Comment
root A1 not applicable A1 由于根 Logger 位于 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,所以没有附加器累积。仅使用附加器 A-sec。
security.access none true A-sec 仅添加“安全性”的附加项,因为“安全性”中的可加性标志设置为false

通常,用户不仅希望自定义输出目标,还希望自定义输出格式。这是通过将* layout *与附加程序相关联来实现的。布局负责根据用户的需求格式化日志记录请求,而附加程序负责将格式化后的输出发送到其目的地。 PatternLayout是标准 logback 分发的一部分,它使用户可以根据类似于 C 语言printf函数的转换模式来指定输出格式。

例如,具有转换模式“%-4relative [%thread]%-5level%logger{32}-%msg%n”的 PatternLayout 将输出类似于:

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

第一个字段是自程序启动以来经过的毫秒数。第二个字段是发出日志请求的线程。第三个字段是日志请求的级别。第四个字段是与日志请求关联的 Logger 的名称。 “-”之后的文本是请求的消息。

Parameterized logging

假设经典 logbackLogger 实现SLF4J 的 Logger 界面,则某些打印方法可以接受多个参数。这些打印方法变体主要旨在提高性能,同时最大程度地减少对代码可读性的影响。

对于某些 Logger logger而言,

logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

产生了构造 message 参数(即将整数ientry[i]都转换为 String 并串联中间字符串)的开销。这与是否将记录消息无关。

避免参数构造成本的一种可能方法是,将 log 语句包含在测试中。这是一个例子。

if(logger.isDebugEnabled()) { 
  logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}

这样,如果为logger禁用了调试,则不会产生参数构造的开销。另一方面,如果为 Logger 启用了 DEBUG 级别,则将产生两次评估该 Logger 是否启用的成本:一次在debugEnabled中,一次在debug中。实际上,这种开销微不足道,因为评估 Logger 所花费的时间不到实际记录请求所花费的时间的 1%。

Better alternative

存在一种基于消息格式的便捷替代方法。假设entry是一个对象,您可以编写:

Object entry = new SomeObject(); 
logger.debug("The entry is {}.", entry);

只有在评估是否记录日志之后,并且只有在决定是肯定的情况下,Logger 实现才会格式化消息,并将' {}'对替换为字符串值entry。换句话说,禁用 log 语句时,此格式不会产生参数构造的开销。

以下两行将产生完全相同的输出。但是,在* disabled *记录语句的情况下,第二个变量将比第一个变量至少好 30 倍。

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

还有两个参数变体。例如,您可以编写:

logger.debug("The new entry is {}. It replaces {}.", entry, oldEntry);

如果需要传递三个或更多参数,则还提供Object[]变体。例如,您可以编写:

Object[] paramArray = {newVal, below, above};
logger.debug("Value {} was inserted between {} and {}.", paramArray);

偷看

在介绍了基本的 logback 组件之后,我们现在准备描述用户调用 Logger 的打印方法时 logback 框架所采取的步骤。现在让我们分析当用户调用名为* com.wombat *的 Logger 的info()方法时,logback 采取的步骤。

1.获得过滤器链决策

如果存在,则调用TurboFilter链。 Turbo 过滤器可以设置上下文范围的阈值,或基于与每个日志记录请求关联的信息,例如MarkerLevelLogger,消息或Throwable来过滤某些事件。如果过滤器链的答复是FilterReply.DENY,那么将丢弃日志记录请求。如果是FilterReply.NEUTRAL,那么我们 continue 下一步,即步骤 2.如果答复是FilterReply.ACCEPT,我们跳过下一步,直接跳到步骤 3.

2.应用基本选择规则

在这一步,logback 将 Logger 的有效级别与请求级别进行比较。如果根据此测试禁用了日志记录请求,则 logback 将丢弃该请求,而无需进一步处理。否则,将 continue 进行下一步。

3.创建一个 LoggingEvent 对象

如果请求在先前的过滤器中仍然存在,则 logback 将创建一个ch.qos.logback.classic.LoggingEvent对象,该对象包含请求的所有相关参数,例如请求的 Logger,请求级别,消息本身,可能已随请求一起传递的异常,当前时间,当前线程,有关发出日志记录请求的类和MDC的各种数据。请注意,其中某些字段仅在实际需要时才延迟初始化。 MDC用于用其他上下文信息修饰日志记录请求。 MDC 在subsequent chapter中讨论。

4.调用附加程序

创建LoggingEvent对象后,logback 将调用所有适用附加程序的doAppend()方法,即从 Logger 上下文继承的附加程序。

Logback 分发附带的所有附加程序都扩展了AppenderBase抽象类,该抽象类在确保线程安全的同步块中实现doAppend方法。 AppenderBasedoAppend()方法还调用附加到附加程序的自定义过滤器(如果存在任何此类过滤器)。可以动态附加到任何附加程序的自定义过滤器以separate chapter表示。

5.格式化输出

被调用的附加程序负责格式化日志记录事件。但是,一些(但不是全部)附加程序将格式化日志记录事件的任务委托给布局。布局会格式化LoggingEvent实例,并以字符串形式返回结果。请注意,某些附加程序(例如SocketAppender)不会将日志记录事件转换为字符串,而是将其序列化。因此,它们没有布局,也不需要布局。

6.发送 LoggingEvent

日志记录事件完全格式化后,每个附加程序将其发送到其目的地。

这是一个序列 UML 图,以显示一切工作原理。您可能要单击该图像以显示其较大版本。

Performance

经常提到的反对日志记录的论点之一是其计算成本。这是一个合理的问题,因为即使大小适中的应用程序也可以生成数千个日志请求。我们的大部分开发工作都花在了衡量和调整 logback 的性能上。与这些工作无关,用户仍应注意以下性能问题。

1.完全关闭日志记录时的日志记录性能

您可以通过将 root 记录程序的级别设置为Level.OFF(可能的最高级别)来完全关闭日志记录。当完全关闭日志记录时,日志请求的成本包括方法调用和整数比较。在 3.2Ghz 的 Pentium D 机器上,此成本通常约为 20 纳秒。

但是,任何方法调用都涉及参数构造的“隐藏”成本。例如,对于某些 Logger* x *的写作,

x.debug("Entry number: " + i + "is " + entry[i]);

无论是否记录消息,都会花费构造消息参数的成本,即将整数ientry[i]都转换为字符串,并 Connecting 间字符串。

参数构造的成本可能很高,并且取决于所涉及参数的大小。为了避免参数构造的开销,您可以利用 SLF4J 的参数化日志记录:

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

此变体不会产生参数构造的成本。与上一次对debug()方法的调用相比,它的速度将大大提高。仅当将日志记录请求发送到附加的附加程序时,消息才会被格式化。此外,格式化消息的组件已得到高度优化。

尽管上述将日志语句置于紧密的循环中,即非常频繁地调用的代码,是一个失败的提议,很可能导致性能下降。即使关闭了日志记录,紧密循环的日志记录也会使您的应用程序变慢,并且即使打开了日志记录,也会生成大量(因此无用)的输出。

2.决定打开日志时是否logback的性能。

在 logback 中,无需遍历 Logger 层次结构。Logger 在创建时便知道其有效级别(即,一旦考虑了级别继承,它的级别)。如果更改了父 Logger 的级别,则将联系所有子 Logger 以注意更改。因此,在基于有效级别接受或拒绝请求之前,Logger 可以做出准即时的决定,而无需咨询其祖先。

3.实际记录(格式化和写入输出设备)

这是格式化日志输出并将其发送到目标目的地的成本。再次在这里,我们付出了巨大的努力来使布局(格式器)尽快执行。Appender 也是如此。当记录到本地计算机上的文件时,实际记录的典型成本约为 9 到 12 微秒。logback到远程服务器上的数据库时,最多需要几毫秒的时间。

尽管功能丰富,但回日志的首要设计目标之一是执行速度,这是仅次于可靠性的要求。一些 logback 组件已被重写多次,以提高性能。

首页