一、体系结构

 logback的体系结构 
Logback的基本结构充分通用,可应用于各种不同环境。目前,logback分为三个模块:Core、Classic和Access。 
Core模块是其他两个模块的基础。Classic模块扩展了core模块。Classic模块相当于log4j的显著改进版。Logback-classic直接实现了SLF4J API,因此你可以在logback与其他记录系统如log4j和java.util.logging (JUL)之间轻松互相切换。Access模块与Servlet容器集成,提供HTTP访问记录功能。 
 
本文中,“logback”代表logback-classic模块。 

 

 Logger、Appender和Layout 
Logback建立于三个主要类之上:Logger(消息类型和级别来记录消息)、Appender(输出目的)和Layout(输出格式)。这三种组件协同工作,使开发者可以按照消息类型和级别来记录消息,还可以在程序运行期内控制消息的输出格式和输出目的地。 
 
Logger类是logback-classic模块的一部分,而Appender和Layout接口来自logback-core。作为一个多用途模块,logback-core不包含任何logger

 

Logger上下文 
任何比System.out.println高级的记录API的第一个也是最重要的优点便是能够在禁用特定记录语句的同时却不妨碍输出其他语句。这种能力源自记录隔离(space)——即所有各种记录语句的隔离——是根据开发者选择的条件而进行分类的。在logback-classic里,这种分类是logger固有的。各个logger都被关联到一个LoggerContext,LoggerContext负责制造logger,也负责以树结构排列各logger。 

 

Logger是命名了的实体。它们的名字大小写敏感且遵从下面的层次化的命名规则:

命名层次:

如果logger的名称带上一个点号后是另外一个logger的名称的前缀,那么,前者就被称为后者的祖先。如果logger与其后代logger之间没有其他祖先,那么,前者就被称为子logger之父。 
 
比如,名为“com.foo"”的logger是名为“com.foo.Bar”之父。同理,“java”是“java.util"”之父,也是“java.util.Vector”的祖先。 
 
根logger位于logger等级的最顶端,它的特别之处是它是每个层次等级的共同始祖。如同其他各logger,根logger可以通过其名称取得,如下所示:

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

其他所有logger也通过org.slf4j.LoggerFactory 类的静态方法getLogger取得。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);  
 
}

 

有效级别(Level)即级别继承 
Logger可以被分配级别。级别包括:TRACE、DEBUG、INFO、WARN和ERROR,定义于ch.qos.logback.classic.Level类。注意在logback里,Level类是final的,不能被继承,Marker对象提供了更灵活的方法。 
 
如果logger没有被分配级别,那么它将从有被分配级别的最近的祖先那里继承级别。更正式地说: logger L的有效级别等于其层次等级里的第一个非null级别,顺序是从L开始,向上直至根logger。

为确保所有logger都能够最终继承一个级别,根logger总是有级别,默认情况下,这个级别是DEBUG。

下面的四个例子包含各种分配级别值和根据级别继承规则得出的最终有效(继承)级别。

Logger名           分配级别                  有效级别 

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继承。

 

root                  DEBUG                   DEBUG 

X                      INFO                      INFO 

X.Y                   none                      INFO 

X.Y.Z                none                      none 

例4里,根logger和X分别被分配了DEBUG和INFO级别。X.Y和X.Y.Z从其最近的父X继承级别,因为X被分配了级别。

 

打印方法和基本选择规则 
根据定义,打印方法决定记录请求的级别。例如,如果L是一个logger实例,那么,语句L.info("..")是一条级别为INFO的记录语句。 
 
记录请求的级别在高于或等于其logger的有效级别时被称为被启用,否则,称为被禁用。如前所述,没有被分配级别的logger将从其最近的祖先继承级别。该规则总结如下: 
基本选择规则 
记录请求级别为p,其logger的有效级别为q,只有则当p>=q时,该请求才会被执行。  
该规则是logback的核心。级别排序为:TRACE < DEBUG < INFO < WARN < ERROR。  
下表显示了选择规则是如何工作的。行头是记录请求的级别p。列头是logger的有效级别q。行(请求级别)与列(有效级别)的交叉部分是按照基本选择规则得出的布尔值。

 

      

logback ExtendedWhitespaceThrowableProxyConverter是什么 logback-classic_Layout


下面是基本选择规则的例子。 
// 取得名为"com.foo"的logger实例 
Logger logger = LoggerFactory.getLogger("com.foo"); // 设其级别为INFO 
logger.setLevel(Level.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.");  
// 名为"com.foo.Bar"的logger实例barlogger, 从"com.foo"继承级别 // 因此下面的请求有效,因为INFO >= INFO. 
barlogger.info("Located nearest gas station.");  
// 该请求无效,因为DEBUG < INFO. 
barlogger.debug("Exiting gas station search"); 
译者注:上例的logger.setLevel(Level.INFO)无效。org.slf4j.Logger没有setLevel()方法,ch.qos.logback.classic.Logger有此方法。

 

获取Logger 
用同一名字调用LoggerFactory.getLogger方法所得到的永远都是同一个logger对象的引用。 
例如, 
Logger x = LoggerFactory.getLogger("wombat"); Logger y = LoggerFactory.getLogger("wombat"); 
x和y指向同一个logger对象。  
因此,可以配置一个logger,然后从其他地方取得同一个实例,不需要到处传递引用。生物学里的父母总是先于其孩子,而logback不同,它可以以任何顺序创建和配置logger。特别的是,即使“父”logger是在其后代初始化之后才初始化的,它仍将查找并链接到其后

代们。 
 
通常是在程序初始化时对logback环境进行配置。推荐用读配置文件类进行配置。稍后会讲这种方法。 
 
Logback简化了logger命名,方法是在每个类里初始化logger,以类的全限定名作为logger名。这种定义logger的方法即有用又直观。由于记录输出里包含logger名,这种命名方法很容易确定记录消息来源。Logback不限制logger名,你可以随意命名logger。 
 
然而,目前已知最好的策略是以logger所在类的名字作为logger名称。 

 

Appender和Layout 
有选择性地启用或禁用记录请求仅仅是logback功能的冰山一角。Logback允许打印记录请求到多个目的地。在logback里,一个输出目的地称为一个appender。目前有控制台、文件、远程套接字服务器、MySQL、PostreSQL、Oracle和其他数据库、JMS和远程UNIX Syslog守护进程等多种appender。 
 
一个logger可以被关联多个appender。 
方法addAppender为指定的logger添加一个appender。对于logger的每个启用了的记录请求,都将被发送到logger里的全部appender及更高等级的appender。换句话说,appender叠加性地继承了logger的层次等级。例如,如果根logger有一个控制台appender,那么所有启用了的请求都至少会被打印到控制台。如果logger L有额外的文件appender,那么,L和L后代的所有启用了的请求都将同时打印到控制台和文件。设置logger的additivity为false,则可以取消这种默认的appender累积行为。 
 
控制appender叠加性的规则总结如下。 
Appender叠加性 
Logger L的记录语句的输出会发送给L及其祖先的全部appender。这就是“appender叠加性”的含义。 
 
然而,如果logger L的某个祖先P设置叠加性标识为false,那么,L的输出会发送给L与P之间(含P)的所有appender,但不会发送给P的任何祖先的appender。Logger的叠加性默认为true。

logback ExtendedWhitespaceThrowableProxyConverter是什么 logback-classic_初始化_02


有些用户希望不仅可以定制输出目的地,还可以定制输出格式。这时为appender关联一个layout即可。Layout负责根据用户意愿对记录请求进行格式化,appender负责将格式化化后的输出发送到目的地。PatternLayout是标准logback发行包的一部分,允许用户按照类似于C语言的printf函数的转换模式设置输出格式。 
例如,转换模式"%-4relative [%thread] %-5level %logger{32} - %msg%n"在PatternLayout里会输出形如:

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

第一个字段是自程序启动以来的逝去时间,单位是毫秒。 第二个地段发出记录请求的线程。 第三个字段是记录请求的级别。

第四个字段是与记录请求关联的logger的名称。 “-”之后是请求的消息文字。

 

参数化记录 
因为logback-classic里的logger实现了SLF4J的Logger接口,某些打印方法可接受多个参数。这些不同的打印方法主要是为了在提高性能的同时尽量不影响代码可读性。 
 
对于某个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的debug级别被禁用时,这个方法可以避免参数构造带来的性能消耗。另一方面,如果logger的DEBUG级别被启用,那么会导致两次评估logger是否被启用:一次是isDebugEnabled方法,一次是debug方法。在实践中,这种额外开销无关紧要,因为评估logger所消耗的时间不足实际记录请求所用时间的1%。

 

更好的替代方法

还有一种基于消息格式的方便的替代方法。假设entry是一个object,你可以编写:

Object entry = new SomeObject();  

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

在评估是否作记录后,仅当需要作记录时,logger才会格式化消息,用entry的字符串值替换"{}"。换句话说,当记录语句被禁用时,这种方法不会产生参数构造所带来的性能消耗。

 

工作原理

介绍过logback的核心组件后,下面描述logback框架在用户调用logger的打印方法时所做的事情。在本例中,用户调用名为com.wombat的logger的info()方法。 
1. 取得过滤链(filter chain)的判定结果 
如果TurboFilter链存在,它将被调用。Turbo filters能够设置一个上下文范围内的临界值,这个临界值或者表示过滤某些与信息有关(比如Marker、级别、Logger、消息)的特定事件,或者表示与每个记录请求相关联的Throwable。如果过滤链的结果是FilterReply.DENY,则记录请求被抛弃。如果结果是FilterReply.NEUTRAL,则继续下一步,也就是第二步。如果结果是FilterReply.ACCEPT,则忽略过第二步,进入第三步。 

2. 应用基本选择规则 

Logback对logger的有效级别与请求的级别进行比较。如果比较的结果是记录请求被禁用,logback会直接抛弃请求,不做任何进一步处理。否则,继续下一步。 

3. 创建LoggingEvent对象 
记录请求到了这一步后,logback会创建一个ch.qos.logback.classic.LoggingEvent对象,该对象包含所有与请求相关的参数,比如请求用的logger、请求级别、消息、请求携带的异常、当前时间、当前线程、执行记录请求的类的各种数据,还有MDC。注意有些成员是延迟初始化的,只有当它们真正被使用时才会被初始化。MDC用来为记录请求添加额外的上下文信息。之后的章节会讨论MDC。

4. 调用appender 
创建了LoggingEvent对象后,logback将调用所有可用appender的doAppend()方法,这就是说,appender继承logger的上下文。 
所有appender都继承AppenderBase抽象类,AppenderBase在一个同步块里实现了doAppend方以确保线程安全。AppenderBase的doAppender()方法也调用appender关联的自定义过滤器,如果它们存在的话。自定义过滤器能被动态地关联到任何appender,另有章节专门讲述它。 

5. 格式化输出 
那些被调用了的appender负责对记录事件(LoggingEvent)进行格式化。然而,有些但不是全部appender把格式化记录事件的工作委托给layout。Layout对LoggingEvent实例进行格式化,然后把结果以字符串的形式返回。注意有些appender,比如SocketAppender,把记录事件进行序列化而不是转换成字符串,所以它们不需要也没有layout。 

6. 发送记录事件(LoggingEvent)

记录事件被格式化后,被各个appender发送到各自的目的地。

logback ExtendedWhitespaceThrowableProxyConverter是什么 logback-classic_Layout_03

性能 

一个关于记录的常见争论是它的计算代价。这种关心很合理,因为即使是中等大小的应用程序也会生成数以千计的记录请求。人们花了很多精力来测算和调整记录性能。尽管如此,用户还是需要注意下面的性能问题。 
1. 记录被彻底关闭时的记录性能 
你可以将根logger的级别设为最高级的Level.OFF,就可以彻底关闭记录。当记录被彻底关闭时,记录请求的消耗包括一次方法调用和一次整数比较。在CPU为3.2Ghz的Pentium D电脑上,一般需要20纳秒。 
但是,任何方法调用都会涉及“隐藏的” 参数构造消耗,例如,对于logger x,

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

把整数i和entry[i]都转换为字符串和连接各字符串会造成消息参数构造消耗,不管消息是否被记录。  
参数构造消耗可以变得非常高,同时也跟参数大小有关。利用SLF4J的参数化记录可以避免这种消耗。

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

这种方式不会造成参数构造消耗。与前面的debug()方法相比,这种方法快得多。只有当请求在被发送给appender时,消息才会被格式化。在格式化的时候,负责格式化消息的组件性能很高,不会对整个过程造成负面影响。格式化1个和3个参数分别需要2和4微妙。 
请注意,无论如何,应当避免在紧密循环里或者非常频繁地调用记录语句,因为很可能降低性能。即使记录被禁用,在紧密循环里作记录仍然会拖慢应用程序,如果记录被启用,就会产生大量(也是无用的)输出。 

2. 当记录启用时,判断是否进行记录的性能

在logback中,logger在被创建时就明确地知道其有效级别(已经考虑了级别继承)。当父logger的级别改变时,所有子logger都会得知这个改变。因此,在根据有效级别去接受或拒绝请求之前,logger能够作出准即时判断,不需要咨询其祖先。 

3. 实际记录(格式化和写入输出设备) 
性能消耗包括格式化肌瘤输出和发送到目的地。我们努力使layout(formatter)和appender都尽可能地快。记录到本地机器的文件里的耗时一般大约在9至12微秒。如果目的地是远程服务器上的数据库时,会增加早几个毫秒。  
尽管功能丰富,logback最首要的一项设计目标就是执行速度,重要程度仅排在可靠性之后。为提高性能,logback的一些组件已经被多次重写。