所有真实的分类都是族谱的。
苏格兰达尔文,
物种的起源
如果不是不可能,任何人都可以纯粹通过阅读来学习一个主题,而不将这些信息应用于具体的问题,从而强迫自己思考所读的内容。而且,我们都学习最好的东西,我们发现自己。
- 计算机程序设计艺术 -
唐纳
Logback的架构
Logback的基本架构足够通用,以适应不同的情况。目前,logback分为三个模块:logback-core,logback-classic和logback-access。
所述
core
模块奠定了其它两个模块的基础。
classic
模块扩展
core
。classic模块对应于log4j的显着改进版本。Logback-classic本身实现了
SLF4J API,
因此您可以在Logback和其他日志记录系统(如JDK 1.4中引入的log4j或java.util.logging(JUL))之间来回切换。第三个称为
access的
模块集成了Servlet容器来提供HTTP访问日志功能。单独的文档涵盖了
访问模块文档
。
在本文的其余部分中,我们将编写“logback”来引用logback-classic模块。
Logger,Appenders和Layouts
Logback建立在三个主要的类上:
Logger
,
Appender
和
Layout
。这三种类型的组件协同工作,使开发人员能够根据消息类型和级别来记录消息,并在运行时控制这些消息的格式和报告位置。
该
Logger
班是的logback-classic模块的一部分。另一方面,
Appender
和
Layout
接口是logback-core的一部分。作为通用模块,logback-core没有logger的概念。
Logger上下文
任何日志记录API优于普通的优势
System.out.println
在于它能够禁用某些日志语句,同时允许其他人无阻碍地进行打印。这种能力假定日志空间,即所有可能的日志语句的空间,按照一些开发人员选择的标准进行分类。在logback-classic中,这种分类是logger的固有部分。每一个logger都被连接到一个
LoggerContext
负责制造记录器的记录器上,并且将它们安排在层次结构树中。
Logger是命名实体。他们的名字是区分大小写的,他们遵循分层的命名规则:
命名层次结构
如果一个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 {
// 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。
以下是根据级别继承规则具有各种分配的级别值以及由此产生的有效(继承)级别的四个示例。
例1
在上面的例子1中,只有根logger被分配了一个级别。这个级别值,
DEBUG
被其他logger继承
X
,
X.Y
并且
X.Y.Z
例2
在上面的例子2中,所有的logger都有一个分配的等级值。等级继承不起作用。
例3
另外,在上述实施例3中,logger
root
,
X
和
X.Y.Z
被分配了等级
DEBUG
,
INFO
和
ERROR
分别。记录器
X.Y
从其父项继承其级别值
X
。
例4
在上面的例子4中,logger
root
和
X
和分别被分配了水平
DEBUG
和
INFO
。logger
X.Y
并
X.Y.Z
从最近的
X
具有指定级别的父级继承它们的级别值。
打印方法和基本的规则选择
根据定义,打印方法确定记录请求的级别。例如,如果
L
是一个logger实例,则该语句
L.info("..")
是级别为INFO的记录语句。
如果记录请求的级别高于或等于其logger的有效级别,则说明该记录请求被
启用
。否则,该请求被认为是被
禁用的
。如前所述,没有指定级别的logger将从其最近的祖先继承。这条规则总结如下。
基本选择规则
如果
p> = q
,则向具有有效级别
q
的
logger
发出的级别为
p的
日志请求被启用 。
这个规则是logback的核心。它假设层次的排列如下:
TRACE < DEBUG < INFO < WARN < ERROR
。
更为直观地说,这里是选择规则的工作原理。在下表中,垂直标题显示记录请求的级别,由
p
指定,而水平标题显示logger的有效级别,由
q
指定。行(级别请求)和列(有效级别)的交集是基本选择规则产生的布尔值。
这是一个基本的选择规则的例子。
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");
检索Logger
例如,在
Logger x = LoggerFactory.getLogger("wombat");
Logger y = LoggerFactory.getLogger("wombat");
x
和
y
引用
完全相同
的logger对象。
因此,可以配置一个logger,然后在代码中的其他位置检索相同的实例,而无需传递引用。与亲生父母的根本矛盾,父母总是先于孩子,可以按任意顺序创建和配置登录logback。特别是,一个“父母”logger即使在它们后面被实例化,也会找到并链接到它的后代。
logback环境的配置通常在应用程序初始化时完成。首选的方法是读取配置文件。这个方法将很快讨论。
Logback通过
软件组件
可以很容易地给logger命名。这可以通过在每个类中实例化一个logger来完成,logger名称等于该类的完全限定名称。这是定义logger的有用和直接的方法。由于日志输出带有生成logger的名称,因此该命名策略可以轻松识别日志消息的来源。然而,这只是一种可能的,尽管是通用的命名logger的策略。Logback不限制可能的logger集合。作为一名开发人员,您可以根据自己的意愿自由选择logger名称。
尽管如此,在他们所在的class之后命名伐木者似乎是目前已知的最好的一般策略。
Appenders和Layouts
根据logger选择性地启用或禁用记录请求的能力只是很小一部分功能。Logback允许记录请求打印到多个目的地。在logback中,输出目标被称为appender。目前,控制台,文件,远程套接字服务器,MySQL,PostgreSQL,Oracle和其他数据库,JMS以及远程UNIX系统日志守护程序都存在appender。
可以将多个appender连接到记录器。
该
addAppender
方法将appender添加到给定的logger。给定logger的每个启用的记录请求将被转发给该logger中的所有appender以及层次结构中较高的appender。换句话说,appender是从logger层次继承的。例如,如果控制台appender被添加到根日志logger,则所有启用的日志记录请求将至少在控制台上打印。如果另外一个文件appender被添加到logger,说
L
,然后启用记录请求
L
和
L
的孩子将打印在一个文件
和
在控制台上。可以重写此默认行为,以便通过将logger的可加性标志设置为false来使appender累积不再具有可加性。
以下总结了Appender可加性的规则。
Appender的可加性
记录器
L
的日志语句的输出将传递给
L
及其祖先中的所有appender 。这就是“appender 可加性”的意思。
但是,如果记录器
L
的祖先(比如说
P
)的可加性标志设置为false,那么
L
的输出将被引导到
L中的
所有appender 和它的祖先直到并包括
P,
但不包括任何appender
P的
祖先。
记录器默认情况下将其可加性标志设置为true。
下表显示了一个例子:
多数情况下,用户希望不仅自定义输出目标,而且还要自定义输出格式。这是通过将
layout
与appender 关联来实现的。该layout负责根据用户的意愿格式化记录请求,而appender负责将格式化的输出发送到其目的地。作为
PatternLayout
标准的logback分发的一部分,用户可以根据类似于C语言
printf
函数的转换模式来指定输出格式。
例如,带有转换模式“%-4relative [%thread]%-5level%logger {32} - %msg%n”的PatternLayout会输出类似于:
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world。
第一个字段是程序启动以来经过的毫秒数。第二个字段是进行日志请求的线程。第三个字段是日志请求的级别。第四个字段是与日志请求关联的记录器的名称。' - '后面的文本是请求的消息。
参数化日志记录
对于一些记录器
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
。另一方面,如果logger启用了DEBUG级别,则会产生评估logger是否启用两次的成本:一次进入
debugEnabled
和一次进入
debug
。在实践中,这种开销是微不足道的,因为评估记录器的时间少于实际记录请求所花费的时间的1%。
更好的选择
基于消息格式存在一个方便的选择。假设
entry
是一个对象,你可以写:
Object
entry
=
new
SomeObject
();
logger.debug("The entry is {}.", entry);
只有在评估是否记录之后,并且只有当决定是肯定的时候,logger实现才会格式化消息并用字符串值替换“{}”对
entry
。换句话说,当日志语句被禁用时,这种形式不会产生参数构造的代价。
以下两行将产生完全相同的输出。但是,如果
禁用了
日志记录语句,则第二个变体的性能将比第一个变体至少高出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框架在用户调用logger的打印方法时所采取的步骤。现在让我们来分析当用户调用
info()
一个名为
com.wombat
的logger的方法时logback所要采取的步骤 。
1.获取过滤器链决定
如果存在,则
TurboFilter
链被调用。涡轮过滤器可以设置一个上下文范围的阈值,或过滤掉基于诸如信息的某些事件
Marker
,
Level
,
Logger
,消息,或
Throwable
与每个日志记录请求相关联。如果过滤器链的回复是
FilterReply.DENY
,则记录请求被丢弃。如果是
FilterReply.NEUTRAL
,那么我们继续下一步,即第二步。如果答复是
FilterReply.ACCEPT
,我们跳过下一步,直接跳到第三步。
2.应用
基本选择规则
在此步骤中,logback将logger的有效级别与请求的级别进行比较。如果根据此测试日志记录请求被禁用,则logback将丢弃该请求而不做进一步处理。否则,就进入下一步。
3.创建一个
LoggingEvent
对象
如果请求在先前的logger中存活,logback将创建一个
ch.qos.logback.classic.LoggingEvent
包含请求的所有相关参数的对象,例如请求的logger,请求级别,消息本身,可能已经与请求一起传递的异常,当前时间,当前线程,关于发出日志请求的类的各种数据以及
MDC
。请注意,这些字段中的一部分只是在实际需要时才被初始化。将
MDC
被用来装饰用额外的上下文信息的日志记录请求。MDC将在
下一章
讨论。
4.调用appenders
创建
LoggingEvent
对象后,logback将调用
doAppend()
所有适用appender 的方法,即从logger上下文继承的appender。
所有随logback发行版一起提供的appender都会将
AppenderBase
实现此
doAppend
方法的抽象类 扩展到 同步块中,以确保线程安全。如果存在任何这样的过滤器,该
doAppend()
方法
AppenderBase
也调用附加到附加器的自定义过滤器。可以动态连接到任何appender的自定义过滤器将在
单独的章节中介绍
。
5.格式化输出
被调用的appender负责格式化记录事件。但是,一些(但不是全部)appender将格式化记录事件的任务委托给布局。布局格式化
LoggingEvent
实例并以String 形式返回结果。请注意,某些appender(例如
SocketAppender
,)不会将日志记录事件转换为字符串,而是将其序列化。因此,他们没有也不需要layout。
6.发送
LoggingEvent
记录事件完全格式化后,每个appender将其发送到目的地。
这是一个序列UML图,以显示一切工作。您可能想点击图片来显示更大的版本。
性能
经常引用的反对记录日志的论据之一是其计算成本。这是一个合理的问题,因为即使是中等规模的应用程序也可能产生数千个日志请求。我们的大部分开发工作都花费在测量和调整logback的性能上。独立于这些努力,用户仍应该意识到以下性能问题。
1.记录完全关闭时的记录性能
您可以通过将根记录器的级别设置
Level.OFF
为最高级别来完全关闭日志记录。当日志记录完全关闭时,日志请求的开销包括方法调用和整数比较。在3.2Ghz Pentium D机器上,这个成本通常在20纳秒左右。
但是,任何方法调用都涉及参数构建的“隐藏”成本。例如,对于某些记录器的
x
书写,
x.debug("Entry number: " + i + "is " + entry[i]);
引发构造消息参数的成本,即转换整数二者
i
并
entry[i]
为一个字符串,和连接中间的字符串,而不管该消息是否将被记录或没有。
参数构建的成本可能相当高,并且取决于所涉及参数的大小。为了避免参数构建的成本,您可以利用SLF4J的参数化日志:
x.debug("Entry number: {} is {}", i, entry[i]);
这个变体不会产生参数构造的代价。与之前的
debug()
方法调用相比, 速度会更快。只有当记录请求被发送给附属的appender时,才会格式化消息。而且,对消息格式化的组件进行了高度优化。
尽管上述将日志语句置于严格的循环中,即非常频繁地被调用的代码是一个双输入的提议,可能会导致性能下降。即使日志记录已关闭,记录紧密的循环也会减慢应用程序的运行速度,并且如果打开日志记录,将会产生大量(因此无用的)输出。
2.打开日志记录时决定是否记录日志的性能。
在logback中,不需要走记录器层次结构。记录器在创建时知道其有效等级(即其等级,一旦考虑了等级继承)。如果父记录器的等级被改变,则联系所有的儿童记录器以注意改变。因此,在接受或拒绝基于有效水平的请求之前,记录器可以做出准瞬时决定,而不需要咨询其祖先。
3.实际记录(格式化和写入输出设备)
这是格式化日志输出并将其发送到目标目标的成本。在这里,我们再次认真地努力使layout(格式化程序)尽可能快地执行。appenders也是如此。登录到本地计算机上的文件时,实际记录的典型成本大约为9到12微秒。登录到远程服务器上的数据库时,上升到几毫秒。
虽然功能丰富,但logback的首要设计目标之一是执行速度,这是仅次于可靠性的要求。一些logback组件已经被重写了好几次以提高性能。