第二章 Logback的架构(一)
Logback的架构
Logback作为一个通用框架,可以应对不同场景的日志记录。目前,Logback 被划分为三个模块:logback-core、logback-classic 和 logback-access。
Logback的core模块为其他两个模块提供基础支持。classic模块扩展了core模块,相当于一个改进版的Log4j。Logback-classic原生实现了SLF4J API,
所以可以轻松的切换到其他日志框架,比如 log4j 或 java.util.logging (JUL)。第三个模块logback-access与Servlet容器集成,以提供HTTP访问日志功能,
需要了解更多详情,请参考logback-access模块文档。
在这份文档的其余部分,我们将用 “logback” 来指代 logback-classic 模块。
Logger, Appenders 和 Layouts
Logger
、Appender
和Layout
共同构成了Logback的核心组件。这些组件协同工作,使开发人员能够根据类型和级别记录消息,并在运行时控制这些消息的格式和报告位置。
Logger
类属于logback-classic模块,Appender
和Layout
接口属于 logback-core 模块。 logback-core作为一个通用模块,并不涉及Logger日志记录器的具体功能。
日志的上下文容器
相较于普通的控制台输出System.out.println
,日志API的首要优势在于它能够在禁用某些日志语句的同时,允许其他语句不受限制地打印。
这个能力假定日志空间(所有可能的日志语句的空间)可以根据开发人员选择的标准进行分类。在logback-classic,这种分类是Logger的一部分。
每个Logger都附属于一个LoggerContext
。LoggerContext
负责创建Logger,并将它们组织成类似于树形结构的层次结构。
Logger对象都是需要命名的实体。它们的名称是区分大小写的,并且遵循层次化命名规则:
如果一个日志记录器的名称加上点号是另一个日志记录器名称的前缀,则前者被认为是后者的父级。
如果两者之间没有其他父级,则前者被认为是后者的直接父级。
比如,名为"com.foo"
的logger是"com.foo.Bar"
的父级。
同样的,"java"
是"java.util"
的父级,并且"java.util"
是"java.util.Vector"
的父级。
对大多数开发人员来说,这个命名方案应该是相同的。
root logger位于Logger层次结构的顶部。它非常特殊,因为它是每个Logger层次结构在其创建时的一部分。
我们可以通过以下方式获取root logger:
Logger rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
其他的所有Logger也可以通过org.slf4j.LoggerFactory的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);
}
日志生效级别
Logger可以分配级别。日志级别集合(TRACE、DEBUG、INFO、WARN 和 ERROR)被定义在ch.qos.logback.classic.Level
类中。
注意,在logback中,Level
类是final的,不能被子类化,这是因为logback提供了一种更加灵活的方法,即使用Marker
对象来实现类似的功能。
如果给定的Logger没有指定级别,则它会从最近的具有指定级别的父级中继承一个级别。更正式地说法是:
给定的Logger L的有效日志级别等于其层次结构中从L本身开始向上递归到根日志记录器的第一个非空日志级别。
为了保证所有的Logger最终都会继承一个日志级别, root logger始终拥有一个指定的日志级别,默认值为DEBUG。
以下是四个示例,它们具有不同的分配级别和根据级别继承规则得出的有效(继承)级别。
Example 1 只有root分配了一个DEBUG级别,X、X.Y和X.Y.Z继承了root的DEBUG级别。
Logger name | Assigned level | Effective level |
---|---|---|
root | DEBUG | DEBUG |
X | none | DEBUG |
X.Y | none | DEBUG |
X.Y.Z | none | DEBUG |
Example 2 所有Logger都分配了自己的级别,级别继承不起作用。
Logger name | Assigned level | Effective level |
---|---|---|
root | ERROR | ERROR |
X | INFO | INFO |
X.Y | DEBUG | DEBUG |
X.Y.Z | WARN | WARN |
Example 3 root、X和X.Y.Z分别被分配了DEBUG,INFO和ERROR级别,X.Y继承其直接父级X的级别INFO。
Logger name | Assigned level | Effective level |
---|---|---|
root | DEBUG | DEBUG |
X | INFO | INFO |
X.Y | none | INFO |
X.Y.Z | ERROR | ERROR |
Example 4 root和X分别被分配了DEBUG和INFO级别,X.Y和X.Y.Z继承拥有级别的最近父级X的级别INFO。
Logger name | Assigned level | Effective level |
---|---|---|
root | DEBUG | DEBUG |
X | INFO | INFO |
X.Y | none | INFO |
X.Y.Z | none | INFO |
日志打印方法和基本选择规则
根据定义,日志打印方法决定了日志请求的级别。比如,如果L
是一个Logger实例,那么语句L.info("..")
就是一条日志请求,它的级别是INFO。
如果一个日志的请求级别高于或等于它的有效级别,这个日志请求就是启用的,否则就是禁用的。
根据之前描述的规则,如果一个Logger没有分配级别,它将从最近的具有指定级别的父级中继承一个级别。这个规则总结如下:
向有效级别为q的Logger发出级别为p的日志请求,如果p >= q,则启用该请求。
这是logback的核心。日志的级别被定义排序为:TRACE < DEBUG < INFO < WARN < ERROR
。
下表可以更形象的展示基本选择规则的工作方式,垂直标题显示了Logger的请求级别(由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 |
下面是一个示例,展示了基本选择规则的工作方式。
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");
获取Loggers
通过调用LoggerFactory.getLogger
方法,传入相同的名称,总是返回一个指向同一logger对象的引用。比如在下面代码中
Logger x = LoggerFactory.getLogger("wombat");
Logger y = LoggerFactory.getLogger("wombat");
x
和 y
都指向同一logger对象。
因此,我们创建一个Logger对象后,不需要通过引用传递就可以获取到相同的Logger对象。不同于生物学的父母总是先于孩子出现,logback的Logger可以任何顺序创建和配置。
特别需要指出,即使一个Logger晚于他的所有子级创建,它也能找到并链接它的所有子级。
Logback环境的配置通常在应用程序初始化时进行。优选的方式是通过读取配置文件进行配置,稍后将进行讨论。
Logback提供了简单的方法使得开发者可以根据软件组件来命名Logger。最为直接有效的命名方式就是在每个类中使用当前类的完全限定类名作为Logger的名称去实例化一个Logger对象,
由于日志输出带有生成日志的Logger的名称,因此这种命名策略可以轻松识别日志消息的来源。Logback并不限制Logger的名称,作为开发人员,您可以随意命名日志记录器。
目前来讲,将Logger命名为其所在类的完全限定类名似乎是最有效的通用策略。