第二章:架构

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

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

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

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

Logback's architecture

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

  • core *模块为其他两个模块奠定了基础。 * classic 模块扩展 core 。经典模块对应于 log4j 的显着改进版本。 Logback-classic 本机实现SLF4J API,因此您可以轻松地在 logback 和 JDK 1.4 中引入的其他日志记录系统(例如 log4j 或 java.util.logging(JUL))之间来回切换。第三个名为 access *的模块与 Servlet 容器集成以提供 HTTP 访问日志功能。单独的文档涵盖了访问模块文档

在本文档的其余部分,我们将编写“ 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 nameAssigned levelEffective level
rootDEBUGDEBUG
XnoneDEBUG
X.YnoneDEBUG
X.Y.ZnoneDEBUG

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

Example 2

Logger nameAssigned levelEffective level
rootERRORERROR
XINFOINFO
X.YDEBUGDEBUG
X.Y.ZWARNWARN

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

Example 3

Logger nameAssigned levelEffective level
rootDEBUGDEBUG
XINFOINFO
X.YnoneINFO
X.Y.ZERRORERROR

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

Example 4

Logger nameAssigned levelEffective level
rootDEBUGDEBUG
XINFOINFO
X.YnoneINFO
X.Y.ZnoneINFO

在上面的示例 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 *
TRACEDEBUGINFOWARNERROROFF
TRACEYESNONONONONO
DEBUGYESYESNONONONO
INFOYESYESYESNONONO
WARNYESYESYESYESNONO
ERRORYESYESYESYESYESNO

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

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 NameAttached AppendersAdditivity FlagOutput TargetsComment
rootA1not applicableA1由于根 Logger 位于 Logger 层次结构的顶部,因此可加性标志不适用于它。
xA-x1, A-x2trueA1,A-x1,A-x2“ x”和根的附加词。
x.ynonetrueA1,A-x1,A-x2“ x”和根的附加词。
x.y.zA-xyz1trueA1,A-x1,A-x2,A-xyz1“ x.y.z”,“ x”和词根的附加词。
securityA-secfalseA-sec由于将可加性标志设置为false,所以没有附加器累积。仅使用附加器 A-sec。
security.accessnonetrueA-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 组件已被重写多次,以提高性能。