以下内容翻译整理自logback官方手册,地址:logback官方手册
logback 的架构
logback的基本架构足够通用,可以应用于不同的环境。目前,logback分为三个模块:logback-core,logback-classic和logback-access。
core模块是其它两个模块的基础,classic模块继承core模块,classic模块相对log4j版本有显著的改进,logback-classic天生实现了SLF4J API,所以你可以在logback和其他日志框架之间自由切换,比如log4j和JDK1.4引入的JUL(java.util.logging)。access模块集成了Servlet容器,用来提供HTTP-access日志功能,一个单独的文档包含访问模块文档。
在本文档的其余部分中,我们将引用logback-classic模块来编写logback。
Logger,Appenders 和 Layouts
Logback基于三个主要类:Logger,Appender和Layout,这三种类型的组件协同工作,使开发人员能够根据消息类型和级别记录消息,并在运行时控制这些消息的格式和报告位置。
Logger类是logback-classic模块的一部分,Appender和Layout接口是logback-core模块的一部分。作为通用模块,logback-core没有日志记录器的概念。
logger是日志记录器,appender是追加器,layout是布局。
Logger 上下文
任何日志API相对于普通的System.out.println的最重要的优势是能够禁用某些日志语句,同时允许其他语句不受阻碍地打印。该功能假定的日志空间是根据一些开发人员选择的标准进行分类的。在logback-classic中,这种分类是logger的固有组成部分。每个logger都附加到一个LoggerContext,该上下文负责生成logger,并将它们安排在类似层次结构的树中。
logger是命名实体。它们的名字区分大小写,并遵循分层命名规则:
如果一个
logger的名称后面跟着一个点,那么这个logger就是另一个logger的祖先。如果在其自身和后代logger之间没有祖先,则该logger被称为子logger的父。
例如,名称为com.foo的logger是名称为com.foo.Bar的logger的父,类似地,java是java.util的父,是java.util.Vector的祖先。开发人员都应该熟悉这种命名方案。
根logger位于logger层次结构的顶部。它的特殊之处在于,它一开始就是每个层次结构的一部分。与每个logger一样,可以通过它的名称获取它,如下所示:
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
所有其他logger也可以通过org.slf4j.LoggerFactory类中的静态方法getLogger()来获取。该方法需要传递日志记录器的名称作为参数。下面列出了Logger接口中的一些基本方法。
package org.slf4j;
public interface Logger {
String ROOT_LOGGER_NAME = "ROOT";
// 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可以被分配级别,可以设置的级别有TRACE, DEBUG, INFO, WARN, ERROR,这些级别别定义在ch.qos.logback.classic.Level类中,该类是final修饰的,不能被子类化。
如果一个给定的logger没有被分配一个级别,那么它将从其最近的祖先那里继承一个级别。为了确保所有的logger最终都能继承到一个级别,根logger有一个默认级别DEBUG。
下面是四个例子,根据级别继承规则,使用各种指定的级别值和产生的有效(继承)级别。
示例1
| Logger name | 指定级别 | 有效级别 |
|---|---|---|
| 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 name | 指定级别 | 有效级别 |
|---|---|---|
| root | ERROR | ERROR |
| X | INFO | INFO |
| X.Y | DEBUG | DEBUG |
| X.Y.Z | WARN | WARN |
示例2中,所有logger都有一个指定的级别值,级别继承不起作用。
示例3
| Logger name | 指定级别 | 有效级别 |
|---|---|---|
| root | DEBUG | DEBUG |
| X | INFO | INFO |
| X.Y | none | INFO |
| X.Y.Z | ERROR | ERROR |
示例3中,日志记录器root, X和X.Y.Z都有指定的级别,X.Y没有指定级别,是从父日志记录器X继承的级别。
示例4
| Logger name | 指定级别 | 有效级别 |
|---|---|---|
| root | DEBUG | DEBUG |
| X | INFO | INFO |
| X.Y | none | INFO |
| X.Y.Z | none | INFO |
示例4中,日志记录器root和X有指定的级别,X.Y和X.Y.Z没有指定级别,从最近的有指定级别的父级X继承级别值。
打印方法和基本选择规则
根据定义,打印方法确定日志请求的级别。例如,如果L是一个logger实例,那么语句L. INFO(“..”)就是一个级别INFO的日志语句。
如果日志记录请求的级别高于或等于其日志记录程序的有效级别,则启用日志记录请求。否则,该请求将被禁用。如前所述,没有指定级别的日志记录器将从其最近的祖先那里继承一个级别。这条规则是logback的核心。它规定各级的次序如下:
TRACE < DEBUG < INFO < WARN < ERROR
下面是一个基本选择规则的例子。
package com.wangbo.cto.logback;
import ch.qos.logback.classic.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @date 2019/9/13 22:48
* @auther wangbo
*/
public class LogLevelTest {
public static void main(String[] args) {
//获取一个名为“com.foo”的logger,为了能设置级别,转换为ch.qos.logback.classic.Logger类型
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foo");
//设置级别
logger.setLevel(Level.INFO);
//继承最近的父com.foo的级别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.");
//info >= info,启用此请求
barlogger.info("Located nearest gas station.");
//debug <= info,此请求已禁用
barlogger.debug("Exiting gas station search");
}
}
运行结果
22:59:44.139 [main] WARN com.foo - Low fuel level.
22:59:44.141 [main] INFO com.foo.Bar - Located nearest gas station.
获取 Logger
调用LoggerFactory.getLogger,相同名称的方法将始终返回相同Logger对象的引用。例如:
Logger x = LoggerFactory.getLogger("wombat");
Logger y = LoggerFactory.getLogger("wombat");
X和Y是相同的Logger对象。
因此,可以配置一个日志程序,然后在代码的其他地方通过相同的名字获取到相同的实例,而不需要传递引用。与生物学意义上的父母(父母总是先于子女)相反,logback日志记录器可以按任何顺序创建和配置。特别是,父logger将发现并链接到它的后代,即使它是在它们之后实例化的。
通常在应用程序初始化时配置logback环境。首选的方法是读取配置文件。不久将讨论这种方法。
以日志记录器所在的类命名日志记录器似乎是迄今为止所知的最佳通用策略。
Appenders 和 Layouts
根据日志程序选择性地启用或禁用日志记录请求的功能只是一部分。Logback允许将日志请求打印到多个目的地。在logback中,输出目的地称为appender。目前,针对控制台、文件、远程套接字服务器、MySQL、PostgreSQL、Oracle和其他数据库、JMS和远程UNIX Syslog守护进程存在附加程序。
一个logger可以附加多个appender。
addAppender方法向给定的logger添加一个appender。对于给定的logger,每个启用的日志请求都将被转发到该logger中的所有appender以及层次结构中更高的appender。换句话说,appender是附加地从日志程序层次结构继承的。例如,如果将控制台appender添加到根logger,那么所有启用的日志请求至少都将打印在控制台上。此外,如果向logger(L)添加了一个文件appender,然后,为 L 和 L 的子节点启用的日志记录请求将打印在文件里和控制台上。通过将logger的additivity flag设置为false,可以覆盖此默认行为,使追加器积累不再是附加的。
下表是一个例子:
| Logger Name | 附加的 Appenders | Additivity Flag | 输出目标 | 注释 |
|---|---|---|---|---|
| root | A1 | 不适用 | A1 | 由于根日志程序位于日志程序层次结构的顶部,所以不应用加法标志。 |
| x | A-x1, A-x2 | true | A1, A-x1, A-x2 | 使用了 x 和 root 的追加器 |
| x.y | none | true | A1, A-x1, A-x2 | 使用了 x 和 root 的追加器 |
| x.y.z | A-xyz1 | true | A1, A-x1, A-x2, A-xyz1 | 使用了 x.y.z,x 和 root的追加器 |
| security | A-sec | false | A-sec | 由于可加性标志设置为 false,所以没有追加器累加,只会使用一个追加器 A-sec |
| security.access | none | true | A-sec | 因为 security 中的可加性标志设置为 false,所以只使用 security 的追加器 A-sec |
通常,用户不仅希望自定义输出目的地,还希望自定义输出格式。可以通过将layout与appender关联来实现。layout负责根据用户的意愿格式化日志请求,appender负责将格式化的输出发送到它的目的地。PatternLayout是标准logback分发版的一部分,允许用户根据类似于C语言printf函数的转换模式指定输出格式。
例如,PatternLayout设置为%-4relative [%thread] %-5level %logger{32} - %msg%n,将输出类似于下面格式的内容:
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.
第一个字段是自程序启动以来经过的毫秒数。第二个字段是发出日志请求的线程。第三个字段是日志请求的级别。第四个字段是与日志请求关联的日志记录器的名称。'-'后面的文本是请求的消息。
参数化日志
考虑到logback-classic中的logger实现了SLF4J的Logger接口,某些打印方法允许多个参数。这些打印方法变体主要是为了提高性能,同时降低对代码可读性的影响。
普通写法
对于一些logger,可以这样写:
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
该参数将整数 i 和 entry[i] 转换为字符串,并连接中间的字符串。会导致构造消息参数的额外开销,但是这与是否记录消息没有关系。
避免参数构造额外开销的一种方法是用一个测试包围 log 语句。比如这样:
if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}
这样,如果logger禁用了DEBUG级别,就不会产生参数构造的开销。另一方面,如果logger启用了DEBUG级别,系统将承担两次评估日志记录器是否启用的成本,一次是在debugEnabled,第二次是在debug,在实践中,这种开销是微不足道的,因为评估一个日志记录器所需时间相对于实际记录一个请求所需的时间不到1%。
推荐写法
存在一种基于消息格式的替代方法。假设entry是一个对象,可以这样写:
Object entry = new SomeObject();
logger.debug("The entry is {}.", entry);
只有在评估是否进行日志记录之后,并且只有在决定记录日志的情况下,日志程序才会实现将消息格式化,并用条目的字符串值替换“{}”。换句话说,当禁用 log 语句时,这种写法不会产生参数构造的成本。
下面两行代码将产生完全相同的输出。然而,在禁用日志语句的情况下,第二种变体的性能至少比第一种变体好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组件之后,现在可以描述当用户调用日志程序的打印方法时,logback框架所采取的步骤。现在让我们分析用户调用名为com.wombat的日志记录器的info()方法时,logback所采取的步骤。
1. 获得过滤器链决策
如果存在,则调用TurboFilter链。Turbo 过滤器可以设置上下文范围的阈值,或者根据与每个日志请求关联的标记、级别、日志记录器、消息或可抛出性等信息过滤掉某些事件。如果过滤器链的响应是拒绝FilterReply.DENY,则日志请求将被删除。如果是中性FilterReply.NEUTRAL,然后我们继续下一步,即第2步。如果是接受FilterReply.ACCEPT,我们跳过下一步,直接跳到步骤3。
2. 应用基本的选择规则
在此步骤中,logback将日志记录器的有效级别与请求的级别进行比较。如果根据此测试禁用日志记录请求,那么logback将删除该请求,而不进行进一步处理。否则,将继续下一步。
3. 创建一个 LoggingEvent 对象
如果请求通过了前面的过滤器,logback将创建一个ch. qs .logback.classic.LoggingEvent对象,该对象包含请求的所有相关参数,例如请求的日志记录器,请求级别,消息本身,可能随请求一起传递的异常、当前时间、当前线程、发出日志记录请求的类的各种数据以及 MDC。注意,其中一些字段是延迟初始化的,只有在实际需要时才会这样做。MDC 用于用附加的上下文信息装饰日志记录请求。MDC将在下一章中讨论。
4. 调用 appenders
创建 LoggingEvent对象之后,logback将调用所有适用的appender的doAppend()方法,即从日志程序上下文中继承的appender。
logback发行版附带的所有附加程序都扩展了AppenderBase抽象类,该类在确保线程安全的同步块中实现doAppend方法。如果存在附加的自定义过滤器,AppenderBase的doAppend()方法也能调用。可以动态附加到任何附加器的自定义过滤器将在单独的一章中介绍。
5. 格式化输出
被调用的附加程序负责格式化日志事件。然而,一些(但不是所有)附加程序将格式化日志事件的任务委托给了layout,布局可以格式化LoggingEvent实例并以字符串的形式返回结果。注意,有些附加程序,如SocketAppender,不将日志事件转换为字符串,而是序列化它。因此,它们没有也不需要布局。
6. 发送 LoggingEvent
日志事件完全格式化后,由每个附加程序将其发送到目的地。下面是一个序列 UML 图,展示了所有事情是如何工作的。









网友评论