飞道的博客

最全最详细Logback高级讲解(上)

349人阅读  评论(0)

前言

在我们日常开发中,毫无疑问,我们需要花费精力去考虑如何将日志打印请求合理的嵌入到我们的程序中。有数据表明,大约有百分之四的代码是用于日志记录。即使是中等大小的应用程序也会嵌入上千行日志记录语句。而且,日志是我们排查 bug ,调试程序的重要依据,所以我们需要学习,并且使用工具来管理这些日志语句。

一 Logback 说明

Logback 目的是作为流行的 log4j 日志框架的继承者。它是由 log4j 的创始人 Ceki Gülcü 设计的。logback 比所有现有的 logging 系统更快,并且占用的内存空间更小。而且,logback 提供了其他日志记录系统所缺少的独特且相当有用的功能。

Logback 的架构可以分为三个模块:logback-core,logback-classic 和 logback-access。logback-core 模块是其他2个模块的基础。logback-classic 扩展了 logback-core,它是对应于 log4j 的显着改良版本。logback-access 模块与 Servlet 容器集成在一起,以提供 HTTP 访问日志功能。我们一般使用 logback-core 和 logback-classic。

使用 Logback-classic 时,除了引入 logback-classic.jar 外,还需要 slf4j-api.jar 和 logback-core.jar。不过如果我们使用 Maven 等构建工具,只需要引入 logback-classic 依赖,会自动引入其他2个依赖。

先简单演示下效果,首先导入依赖 logback-classic。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.nobody</groupId>
    <artifactId>logback-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>

</project>
package com.nobody;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Description
 * @Author Mr.nobody
 * @Date 2021/2/20
 * @Version 1.0
 */
public class Main {
   
    public static void main(String[] args) {
   
        // 定义一个 Logger 记录器对象,名字为Main类的全限定名,即 com.nobody.Main
        Logger logger = LoggerFactory.getLogger(Main.class);
        // 在 info 级别上输出日志 Hello Logback!
        logger.info("Hello Logback!");
    }
}

运行程序后,在控制台打印出了如下日志。根据 logback 的默认配置策略,当未找到默认日志配置文件时,logback 会添加一个 ConsoleAppender(控制台输出器) 关联到根 logger 记录器中。

22:46:18.430 [main] INFO com.nobody.Main - Hello Logback!

请注意,以上示例未明显引用任何 logback 类,是不是很奇妙。其实在大多数情况下,就日志记录而言,需要打印日志的类中仅需要导入 SLF4J 的类即可。通过 LoggerFactory 获取 Logger 日志记录器对象。因为 SLF4J 运用了门面设计模式,屏蔽了底层具体的日志实现框架。因为 logback 完整实现了SLF4J API ,所以我们可以很方便地更换成其它日志系统如 log4j 或 JDK14 Logging,而不用修改代码。

Logback 可以通过使用内置状态系统打印有关其内部状态的信息。我们可以通过一个叫 StatusManager 的组件来访问在 logback 生命周期内发生的重要事件。我们通过调用 StatusPrinter 类的静态方法 print() 来指示 logback 打印其内部状态信息 。

package com.nobody;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.StatusPrinter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main {
   
    public static void main(String[] args) {
   
        Logger logger = LoggerFactory.getLogger(Main.class);
        logger.info("Hello Logback!");

        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        StatusPrinter.print(loggerContext);
    }
}

运行程序,控制台输出如下日志。意思是说没有找到 logback-test.xml 和 logback.xml 配置文件,所以使用默认策略配置了 ConsoleAppender。一个 Appender 可以看成是一个日志输出目的地的类。它有很多种输出目的地,包括控制台,文件,Syslog,TCP Sockets,JMS等等。当然,我们也可以根据自己的具体情况轻松创建自己的Appender。

请注意,如果出现错误,logback 将会自动在控制台上打印其内部状态信息。

22:59:57.169 [main] INFO com.nobody.Main - Hello Logback!
22:59:57,020 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
22:59:57,021 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
22:59:57,021 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.xml]
22:59:57,036 |-INFO in ch.qos.logback.classic.BasicConfigurator@439f5b3d - Setting up default configuration.

我们可以通过以下步骤来往我们的项目中添加日志系统:

  1. 通过 Maven 或 Gradle 添加日志框架依赖
  2. 配置日志配置文件 logback.xml
  3. 在需要打印日志的类中,通过 LoggerFactory 获取 Logger 对象,然后调用它的 debug(),info(),warn() 和 error() 等方法。

二 Logger,Appenders 和 Layouts

Logback 依赖于三个主要类:Logger,Appender 和 Layout。配合这三种类型的组件,能让开发人员能够根据日志消息类型和级别记录日志,并在运行时控制日志格式。

Logger 类是 logback-classic 模块的一部分。而 Appender 和 Layout 接口是 logback-core 模块的一部分。作为通用模块,logback-core 没有 Logger 记录器的概念。

2.1 Logger 说明

在 logback-classic 中,Logger 是有继承关系的。每个单独的 logger 都会关联到一个 LoggerContext,LoggerContext 负责制造 logger, 并将它们按树状结构排列。

logger 记录器是带有名称的 Logger 对象,它们的名称区分大小写,并且遵循层级命名规则。

Logger 名称层次规则
记录器的名称层级规则跟 “.” 有关,它们有父亲或者祖先的关系。
例如命名为 com.nobody 的 logger 是命名为 com.nobody.User 的 logger 的父亲;同理,java 是 java.util 的父亲,但是是 java.util.List 的祖先。

root logger 位于 Logger 层次结构的顶部。我们可以按其名称获取到它,如下所示:

public class Main {
   
    public static void main(String[] args) {
   
        Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
        rootLogger.info("rootLogger:{}", rootLogger.getName()); // 输出 rootLogger:ROOT
    }
}

我们一般用如下方式,将类的全限定名称作为 logger 名称,获取一个 Logger 对象,用于类中打印日志:

// Logger logger = LoggerFactory.getLogger("com.nobody.Main"); 等价
Logger logger = LoggerFactory.getLogger(Main.class);
logger.info("Hello Logback!");

通过 LoggerFactory.getLogger 获取相同名字的 logger 记录器,都是返回同一对象。例如以下返回的三个对象都是同一个对象。

Logger logger1 = LoggerFactory.getLogger(Main.class);
Logger logger2 = LoggerFactory.getLogger("com.nobody.Main");
Logger logger3 = LoggerFactory.getLogger("com.nobody.Main");

2.1.1 有效级别

有效级别也称为日志级别继承规则。我们可以为 logger 分配级别,级别种类有 TRACE,DEBUG,INFO,WARN 和 ERROR,它们在 ch.qos.logback.classic.Level 中定义。在 logback 中,Level 类是 final 的,不能被继承的。

级别按以下顺序排序: TRACE < DEBUG < INFO < WARN < ERROR。

如果没有为给定的 logger 记录器分配一个级别,那么它将从其最接近的祖先那里继承一个已分配的级别。严格上讲,比如一个 logger 的有效级别等于其层次结构中的第一个非空级别,它从其本身开始,在层次结构中向上扩展直到 root logger。

为了确保所有 logger 记录器最终都可以继承到级别,root logger 始终具有分配的级别。默认情况下,此级别是 DEBUG。

Logger name Assigned level Effective level
root DEBUG DEBUG
X none DEBUG
X.Y none DEBUG
X.Y.Z none DEBUG
Logger name Assigned level Effective level
root ERROR ERROR
X INFO INFO
X.Y DEBUG DEBUG
X.Y.Z WARN WARN
Logger name Assigned level Effective level
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z ERROR ERROR
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 的有效级别时被称为被启用,否则,称为被禁用。假设记录请求级别为 p,其 logger 的有效级别为 q,只有则当 p>=q 时,该请求才会被执行。

以下演示 logger 级别的继承关系,日志级别关系决定是否能打印:

package com.nobody;

import ch.qos.logback.classic.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Description
 * @Author Mr.nobody
 * @Date 2021/2/21
 * @Version 1.0
 */
public class LoggerLevelTest {
   
    public static void main(String[] args) {
   

        // 获取一个名为 "com.foo" 的 logger 对象,并且转换为 ch.qos.logback.classic.Logger logger,
        // 这样我们能为它设置级别
        ch.qos.logback.classic.Logger logger =
                (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foo");
        logger.setLevel(Level.INFO);

        // 获取一个名为 "com.boo.Bar" 的 logger 对象,没有设置级别,根据继承关系,继承"com.foo"的 logger 的级别 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");
    }
}

运行程序,控制台输出的日志信息如下:

14:37:58.354 [main] WARN com.foo - Low fuel level.
14:37:58.368 [main] INFO com.foo.Bar - Located nearest gas station.

2.2 Appenders 说明

Logback 允许日志记录请求打印到多个目标目的地。在 logback 中,输出目标称为 appender。目前,存在的 appender 有,控制台,文件,远程 socket 服务,MySQL,PostgreSQL,Oracle 和其他数据库,JM S和 远程 UNIX Syslog 守护程序等等。

一个 logger 可以与多个 appender 绑定。

一个可以执行的日志打印请求,会将日志输出到当前 logger 关联的 appender,并且会根据层级关系输出到所有上层级 logger 所关联的 appender 中。我们可以通过将 logger 的可叠加性标志(additivity flag)设置为 false,覆盖此默认行为,这样不会将日志输出到更高层级 logger 的 appender 中。

假如有个 logger X.Y.Z,默认会将日志输出到 X.Y.Z,X.Y,X 这三个 logger 所关联的 appender 中。如果将 X.Y 这个 logger 的 additivity flag 设置为 false,则 X.Y.Z logger 打印的日志只会输出到 X.Y.Z 和 X.Y。如果将 X.Y.Z logger 的 additivity flag 设置为 false,则 X.Y.Z logger 打印的日志只会输出到 X.Y.Z 。

Logger Name Attached Appenders Additivity Flag Output Targets Comment
root A1 不适用 A1 由于 root logger 位于层次结构的顶部,因此可加性标志不适用于它。
X A2,A3 true A1,A2,A3 root 和 X logger 总的 appender
X.Y none true A1,A2,A3 root 和 X logger 总的 appender
X.Y.Z A4 true A1,A2,A3,A4 root 和 X 和 X.Y.Z logger 总的 appender
K B1 false B1 因为 K 的 additivity flag 设置为 false,所以 appender 只有它自己关联的。
K.J none true B1 因为 K 的 additivity flag 设置为 false,所以层级往上查找只到K,不能到 root。

2.3 Layouts 说明

通常,我们不仅希望自定义日志输出目的地,还希望自定义日志输出格式。这可以通过将 layout 和 appender 相关联来实现。layout 负责根据用户的需求格式化日志记录请求,而 appender 负责将格式化后的日志输出发送到目的地。

例如,如果 patternLayout 配置为 “%-4relative [%thread] %-5level %logger{32} - %msg%n”,将输出类似以下格式日志信息:

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

第一个字段是自程序启动以来经过的毫秒数。第二个字段是发出日志请求的线程名称。第三个字段是日志请求的级别。第四个字段是与日志请求关联的 logger 的名称。“-” 之后的文本是日志信息。

参数化的日志

因为 logback-classic 的 Logger 实现了 SLF4J 的 Logger 接口,所以有些日志打印方法允许使用多个参数。这些带有多个参数的打印方法能提高性能,同时最大程度提高代码可读性。

例如,以下写法,为了构造 debug 方法的 msg 参数,会将整数 i 和 entry[i] 字符串都转换为字符串,并连接中间字符串,从而产生开销。不管最终此行代码是否会打印。

logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

为避免参数构造成本的一种方法,是在打印日志前,判断此 logger 是否开启此级别。

if(logger.isDebugEnabled()) {
    
  logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}

这样,如果我们禁用了 debug 级别,则不会产生 msg 参数构造的开销。但是,如果 logger 开启了 debug 级别,则将产生两次开销,一次是判断 debugEnabled,一次是 debug 打印。实际上,debugEnabled 这种开销微不足道,因为它是实际日志打印请求所花费时间的不到1%。

不过有更好的选择,基于消息格式的方法,例如如下所示:

Object entry = new SomeObject(); 
logger.debug("The entry is {}.", entry);

只有在 debug 打印语句是开启的情况下,logger 才会格式化消息,并用 entry 代替 {}。也就是说,当禁用 debug 级别时,是不会产生 msg 参数构造的开销的。

以下两行将产生完全相同的输出。但是,在禁用日志记录语句的情况下,第二个将比第一个好至少30倍。

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

如果需要传递三个或更多参数,您可以这样写:

Object[] paramArray = {
   newVal, below, above};
logger.debug("Value {} was inserted between {} and {}.", paramArray);

三 Logback 日志打印步骤

当用户调用 logger 的日志打印方法时,logback 框架所采取的步骤是怎么样的呢?现在我们分析当用户调用名为 “com.nobody.UserService” 的 logger 的 info() 方法时,logback 采取的步骤 :

  1. 获得过滤器链决策
    如果存在,那么 TurboFilter 链会被调用。Turbo filters 可以设置一个上下文范围阈值,或过滤掉某些基于信息例如 Marker, Level,Logger,日志消息的事件,或与每个日志记录请求相关联的 Throwable。如果过滤器链的答复是 FilterReply.DENY,那么日志记录请求会被抛弃。如果是 FilterReply.NEUTRAL,则继续进行下一步,即步骤2。如果答复为 FilterReply.ACCEPT,则跳过下一步,直接跳至步骤3。

  2. logger 级别比较规则
    在此步骤中,logback 将 logger 的有效级别与打印请求级别进行比较。如果根据级别规则禁用了日志记录请求,则 logback 将丢弃该请求,(请求打印日志的级别小于 logger 设定的级别则抛弃请求)而无需进一步处理。否则,将继续进行下一步。

  3. 创建一个 LoggingEvent 对象
    如果请求在先前的过滤器中存活下来了,则 logback 将创建一个 ch.qos.logback.classic.LoggingEvent 对象,其中包含请求的所有相关参数,例如请求的 logger,请求级别,日志消息,可能与请求一起传递的异常,当前时间,当前线程,跟发出日志记录请求相关的类的各种数据以及MDC。其中某些字段仅在实际需要时才延迟初始化。MDC用额外的上下文信息(例如请求唯一ID)来装饰日志记录请求。

  4. 调用 appenders
    创建 LoggingEvent 对象后,logback 将调用所有能用的 appenders 的 doAppend() 方法。

  5. 格式化输出
    被调用的 appender 负责格式化日志记录事件。但是,一些(但不是全部) appender 将格式化日志记录事件的任务委托给 layout。layout 会格式化 LoggingEvent 实例,并以字符串形式返回结果。注意,某些 appender,例如 SocketAppender 不会将日志记录事件转换为字符串,而是将其序列化。因此,它们没有 layout,也不需要 layout。

  6. 发出 LoggingEvent
    日志记录事件被完全格式化后,每个 appender 会将其发送到其目的地。

下面是一个显示了全部工作原理的 UML 序列图。

四 logback.xl 配置

通过编程,或使用以 XML 或 Groovy 格式表示的配置脚本都可以达到配置 Logback 的效果。

logback 会按以下步骤来配置自己:

  1. Logback 尝试在 classpath 中找一个名为 logback-test.xml 的文件 。

  2. 如果找不到此类文件,则 logback 尝试在 classpath 中找一个名为 logback.groovy 的文件 。

  3. 如果找不到这样的文件,它将在 classpath 中找一个名为 logback.xml 的文件。

  4. 如果还没有找到这样的文件, ServiceLoader(在JDK 1.6中引入)会通过 META-INF\services\ch.qos.logback.classic.spi.Configurator 加载 com.qos.logback.classic.spi.Configurator 接口的实现类。

  5. 如果以上方法均未成功,则 logback 将使用 BasicConfigurator 进行自动配置,这会将日志输出定向到控制台。

以下我们演示没有配置文件,logback 使用默认配置的效果。

package com.nobody;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyApp1 {
   
    private static final Logger LOGGER = LoggerFactory.getLogger(MyApp1.class);
    public static void main(String[] args) {
   
        LOGGER.info("我是在 MyApp1 类中,使用info级别打印日志");
        User user = new User();
        user.say();
    }
}

package com.nobody;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class User {
   
    private static final Logger LOGGER = LoggerFactory.getLogger(User.class);

    public void say() {
   
        LOGGER.debug("我是在 User 类中,使用debug级别打印日志");
    }
}

运行程序,控制台输出日志如下:

21:20:34.035 [main] INFO com.nobody.MyApp1 - 我是在 MyApp1 类中,使用info级别打印日志
21:20:34.041 [main] DEBUG com.nobody.User - 我是在 User 类中,使用debug级别打印日志

如果找不到配置文件,那么 logback 默认会调用 BasicConfigurator ,创建一个最小化配置。最小化配置由一个关联到根 logger 的 ConsoleAppender 组成。输出用模式为 %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 的 PatternLayoutEncoder 进行格式化。默认情况下,为 root logger 分配了 DEBUG 级别。

Logback 配置文件的语法非常灵活。因为灵活,所以无法用 DTD 或 XML schema 进行定义。尽管如此,可以这样描述配置文件的基本结构:以 <configuration> 开头,后面有零个或多个 <appender> 元素,有零个或多个 <logger> 元素,有最多一个 <root> 元素。

下面我们演示使用 logback.xml 进行配置,首先我们在类路径中创建 logback.xml 文件,此为 logback-demo/src/main/resources/logback.xml ,填入以下内容:

<configuration>
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
		<encoder>
			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
		</encoder>
	</appender>

	<root level="debug">
		<appender-ref ref="STDOUT" />
	</root>
</configuration>

再运行上面的程序,控制台会输出如下信息:

21:45:40.941 [main] INFO  com.nobody.MyApp1 - 我是在 MyApp1 类中,使用info级别打印日志
21:45:40.954 [main] DEBUG com.nobody.User - 我是在 User 类中,使用debug级别打印日志

如果程序在解析配置文件期间发生警告或错误,则 logback 会自动在控制台上打印其内部状态信息。如果在没有警告或错误时,你也希望检查 logback 的内部状态,你可以指示通过调用 StatusPrinter 类的 print() 方法。如下所示:

LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
StatusPrinter.print(lc);

如果你不想在程序中编写打印 logback 的内部状态,那可以在配置文件 configuration 元素的 debug 属性设置为 true,同样也可以在程序启动时打印 logback 内部状态。当然,前提是找到配置文件或者配置文件是格式正确的XML才会输出内部状态。

<configuration debug = "true">
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
		<encoder>
			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
		</encoder>
	</appender>

	<root level="debug">
		<appender-ref ref="STDOUT" />
	</root>
</configuration>

当然,我们也可以通过配置系统属性(logback.configurationFile)的方式,指定 logback 配置文件的位置。属性的值可以是URL,类路径上的资源或应用程序外部文件的路径。

java -Dlogback.configurationFile=/path/to/logback.xml com.nobody.MyApp1

4.1 自动重新加载配置文件

如果开启了自动重新加载配置文件,logback-classic 会扫描配置文件中的更改,并在配置文件更改时自动重新配置自身。在 <configuration> 标签中将 scan 属性设置为 true 即可开启。

当将 scan 属性设置为 true 时,在后台 ReconfigureOnChangeTask 会在单独的线程中运行,它会检查配置文件是否已更改。

由于在编辑配置文件时很容易出错,因此如果最新版本的配置文件具有 XML 语法错误,则它将回退到先前没有 XML 语法错误的配置文件。

<configuration scan="true"> 
  ... 
</configuration> 

默认情况下,每1分钟扫描一次配置文件是否有更改。我们可以设置 <configuration> 标签中的 scanPeriod 属性来指定扫描周期。单位可以为毫秒,秒,分钟或小时。如果未指定时间单位,则时间单位默认为毫秒

<configuration debug = "true" scan="true" scanPeriod="30 seconds">
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
		<encoder>
			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
		</encoder>
	</appender>

	<root level="debug">
		<appender-ref ref="STDOUT" />
	</root>
</configuration>

4.2 在堆栈跟踪中启用包数据

注意从版本1.1.4开始,包装数据默认为禁用。可按如下配置开启包数据:

<configuration packagingData="true">
  ...
</configuration>

当然,也可以在程序中进行配置,如下:

LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
lc.setPackagingDataEnabled(true);

如果开启了,logback 会在输出的堆栈行中显示它是属于哪个 jar 或者哪个类的。此信息由 jar 文件的名称和版本组成,表明堆栈信息来源于此。此机制对于识别软件版本问题非常有用。但是,计算成本相当昂贵,尤其是在经常引发异常的应用程序中。以下演示开启的结果,即多了 [] 括号内的信息。

14:28:48.835 [btpool0-7] INFO  c.q.l.demo.prime.PrimeAction - 99 is not a valid value
java.lang.Exception: 99 is invalid
  at ch.qos.logback.demo.prime.PrimeAction.execute(PrimeAction.java:28) [classes/:na]
  at org.apache.struts.action.RequestProcessor.processActionPerform(RequestProcessor.java:431) [struts-1.2.9.jar:1.2.9]
  at org.apache.struts.action.RequestProcessor.process(RequestProcessor.java:236) [struts-1.2.9.jar:1.2.9]
  at org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:432) [struts-1.2.9.jar:1.2.9]
  at javax.servlet.http.HttpServlet.service(HttpServlet.java:820) [servlet-api-2.5-6.1.12.jar:6.1.12]
  at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:502) [jetty-6.1.12.jar:6.1.12]
  at ch.qos.logback.demo.UserServletFilter.doFilter(UserServletFilter.java:44) [classes/:na]
  at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1115) [jetty-6.1.12.jar:6.1.12]
  at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:361) [jetty-6.1.12.jar:6.1.12]
  at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:417) [jetty-6.1.12.jar:6.1.12]
  at org.mortbay.jetty.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:230) [jetty-6.1.12.jar:6.1.12]

4.3 停止 logback-classic

为了释放 logback-classic 资源,停止 logback context 是一个好主意。如果停止,会关闭所有 loggers 关联的 appenders,并有序的停止所有活动线程。

import org.sflf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
...

LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.stop();

4.4 配置文件语法

4.4.1 <logger> 元素

一个 logger 记录器可以使用 <logger> 元素配置。<logger> 元素中,name 属性是必须的,level 级别属性是可选的,additivity 可叠加性属性也是可以选的(它的值是 true 或 false)。级别 level 属性的值是不区分大小写的字符串 TRACE,DEBUG,INFO,WARN,ERROR,ALL,OFF。还有不区分大小写的值 INHERITED 或其同义词 NULL,代表将强制从层次结构中较高的层次继承记录器继承级别。

<logger> 元素里面可包含0或多个 <appender-ref> 元素,这样引用的 appender 会关联到此 logger。不同 log4j,即便你在配置文件配置了 logger 关联的 appender,logback-classic 也不会关闭或者移除之前关联的 appender。

4.4.2 配置 root logger,<root> 元素

<root>元素用来配置 root logger。它支持单个属性,即 level 级别属性。它没有其他属性,因为可叠加性标志不适用于根记录器。此外,由于根记录器已被命名为 “ ROOT” ,因此它也不允许使用 name 属性。level 属性的值可以是不区分大小写的字符串 TRACE,DEBUG,INFO,WARN,ERROR,ALL或OFF之一。但是,根记录器的级别不能设置为 INHERITED 或 NULL。

与 <logger> 元素类似,<root> 元素也可以包含零个或多个 <appender-ref>元素。如此引用的每个附加程序都会添加到根记录器中。不同 log4j,即便你在配置文件配置了 root logger 关联的 appender,logback-classic 也不会关闭或者移除之前关联的 appender。

下面演示个 demo,假设我们不想打印 “com.nobody.entity” 包下任何组件的任何 DEBUG 消息。可以按如下配置:

<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoder 默认被分配 ch.qos.logback.classic.encoder.PatternLayoutEncoder 类 -->
    <!-- 当然你也可以通过 class 属性 显示指定,即 <encoder class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <logger name="com.nobody.entity" level="INFO"/>

  <!-- 其实此level属性设置也可以去除,因为默认就是 DEBUG 级别 -->
  <root level="DEBUG">          
    <appender-ref ref="STDOUT"/>
  </root>  
  
</configuration>

我们可以根据需要配置任意数量的记录器。如下,我们将 com.nobody.A 记录器的级别设置 为 INFO,但同时将 com.nobody.B 记录器的级别设置为DEBUG。

<configuration>

  ...

  <logger name="com.nobody.A" level="INFO"/>
  <logger name="com.nobody.B" level="DEBUG"/>
	
  ...	
	
</configuration>

4.4.3 配置 Appenders

一个 appender 使用 <appender> 元素配置,该元素具有两个必填属性 name 和 class。name 属性指定 appender 的名称,class 属性指定实例化此 appender 的类。<appender> 元素可包含零个或一个 <layout> 元素,零个或多个 <encoder> 元素,零个或多个层 <filter> 元素。除了这三个公共元素之外,<appender> 可以包含任意数量的与 appender 类的JavaBean属性相对应的元素。

<layout> 有个必填的属性指定实例化此对象的全限定类名。和 <appender>一样,它也有自己的相关属性。PatternLayout 有默认的属性值,所以可以不指定属性值。

<encoder>有个必填的属性指定实例化此对象的全限定类名。PatternLayoutEncoder 有默认的属性值,所以可以不指定属性值。

<configuration debug="false" scan="true" scanPeriod="30 seconds" packagingData="true">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>-->
            <pattern>%-4relative [%thread] %-5level %logger{32} - %msg%n</pattern>
        </encoder>
    </appender>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
		<file>myApp.log</file>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

上述配置文件定义了两个命名为 FILE 和STDOUT 的 appender 。FILE appender 将日志输出到 myApp.log 文件。STDOUT appender 将日志输出到控制台。

默认情况下,附加程序是累积式的:记录器将记录到附加到其自身的附加程序(如果有)以及附加到其祖先的所有附加程序。因此,将同一附加程序附加到多个记录器将导致记录输出重复。

默认情况下,appender 是累积式的:一个 logger 会将日志输出到它自己关联的所有 appender 和 它上层级(祖先)所关联的所有 appender。所以,如果将同一个 appender 关联到不同的 logger,有可能会导致输出的日志会重复。例如下面这个例子:

<configuration debug="false" scan="true" scanPeriod="30 seconds" packagingData="true">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>-->
            <pattern>%-4relative [%thread] %-5level %logger{32} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
		<file>myApp.log</file>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="com.nobody.Main">
        <appender-ref ref="STDOUT"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>
package com.nobody;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.StatusPrinter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main {
   

    private static final Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
   

        logger.info("Hello Logback!");
  
        Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
        rootLogger.info("rootLogger:{}", rootLogger.getName());
    }
}

会在控制台输出如下结果,因为名为 com.nobody.Main 的 logger 的可叠加性标识默认为 true,所以会将日志输出到它上级的 logger 关联的 appender 中,所以输出2遍。而 root logger 没有上级,输出1遍。

674  [main] INFO  com.nobody.Main - Hello Logback!
674  [main] INFO  com.nobody.Main - Hello Logback!
679  [main] INFO  ROOT - rootLogger:ROOT

当然,你可以将 com.nobody.Main 的 logger 的可叠加性标识默认为 false,那它的日志就不会输送到上层级中。

<logger name="com.nobody.Main" additivity="false">
    <appender-ref ref="STDOUT"/>
</logger>

4.5 设置上下文名称

每个 logger 记录器都附加到一个记录器上下文。默认情况下,它的名称为 “default”。通过 <contextName> 配置可以更改其名称,使用此值打印到日志中,用于区分不同应用程序的记录。但一旦设置,它的名称就无法变更。

<configuration>
  <contextName>myAppName</contextName>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

4.6 变量定义和替换

logback 配置文件支持变量的定义和替换。变量具有作用域。变量可以在配置文件中,在外部文件中,在外部资源中,甚至可以即时计算和定义。

变量替换可以发生在配置文件中可以指定值的任何位置。语法是 ${variableName}。

考虑到常用性,HOSTNAME 和 CONTEXT_NAME 变量默认已定义,并具有上下文作用域。考虑到在某些环境中可能需要花费一些时间来计算主机名,因此它的值是延迟计算的(仅在需要时)。

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>%d{HH:mm:ss.SSS} ${HOSTNAME} ${CONTEXT_NAME} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

变量可以在 logback 自己的配置文件中定义,也可以从外部属性文件或外部资源中批量加载。由于历史原因,用于定义变量用 <property>。

<configuration>

  <property name="LOG_HOME" value="./logs"/>

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${LOG_HOME}/myApp.log</file>
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>

  <root level="debug">
    <appender-ref ref="FILE" />
  </root>
</configuration>

它下面的效果一样,logback 将在 System 属性中查找它。

java -LOG_HOME="./logs" MyApp

如果你定义的变量太多时,可以创建单独的文件来保存,方便管理。

<configuration>

  <property file="src/main/java/resources/logback-variables.properties"/>

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${LOG_HOME}/myApp.log</file>
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>

  <root level="debug">
    <appender-ref ref="FILE" />
  </root>
</configuration>

此配置会读取logback-variables.properties文件中的变量,然后在本地范围内使用。logback-variables.properties文件定义的变量如下:

LOG_HOME=./logs

当然,也可以写成引入类路径上的资源文件的形式。

<configuration>

  <property resource="logback-variables.properties"/>

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${LOG_HOME}/myApp.log</file>
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>

  <root level="debug">
    <appender-ref ref="FILE" />
  </root>
</configuration>

变量作用域

定义的变量是有作用域的,如本地作用域,上下文作用域,系统级作用域。默认是本地作用域。从操作系统环境中读取变量很容易,但是无法写入到操作系统环境中。

  1. Local Scope(本地作用域):从配置文件中定义的本地变量即在本地配置文件使用。每次解析和执行配置文件时,都会重新定义本地作用域中的变量。
  2. Context Scope(上下文作用域):一个拥有上下文作用域的变量存在于上下文中,于上下文共存,直到被清除。在所有记录事件中都可用到,包括那些通过序列化发送到远程主机的事件。
  3. System Scope(系统级作用域):系统级作用域的变量被插入到JVM的系统属性中,生命周期和JVM一致,直到被清除。

在进行属性替换时,查找变量的顺序为:local scope,context scope,system properties,OS environment。

<configuration>

  <property scope="context" name="LOG_HOME" value="./logs"/>

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${LOG_HOME}/myApp.log</file>
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>

  <root level="debug">
    <appender-ref ref="FILE" />
  </root>
</configuration>

引入变量时,可能变量未定义或者值为null,我们可以使用“:-”符号指定默认值。例如${LOG_HOME:-./logs}。

支持变量嵌套,默认值和值定义都可以引用其他变量。例如:

LOG_HOME=./logs
FILE_NAME=myApp.log
destination=${LOG_HOME}/${FILE_NAME}

名称嵌套

引用变量时,变量名称可能包含对另一个变量的引用。例如,如果为名为“ userid”的变量分配了值“ alice”,则“ $ {$ {userid} .password}”引用名称为“ alice.password”的变量。

默认值嵌套

变量的默认值可以引用另一个变量。例如,假设未分配变量“ id”,并且为变量“ userid”分配了值“ alice”,则表达式“ $ {id :- $ {userid}}”将返回“ alice”。

有条件地处理配置文件

我们可能需要在不同的环境(例如dev,test,prod)切换不同的logback配置文件。然而这些配置文件大部分内容是一样的,极少内容是不同的。为了减少多个配置文件,可以使用条件处理标签,<if>, <then> 和 <else>,根据不同环境进行配置。不过,需要引入Janino 库。

   <!-- if-then 形式 -->
   <if condition="表达式">
    <then>
      ...
    </then>
  </if>
  
  <!-- if-then-else 形式 -->
  <if condition="表达式">
    <then>
      ...
    </then>
    <else>
      ...
    </else>    
  </if>

condition 条件只能是上下文属性或系统属性的Java表达式。对于通过参数传递的键,可以通过 property() 或简写的 p() 方法返回属性的字符串值。例如,property(“k”) 或 p(“k”) 访问键“ k”的值。如果键“ k”的属性未定义,则属性方法将返回空字符串,而不是null。这能避免判断null值。

isDefined()方法可用于检查是否定义了属性。例如,isDefined(“k”) 。如果需要检查属性是否为null,则可以使用 isNull() 方法。例如,isNull(“k”)。

<configuration debug="true">

  <if condition='property("HOSTNAME").contains("torino")'>
    <then>
      <appender name="CON" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
          <pattern>%d %-5level %logger{35} - %msg %n</pattern>
        </encoder>
      </appender>
      <root>
        <appender-ref ref="CON" />
      </root>
    </then>
  </if>

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${randomOutputDir}/conditional.log</file>
    <encoder>
      <pattern>%d %-5level %logger{35} - %msg %n</pattern>
   </encoder>
  </appender>

  <root level="ERROR">
     <appender-ref ref="FILE" />
  </root>
</configuration>

在<configuration>范围内都可以使用条件处理语句。还支持嵌套的if-then-else语句。但是,XML语法非常繁琐,为以后后续其他开发者以及自己能快速理解,尽量少用。

文件包含

Joran支持将配置文件的一部分包含在另一个文件中。这是通过声明一个 元素来完成的,如下所示:

可以通过标签<include>来引入另一个配置文件。

<configuration>
  <include file="src/main/java/resources/includedConfig.xml"/>

  <root level="DEBUG">
    <appender-ref ref="includedConsole" />
  </root>

</configuration>

includedConfig.xml文件定义了被引用的内容:

<included>
  <appender name="includedConsole" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>"%d - %m%n"</pattern>
    </encoder>
  </appender>
</included>

<include>标签引入的文件可以为一个文件,一个类路径上的资源,或者一个URL。如下:

<include file="src/main/java/resources/includedConfig.xml"/>

<include resource="includedConfig.xml"/>

<include url="http://xxx.com/includedConfig.xml"/>

如果被引用的文件不存在,logback会打印内部的状态信息。如果包含的文件是可选的,可以通过optional属性设置为true来进制打印显示警告信息。

<include optional="true" ..../>

五 Appenders

Logback 将写日志记录事件的任务委派给appender组件。Appenders必须实现ch.qos.logback.core.Appender 接口。此接口主要方法如下:

package ch.qos.logback.core;

import ch.qos.logback.core.spi.ContextAware;
import ch.qos.logback.core.spi.FilterAttachable;
import ch.qos.logback.core.spi.LifeCycle;

public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable<E> {
   

    /**
     * Get the name of this appender. The name uniquely identifies the appender.
     */
    String getName();

    /**
     * This is where an appender accomplishes its work. Note that the argument 
     * is of type Object.
     * @param event
     */
    void doAppend(E event) throws LogbackException;

    /**
     * Set the name of this appender. The name is used by other components to
     * identify this appender.
     * 
     */
    void setName(String name);

}

Appender接口中的大多数方法是setter和getter。不过有个例外是doAppend(E event)方法。E的实际类型取决于logback模块。在logback-classic模块中,E的类型为ILoggingEvent;在logback-access模块​​中,类型的E为AccessEvent。doAppend()方法是logback框架中比较重要的方法。它负责以合适的格式将日志记录事件输出到合适的输出设备中。

Appender接口扩展了FilterAttachable接口。所以可以将一个或多个过滤器关联到appender实例。

Appender负责输出日志记录事件。但是,他们可以将事件的实际格式委托给Layout或Encoder对象处理。每个layout和encoder都只能与有且一个appender相关联。某些appender具有内置或固定的事件格式。因此,它们不需要Layout或Encoder。


转载:https://blog.csdn.net/chenlixiao007/article/details/113899664
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场