1. logback的体系结构
1.1. Logback的基本结构充分通用, 可应用于各种不同环境。目前, logback分为三个模块: Core、Classic和Access。
1.2. Core模块是其他两个模块的基础。Classic模块扩展了core模块。Classic模块相当于log4j的显著改进版。Logback-classic直接实现了SLF4J API, 因此你可以在logback与其他记录
系统如log4j和java.util.logging(JUL)之间轻松互相切换。Access模块与Servlet容器集成, 提供HTTP访问记录功能。
2. Logger、Appender和Layout
2.1. Logback建立于三个主要类之上: Logger、Appender和Layout。这三种组件协同工作, 使开发者可以按照消息类型和级别来记录消息, 还可以在程序运行期内控制消息的输出格式
和输出目的地。
2.2. Logger类是logback-classic模块的一部分, 而Appender和Layout接口来自logback-core。作为一个多用途模块, logback-core不包含任何logger。
3. Logger上下文
3.1. 任何比System.out.println高级的记录API的第一个也是最重要的优点便是能够在禁用特定记录语句的同时却不妨碍输出其他语句。这种能力源自记录隔离(space), 即所有各
种记录语句的隔离是根据开发者选择的条件而进行分类的。在logback-classic里, 这种分类是logger固有的。各个logger都被关联到一个LoggerContext, LoggerContext负责制造logger, 也负责以树结构排列各logger。
4. 获取Logger
4.1. 用同一名字调用LoggerFactory.getLogger方法所得到的永远都是同一个logger对象的引用。
4.2. x和y指向同一个logger对象。
Logger x = LoggerFactory.getLogger("wombat");
Logger y = LoggerFactory.getLogger("wombat");
4.3. Logback简化了logger命名, 方法是在每个类里初始化logger, 以类的全限定名作为logger名。这种定义logger的方法即有用又直观。由于记录输出里包含logger名, 这种命名方法很容易确定记录消息来源。
4.4. Logback不限制logger名, 你可以随意命名logger。然而, 目前已知最好的策略是以logger所在类的名字作为logger名称。
5. Level层次化的命名规则
5.1. Logger是命名了的实体。它们的名字大小写敏感且遵从下面的层次化的命名规则: 如果logger的名称带上一个点号后是另外一个logger的名称的前缀, 那么, 前者就被称为后者的祖先。如果logger与其后代logger之间没有其他祖先, 那么, 前者就被称为子logger之父。
5.2. 根logger位于logger等级的最顶端, 它的特别之处是它是每个层次等级的共同始祖。如同其他logger, 根logger可以通过其名称取得, 如下所示:
Logger rootLogger1 = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
Logger rootLogger2 = (Logger) LoggerFactory.getLogger("ROOT");
5.3. 例子
5.3.1. 新建一个名为LogbackArchitecture的Java工程, 添加相关jar包
5.3.2. 编写LoggerGradation.java
package com.zr.rct;
import java.util.List;
import ch.qos.logback.classic.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
public class LoggerGradation {
public static void main(String[] args) {
LoggerFactory.getLogger("com.zr.zrsc.Shopping");
LoggerFactory.getLogger("net.zr.zrxxw.Article");
LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
/**
* logger列表
*/
List<Logger> loggers = lc.getLoggerList();
for (Logger logger : loggers) {
System.out.println(logger.getName());
}
}
}
5.3.3. 运行结果
5.3.4. 层次结构
5.4. 各个logger都被关联到一个LoggerContext, LoggerContext负责制造logger, 也负责以树结构排列各logger。
6. Level类
6.1. Level类位于ch.qos.logback.classic包下, 并且是最终类, 不能被继承。
6.2. Level类定义了几个日志级别: ERROR、WARN、INFO、DEBUG和TRACE。
6.3. Level类还有2个特殊的日志级别: OFF和ALL。
6.4. 日志级别都分配了相应的整数, OFF的整数值是Integer.MAX_VALUE, ERROR的整数值是40000, WARN的整数值是30000, INFO的整数值是20000, DEBUG的整数值是10000, TRACE的整数值是5000, ALL的整数值是Integer.MIN_VALUE。
7. Level级别继承
7.1. Logger可以被分配级别。级别包括: TRACE、DEBUG、INFO、WARN和ERROR。如果logger没有被分配级别, 那么它将从有被分配级别的最近的祖先那里继承级别。
7.2. 为确保所有logger都能够最终继承一个级别, 根logger总是有级别, 默认情况下, 这个级别是DEBUG。
7.3. 仅根logger被分配了级别。根级别值DEBUG被其他logger, X、X.Y和X.Y.Z继承。
7.4. 所有logger都被分配了级别。级别继承不发挥作用。
7.5. 根logger、X和X.Y.Z分别被分配了DEBUG、INFO和ERROR级别。X.Y从其父X继承级别。
7.6. 根logger和X分别被分配了DEBUG和INFO级别。X.Y和X.Y.Z从其最近的父X继承级别, 因为X被分配了级别。
7.7. 例子
7.7.1. 编写LoggerLevelGradation.java
package com.zr.rct;
import java.util.List;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
public class LoggerLevelGradation {
public static void main(String[] args) {
LoggerFactory.getLogger("com.zr.zrsc.Shopping");
LoggerFactory.getLogger("net.zr.zrxxw.Article");
LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
List<Logger> loggers = lc.getLoggerList();
System.out.println("日志名\t日志级别\t有效级别");
defaultLevel(loggers);
}
public static void defaultLevel(List<Logger> loggers) {
for (Logger logger : loggers) {
System.out.println(logger.getName() + " " + logger.getLevel() + " " + logger.getEffectiveLevel());
}
}
public static void comInfoLevel(List<Logger> loggers) {
Logger rootLogger = (Logger) LoggerFactory.getLogger("com");
rootLogger.setLevel(Level.INFO);
for (Logger logger : loggers) {
System.out.println(logger.getName() + " " + logger.getLevel() + " " + logger.getEffectiveLevel());
}
}
public static void netRjbdWarnLevel(List<Logger> loggers) {
Logger rootLogger = (Logger) LoggerFactory.getLogger("net.zr");
rootLogger.setLevel(Level.WARN);
for (Logger logger : loggers) {
System.out.println(logger.getName() + " " + logger.getLevel() + " " + logger.getEffectiveLevel());
}
}
public static void rootAllLevel(List<Logger> loggers) {
Logger rootLogger = (Logger) LoggerFactory.getLogger("ROOT");
rootLogger.setLevel(Level.ALL);
for (Logger logger : loggers) {
System.out.println(logger.getName() + " " + logger.getLevel() + " " + logger.getEffectiveLevel());
}
}
public static void rootOffLevel(List<Logger> loggers) {
Logger rootLogger = (Logger) LoggerFactory.getLogger("ROOT");
rootLogger.setLevel(Level.OFF);
for (Logger logger : loggers) {
System.out.println(logger.getName() + " " + logger.getLevel() + " " + logger.getEffectiveLevel());
}
}
}
7.7.2. 都不设置日志级别, 默认使用根logger的日志级别
7.7.3. 日志com设置info日志级别, com.zr, com.zr.zrsc和com.zr.zrsc.Shopping的日志级别都是null, 但是它们的有效级别继承自com, 是info。
7.7.4. 日志net.zr设置warn日志级别, net.zr.zrxxw和net.zr.zrxxw.Article的日志级别都是null, 但是它们的有效级别继承自net.zr, 是warn。net是net.zr的父亲, 它的日志级别是null, 有效日志级别继承自根日志级别是debug。
7.7.5. 根日志级别设置为all。
7.7.6. 根日志级别设置为off。
8. 打印方法和基本选择规则
8.1. 根据定义, 打印方法决定记录请求的级别。例如, 如果L是一个logger实例, 那么, 语句L.info("..")是一条级别为INFO的记录语句。
8.2. 记录请求的级别在高于或等于其logger的有效级别时被称为被启用, 否则, 称为被禁用。如前所述, 没有被分配级别的logger将从其最近的祖先继承级别。
8.3. 记录请求级别为p, 其logger的有效级别为q, 只有则当p>=q时, 该请求才会被执行。该规则是logback的核心。级别排序为:TRACE < DEBUG < INFO < WARN < ERROR。
8.4. 下表显示了选择规则是如何工作的。行头是记录请求的级别p。列头是logger的有效级别q。行(请求级别)与列(有效级别)的交叉部分是按照基本选择规则得出的布尔值。
8.5. 例子
8.5.1. 编写LoggerPrint.java
package com.zr.rct;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
public class LoggerPrint {
public static void main(String[] args) {
Logger rootLogger = (Logger) LoggerFactory.getLogger("ROOT");
Logger comLogger = (Logger) LoggerFactory.getLogger("com.rjbd.hnsc.Shopping");
Logger netLogger = (Logger) LoggerFactory.getLogger("net.rjbd.rjbdw.Article");
netWarn(rootLogger);
comLogger.error("com.zr.zrsc.Shopping error");
comLogger.warn("com.zr.zrsc.Shopping warn");
comLogger.info("com.zr.zrsc.Shopping info");
comLogger.debug("com.zr.zrsc.Shopping debug");
comLogger.trace("com.zr.zrsc.Shopping trace");
netLogger.error("net.zr.zrxxw.Article error");
netLogger.warn("net.zr.zrxxw.Article warn");
netLogger.info("net.zr.zrxxw.Article info");
netLogger.debug("net.zr.zrxxw.Article debug");
netLogger.trace("net.zr.zrxxw.Article trace");
}
public static void comInfo(Logger comLogger) {
comLogger.setLevel(Level.INFO);
}
public static void netWarn(Logger netLogger) {
netLogger.setLevel(Level.WARN);
}
public static void rootAll(Logger rootLogger) {
rootLogger.setLevel(Level.ALL);
}
public static void rootOff(Logger rootLogger) {
rootLogger.setLevel(Level.OFF);
}
}
8.5.2. 日志com设置为info, 打印信息
8.5.3. 日志net设置为warn, 打印信息
8.5.4. 根日志设置为all, 打印信息
8.5.5. 根日志设置为off, 关闭所有日志信息。
9. Appender和Layout
9.1. 在logback里, 一个输出目的地称为一个appender。目前有控制台、文件、远程套接字服务器、MySQL、PostreSQL、Oracle和其他数据库和远程UNIX Syslog守护进程等多种appender。
9.2. 一个logger可以被关联多个appender。方法addAppender为指定的logger添加一个appender。对于logger的每个启用了的记录请求, 都将被发送到logger里的全部appender及继承的appender。
9.3. Logger L的记录语句的输出会发送给L及其祖先的全部appender。这就是"appender叠加性"的含义。然而, 如果logger L的某个祖先P设置叠加性标识为false, 那么, L的输出会发送给L与P之间(含P)的所有appender, 但不会发送给P的任何祖先的appender。Logger的叠加性默认为true。
9.4. 示例
9.5. 有些用户希望不仅可以定制输出目的地, 还可以定制输出格式。这时为appender关联一个layout即可。Layout负责根据用户意愿对记录请求进行格式化, appender负责将格式化后的输出发送到目的地。PatternLayout是标准logback发行包的一部分, 允许用户按照类似于C语言的printf函数的转换模式设置输出格式。
9.6. 例如
9.6.1. 转换模式"%-4relative [%thread] %-5level %logger{32} - %msg%n"。
9.6.2. 第一个字段是自程序启动以来的逝去时间, 单位是毫秒。
9.6.3. 第二个地段发出记录请求的线程。
9.6.4. 第三个字段是记录请求的级别。
9.6.5. 第四个字段是与记录请求关联的logger的名称。
9.6.6. "-"之后是请求的消息文字。
9.7. 参数化记录
9.7.1. 因为logback-classic里的logger实现了SLF4J的Logger接口, 某些打印方法可接受多个参数。这些不同的打印方法主要是为了在提高性能的同时尽量不影响代码可读性。
9.7.2. 对于某个Logger, 如下面的代码, 在构造消息参数时有性能消耗, 即把整数i和entry[i]都转换为字符串时, 还有连接多个字符串时。不管消息会不会被记录, 都会造成上述消耗。
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
9.7.3. 一个可行的办法是用测试语句包围记录语句以避免上述消耗, 如下面的代码, 当logger的debug级别被禁用时, 这个方法可以避免参数构造带来的性能消耗。另一方面, 如果logger的DEBUG级别被启用, 那么会导致两次评估logger是否被启用: 一次是isDebugEnabled方法, 一次是debug方法。在实践中, 这种额外开销无关紧要, 因为评估logger所消耗的时间不足实际记录请求所用时间的1%。
if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}
9.7.4. 还有一种基于消息格式的更好的替代方法。如下面的代码, 在评估是否作记录后, 仅当需要作记录时, logger才会格式化消息, 用entry的字符串值替换"{}"。换句话说, 当记录语句被禁用时, 这种方法不会产生参数构造所带来的性能消耗。
Object entry = new SomeObject(); logger.debug("The entry is {}.", entry);
9.8. 例子
9.8.1. 编写LoggerAppender.java
package com.zr.rct;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.FileAppender;
public class LoggerAppender {
public static void main(String[] args) {
Logger comLogger = (Logger) LoggerFactory.getLogger("com.zr.zrsc.Shopping");
Logger netLogger = (Logger) LoggerFactory.getLogger("net.zr.zrxxw.Article");
LoggerContext lc = (LoggerContext)LoggerFactory.getILoggerFactory();
PatternLayout pl = new PatternLayout();
pl.setPattern("%-4relative [%thread] %-5level %logger{32} - %msg%n");
pl.setContext(lc);
pl.start();
FileAppender<ILoggingEvent> fa = new FileAppender<ILoggingEvent>();
fa.setContext(lc);
fa.setFile("log/my.log");
fa.setLayout(pl);
fa.start();
Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(fa);
comLogger.error("com.zr.zrsc.Shopping {}", "错误");
comLogger.warn("com.zr.zrsc.Shopping {}", "警告");
comLogger.info("com.zr.zrsc.Shopping {}", "信息");
comLogger.debug("com.zr.zrsc.Shopping {}", "测试");
comLogger.trace("com.zr.zrsc.Shopping {}", "跟踪");
netLogger.error("net.zr.zrxxw.{} {}", "Article", "错误");
netLogger.warn("net.zr.zrxxw.{} {}", "Article", "警告");
netLogger.info("net.zr.zrxxw.{} {}", "Article", "信息");
netLogger.debug("net.zr.zrxxw.{} {}", "Article", "测试");
netLogger.trace("net.zr.zrxxw.{} {}", "Article", "跟踪");
}
}
9.8.2. 运行结果, 在项目根目录下多了一个log文件夹, log文件夹下生成了一个my.log文件
10. 工作原理
10.1. 取得过滤链(filter chain)的判定结果
10.1.1. 如果TurboFilter链存在, 它将被调用。Turbo filters能够设置一个上下文范围内的临界值, 这个临界值或者表示过滤某些与信息有关(比如Marker、级别、Logger、
消息)的特定事件或者表示与每个记录请求相关联的Throwable。如果过滤链的结果是FilterReply.DENY, 则记录请求被抛弃。如果结果是FilterReply.NEUTRAL, 则继续下一个步, 也就是第二步。如果结果是FilterReply.ACCEPT, 则忽略过第二步, 进入第三步。
10.2. 应用基本选择规则
10.2.1. Logback对logger的有效级别与请求的级别进行比较。如果比较的结果是记录请求被禁用, logback会直接抛弃请求, 不做任何进一步处理。否则, 继续下一步。
10.3. 创建LoggingEvent对象
10.3.1. 记录请求到了这一步后, logback会创建一个ch.qos.logback.classic.LoggingEvent对象, 该对象包含所有与请求相关的参数, 比如请求用的logger、请求级别、消息、请求携带的异常、当前时间、当前线程、执行记录请求的类的各种数据, 还有MDC。注意有些成员是延迟初始化的, 只有当它们真正被使用时才会被初始化。MDC用来为记录请求添加额外的上下文信息。
10.4. 调用appender
10.4.1. 创建了LoggingEvent对象后, logback将调用所有可用appender的doAppend()方法, 这就是说, appender继承logger的上下文。
10.4.2. 所有appender都继承AppenderBase抽象类, AppenderBase在一个同步块里实现了doAppend方以确保线程安全。AppenderBase的doAppender()方法也调用appender
关联的自定义过滤器, 如果它们存在的话。自定义过滤器能被动态地关联到任何appender。
10.5. 格式化输出
10.5.1. 那些被调用了的appender负责对记录事件(LoggingEvent)进行格式化。然而, 有些但不是全部appender把格式化记录事件的工作委托给layout。Layout对
LoggingEvent实例进行格式化, 然后把结果以字符串的形式返回。注意有些appender, 比如SocketAppender, 把记录事件进行序列化而不是转换成字符串, 所
以它们不需要也没有layout。
10.6. 发送记录事件(LoggingEvent)
10.6.1. 记录事件被格式化后, 被各个appender发送到各自的目的地。
10.7. 下图是整个流程的UML图