源码详解系列(七) ------ 全面讲解logback的使用和源码

什么是logback

logback 用于日志记录,能够将日志输出到控制台、文件、数据库和邮件等,相比其它全部的日志系统,logback 更快而且更小,包含了许多独特而且有用的特性。html

logback 被分红三个不一样的模块:logback-core,logback-classic,logback-access。java

  1. logback-core 是其它两个模块的基础。
  2. logback-classic 模块能够看做是 log4j 的一个优化版本,它自然的支持 SLF4J。
  3. logback-access 提供了 http 访问日志的功能,能够与 Servlet 容器进行整合,例如:Tomcat、Jetty。

本文将介绍如下内容,因为篇幅较长,可根据须要选择阅读:mysql

  1. 如何使用 logback:将日志输出到控制台、文件和数据库,以及使用 JMX 配置 logback;git

  2. logback 配置文件详解;github

  3. logback 的源码分析。sql

如何使用logback

需求

  1. 使用 logback 将日志信息分别输出到控制台、文件、数据库。
  2. 使用 JMX 方式配置 logback。

工程环境

JDK:1.8.0_231
maven:3.6.1
IDE:Spring Tool Suite 4.3.2.RELEASE
mysql:5.7.28数据库

主要步骤

  1. 搭建环境;
  2. 配置 logback 文件;
  3. 编写代码:获取 Logger 实例,并打印指定等级的日志;
  4. 测试。

建立项目

项目类型 Maven Project ,打包方式 jar。express

引入依赖

logack 自然的支持 slf4j,不须要像其余日志框架同样引入适配层(如 log4j 需引入 slf4j-log4j12 )。经过后面的源码分析可知,logback 只是将适配相关代码放入了 logback-classic。windows

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- logback+slf4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
        <!-- 输出日志到数据库时须要用到 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>
        <!-- 使用数据源方式输出日志到数据库时须要用到 -->
        <dependency>
            <groupId>com.mchange</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.5.4</version>
        </dependency>
    </dependencies>

将日志输出到控制台

配置文件

配置文件放在 resources 下,文件名能够为 logback-test.xml 或 logback.xml,实际项目中能够考虑在测试环境中使用 logback-test.xml ,在生产环境中使用 logback.xml( 固然 logback 还支持使用 groovy 文件或 SPI 机制进行配置,本文暂不涉及)。api

在 logback中,logger 能够当作为咱们输出日志的对象,而这个对象打印日志时必须遵循 appender 中定义的输出格式和输出目的地等。注意,root logger 是一个特殊的 logger。

<configuration>
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!--定义控制台输出格式-->
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

另外,即便咱们没有配置,logback 也会默认产生一个 root logger ,并为它配置一个 ConsoleAppender

编写测试类

为了程序的解耦,通常咱们在使用日志时会采用门面模式,即经过 slf4j 或 commons-logging 来获取 Logger 对象。

如下代码中,导入的两个类 LoggerLoggerFactory都定义在 slf4j-api 中,彻底不会涉及到 logback 包的类。这时,若是咱们想切换 log4j 做为日志支持,只要修改 pom.xml 和日志配置文件就行,项目代码并不须要改动。源码分析部分将分析 slf4j 如何实现门面模式。

@Test
    public void test01() {
        Logger logger = LoggerFactory.getLogger(LogbackTest.class);
        
        logger.debug("输出DEBUG级别日志");
        logger.info("输出INFO级别日志");
        logger.warn("输出WARN级别日志");
        logger.error("输出ERROR级别日志");
        
    }

注意,这里获取的 logger 不是咱们配置的 root logger,而是以 cn.zzs.logback.LogbackTest 命名的 logger,它继承了祖先 root logger 的配置

测试

运行测试方法,能够看到在控制台打印以下信息:

2020-01-16 09:10:40 [main] INFO  ROOT - 输出INFO级别的日志
2020-01-16 09:10:40 [main] WARN  ROOT - 输出WARN级别的日志
2020-01-16 09:10:40 [main] ERROR ROOT - 输出ERROR级别的日志

这时咱们会发现,怎么没有 debug 级别的日志?由于咱们配置了日志等级为 info,小于 info 等级的日志不会被打印出来。日志等级以下:

ALL < TRACE < DEBUG < INFO < WARN < ERROR < OFF

将日志输出到滚动文件

本例子将在以上例子基础上修改。测试方法代码不须要修改,只要修改配置文件就能够了。

配置文件

前面已经讲过,appender 中定义日志的输出格式和输出目的地等,因此,要将日志输出到滚动文件,只要修改appender 就行。logback 提供了RollingFileAppender来支持打印日志到滚动文件。

如下配置中,设置了文件大小超过100M后会按指定命名格式生成新的日志文件。

<configuration>

    <!-- 定义变量 -->
    <property name="LOG_HOME" value="D:/growUp/test/log" />
    <property name="APP_NAME" value="logback-demo"/>
    
    <!-- 滚动文件输出 -->
    <appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 指定日志文件的名称 -->
        <file>${LOG_HOME}/${APP_NAME}/error.log</file>
        
        <!-- 配置追加写入 -->
        <append>true</append>    
        
        <!-- 级别过滤器 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        
        <!-- 滚动策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 滚动文件名称  -->
            <fileNamePattern>${LOG_HOME}/${APP_NAME}/notError-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!-- 可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。
                注意,删除旧文件时, 那些为了归档而建立的目录也会被删除。 -->
            <MaxHistory>50</MaxHistory>
            <!-- 当日志文件超过maxFileSize指定的大小时,根据上面提到的%i进行日志文件滚动 -->
            <maxFileSize>100MB</maxFileSize>
            <!-- 设置文件总大小 -->
            <totalSizeCap>20GB</totalSizeCap>
        </rollingPolicy>
        
        <!-- 日志输出格式-->
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

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

测试

运行测试方法,咱们能够在指定目录看到生成的日志文件。

file_appender_01

查看日志文件,能够看到只打印了 error 等级的日志:

file_appender_02

将日志输出到数据库

logback 提供了DBAppender来支持将日志输出到数据库中。

建立表

logback 为咱们提供了三张表用于记录日志, 在使用DBAppender以前,这三张表必须存在。

这三张表分别为:logging_event, logging_event_property 与 logging_event_exception。logback 自带 SQL 脚原本建立表,这些脚本在 logback-classic/src/main/java/ch/qos/logback/classic/db/script 文件夹下,相关脚本也能够再本项目的 resources/script 找到。

logback日志表脚本

因为本文使用的是 mysql 数据库,执行如下脚本(注意,官方给的 sql 中部分字段设置了NOT NULL 的约束,可能存在插入报错的状况,能够考虑调整):

BEGIN;
DROP TABLE IF EXISTS logging_event_property;
DROP TABLE IF EXISTS logging_event_exception;
DROP TABLE IF EXISTS logging_event;
COMMIT;

BEGIN;
CREATE TABLE logging_event 
  (
    timestmp         BIGINT NOT NULL,
    formatted_message  TEXT NOT NULL,
    logger_name       VARCHAR(254) NOT NULL,
    level_string      VARCHAR(254) NOT NULL,
    thread_name       VARCHAR(254),
    reference_flag    SMALLINT,
    arg0              VARCHAR(254),
    arg1              VARCHAR(254),
    arg2              VARCHAR(254),
    arg3              VARCHAR(254),
    caller_filename   VARCHAR(254),
    caller_class      VARCHAR(254) NOT NULL,
    caller_method     VARCHAR(254) NOT NULL,
    caller_line       CHAR(4) NOT NULL,
    event_id          BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY
  );
COMMIT;

BEGIN;
CREATE TABLE logging_event_property
  (
    event_id          BIGINT NOT NULL,
    mapped_key        VARCHAR(254) NOT NULL,
    mapped_value      TEXT,
    PRIMARY KEY(event_id, mapped_key),
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
  );
COMMIT;

BEGIN;
CREATE TABLE logging_event_exception
  (
    event_id         BIGINT NOT NULL,
    i                SMALLINT NOT NULL,
    trace_line       VARCHAR(254) NOT NULL,
    PRIMARY KEY(event_id, i),
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
  );
COMMIT;

能够看到生成了三个表:

logback的三张日志表

配置文件

logback 支持使用 DataSourceConnectionSource,DriverManagerConnectionSource 与 JNDIConnectionSource 三种方式配置数据源 。本文选择第一种,并使用以 c3p0 做为数据源(第二种方式文中也会给出)。

这里须要说明下,由于实例化 c3p0 的数据源对象ComboPooledDataSource时,会去自动加载 classpath 下名为 c3p0-config.xml 的配置文件,因此,咱们不须要再去指定 dataSource 节点下的参数,若是是 druid 或 dbcp 等则须要指定。

<configuration>

    <!--数据库输出-->
    <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
        <!-- 使用jdbc方式 -->
        <!-- <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
            <driverClass>com.mysql.cj.jdbc.Driver</driverClass>
            <url>jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true</url>
            <user>root</user>
            <password>root</password>
        </connectionSource> -->
        <!-- 使用数据源方式 -->
        <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
           <dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
           </dataSource>
        </connectionSource>
    </appender>
        
    <root level="info">
        <appender-ref ref="DB" />
    </root>
</configuration>

测试

运行测试方法,能够看到数据库中插入了如下数据:

logback日志表数据

使用JMX配置logback

logback 支持使用 JMX 动态地更新配置。开启 JMX 很是简单,只须要增长 jmxConfigurator 节点就能够了,以下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

    <!-- 定义变量 -->
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 开启JMX支持 -->
    <jmxConfigurator />
    
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
        
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

在咱们经过 jconsole 链接到服务器上以后(jconsole 在 JDK 安装目录的 bin 目录下),在 MBeans 面板上,在 "ch.qos.logback.classic.jmx.Configurator" 文件夹下你能够看到几个选项。以下图所示:

logback_jmx_01

咱们能够看到,在属性中,咱们能够查看 logback 已经产生的 logger 和 logback 的内部状态,经过操做,咱们能够:

  • 获取指定 logger 的级别。返回值能够为 null
  • 设置指定的 logger 的级别。想要设置为 null,传递 "null" 字符串就能够
  • 经过指定的文件从新加载配置
  • 经过指定的 URL 从新加载配置
  • 使用默认配置文件从新加载 logback 的配置
  • 或者指定 logger 的有效级别

更多 JMX 相关内容可参考个人另外一篇博客:如何使用JMX来管理程序?

补充--两种打印方式

实际项目中,有时咱们须要对打印的内容进行必定处理,以下:

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

这种状况会产生构建消息参数的成本,为了不以上损耗,能够修改以下:

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

当咱们打印的是一个对象时,也能够采用如下方法来优化:

// 不推荐
logger.debug("The new entry is " + entry + ".");
// 推荐
logger.debug("The new entry is {}", entry);

配置文件详解

前面已经说过, logback 配置文件名能够为 logback-test.xml 、 logback.groovy 或 logback.xml ,除了采用配置文件方式, logback 也支持使用 SPI 机制加载 ch.qos.logback.classic.spi.Configurator 的实现类来进行配置。如下讲解仅针对 xml 格式文件的配置方式展开。

另外,若是想要自定义配置文件的名字,能够经过系统属性指定:

-Dlogback.configurationFile=/path/to/config.xml

若是没有加载到配置,logback 会调用 BasicConfigurator 进行默认的配置。

configuration

configuration 是 logback.xml 或 logback-test.xml 文件的根节点。

logback_configuration_01

configuration 主要用于配置某些全局的日志行为,常见的配置参数以下:

属性名 描述
debug 是否打印 logback 的内部状态,开启有利于排查 logback 的异常。默认 false
scan 是否在运行时扫描配置文件是否更新,若是更新时则从新解析并更新配置。若是更改后的配置文件有语法错误,则会回退到以前的配置文件。默认 false
scanPeriod 多久扫描一次配置文件是否修改,单位能够是毫秒、秒、分钟或者小时。默认状况下,一分钟扫描一次配置文件。

配置方式以下:

<configuration debug="true" scan="true" scanPeriod="60 seconds" >
    
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
       <target>system.err</target>   
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

使用以上配置进行测试:

logback_configuration_debug.png

如上图,经过控制台咱们能够查看 logback 加载配置的过程,这时,咱们尝试修改 logback 配置文件的内容:

logback_configuration_scan.png

观察控制台,能够看到配置文件从新加载:

logback_configuration_scan2.png

logger

前面提到过,logger 是为咱们打印日志的对象,这个概念很是重要,有助于更好地理解 logger 的继承关系。

在如下代码中,咱们能够在getLogger方法中传入的是当前类的 Class 对象或全限定类名,本质上获取到的都是一个 logger 对象(若是该 logger 不存在,才会建立)。

@Test
    public void test01() {
        Logger logger1 = LoggerFactory.getLogger(LogbackTest.class);
        Logger logger2 = LoggerFactory.getLogger("cn.zzs.logback.LogbackTest");
        System.err.println(logger == logger2);// true
    }

这里补充一个问题,该 logger 对象以 cn.zzs.logback.LogbackTest 命名,和咱们配置文件中定义的 root logger 并非同一个,可是为何这个 logger 对象却拥有 root logger 的行为?

这要得益于 logger 的继承关系,以下图:

logback_logger_01

若是咱们未指定当前 logger 的日志等级,logback 会将其日志等级设置为最近父级的日志等级。另外,默认状况下,当前 logger 也会继承最近父级持有的 appender。

下面测试下以上特性,将配置文件进行以下修改:

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds" debug="true">


    <!-- 定义变量 -->
    <property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" />
    <property scope="system" name="APP_NAME" value="logback-demo"/>
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    
    <timestamp key="bySecond" datePattern="yyyy-MM-dd'T'HH-mm-ss" />
    <!-- 文件输出 -->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <append>true</append>
        <file>${LOG_HOME}/${APP_NAME}/file-${bySecond}.log</file>
        <immediateFlush>true</immediateFlush>
        <!-- 是否启用安全写入 -->
        <prudent>false</prudent>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    
    <logger name="cn.zzs" level="error">
        <appender-ref ref="FILE" />
    </logger>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

这里自定义了一个 logger,日志等级是 error,appender 为文件输出。运行测试方法:

logback_logger_02

能够看到,名为 cn.zzs.logback.LogbackTest 的 logger 继承了名为 cn.zzs 的 logger 的日志等级和 appender,以及继承了 root logger 的 appender。

实际项目中,若是不但愿继承父级的 appender,能够配置 additivity="false" ,以下:

<logger name="cn.zzs" additivity="false">
       <appender-ref ref="FILE" />
    </logger>

注意,由于如下配置都是创建在 logger 的继承关系上,因此这部份内容必须很好地理解。

appender

appender 用于定义日志的输出目的地和输出格式,被 logger 所持有。logback 为咱们提供了如下几种经常使用的appender:

类名 描述
ConsoleAppender 将日志经过 System.out 或者 System.err 来进行输出,即输出到控制台。
FileAppender 将日志输出到文件中。
RollingFileAppender 继承自 FileAppender,也是将日志输出到文件,但文件具备轮转功能。
DBAppender 将日志输出到数据库
SocketAppender 将日志以明文方式输出到远程机器
SSLSocketAppender 将日志以加密方式输出到远程机器
SMTPAppender 将日志输出到邮件

本文仅会讲解前四种,后四种可参考官方文档

ConsoleAppender

ConsoleAppender 支持将日志经过 System.out 或者 System.err 输出,即输出到控制台,经常使用属性以下:

属性名 类型 描述
encoder Encoder 后面单独讲
target String System.out 或 System.err。默认为 System.out
immediateFlush boolean 是否当即刷新。默认为 true。
withJansi boolean 是否激活 Jansi 在 windows 使用 ANSI 彩色代码,默认值为 false。
在windows电脑上我尝试开启这个属性并引入 jansi 包,但总是报错,暂时没有解决方案。

具体配置以下:

<!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

FileAppender

FileAppender 支持将日志输出到文件中,经常使用属性以下:

属性名 类型 描述
append boolean 是否追加写入。默认为 true
encoder Encoder 后面单独讲
immediateFlush boolean 是否当即刷新。默认为 true。
file String 要写入文件的路径。若是文件不存在,则新建。
prudent boolean 是否采用安全方式写入,即便在不一样的 JVM 或者不一样的主机上运行 FileAppender 实例。默认的值为 false。

具体配置以下:

<!-- 定义变量 -->
    <property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" />
    <property scope="system" name="APP_NAME" value="logback-demo"/>
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    <timestamp key="bySecond" datePattern="yyyy-MM-dd'T'HH-mm-ss" />
    
    <!-- 文件输出 -->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>${LOG_HOME}/${APP_NAME}/file-${bySecond}.log</file>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

RollingFileAppender

RollingFileAppender 继承自 FileAppender,也是将日志输出到文件,但文件具备轮转功能。

RollingFileAppender 的属性以下所示:

属性名 类型 描述
file String 要写入文件的路径。若是文件不存在,则新建。
append boolean 是否追加写入。默认为 true。
immediateFlush boolean 是否当即刷新。默认为true。
encoder Encoder 后面单独将
rollingPolicy RollingPolicy 定义文件如何轮转。
triggeringPolicy TriggeringPolicy 定义何时发生轮转行为。若是 rollingPolicy 使用的类已经实现了 triggeringPolicy 接口,则不须要再配置 triggeringPolicy,例如 SizeAndTimeBasedRollingPolicy。
prudent boolean 是否采用安全方式写入,即便在不一样的 JVM 或者不一样的主机上运行 FileAppender 实例。默认的值为 false。

具体配置以下:

<!-- 定义变量 -->
    <property scope="system" name="LOG_HOME" value="D:/growUp/test/logs" />
    <property scope="system" name="APP_NAME" value="logback-demo"/>
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>   
    <!-- 轮转文件输出 -->
    <appender name="FILE-ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        
        <!-- 轮转策略,它根据时间和文件大小来制定轮转策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天轮转  -->
            <fileNamePattern>${LOG_HOME}/${APP_NAME}/log-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!-- 保存 30 天的历史记录,最大大小为 30GB -->
            <MaxHistory>30</MaxHistory>
            <totalSizeCap>30GB</totalSizeCap>
            <!-- 当日志文件超过100MB的大小时,根据上面提到的%i进行日志文件轮转 -->
            <maxFileSize>100MB</maxFileSize>
        </rollingPolicy>
        
        <!-- 日志输出格式-->
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>

DBAppender

参见使用例子。

encoder

encoder 负责将日志事件按照配置的格式转换为字节数组,经常使用属性以下:

属性名 类型 描述
pattern String 日志打印格式。
outputPatternAsHeader boolean 是否将 pattern 字符串插入到日志文件顶部。默认false。

针对 pattern 属性,这里补充下它的经常使用转换字符:

转换字符 描述
c{length}
lo{length}
logger{length}
输出 logger 的名字。能够经过 length 缩短其长度。
可是,logger 名字最右边永远都会存在。
例如,当咱们设置 logger{0}时,cn.zzs.logback.LogbackTest 中的
LogbackTest 永远不会被删除
C{length}
class{length}
输出发出日志请求的类的全限定名称。
能够经过 length 缩短其长度。
d{pattern}
date{pattern}
d{pattern, timezone}
date{pattern, timezone}
输出日志事件的日期。
能够经过 pattern 设置日期格式,timezone 设置时区。
m / msg / message 输出与日志事件相关联的,由应用程序提供的日志信息。
M / method 输出发出日志请求的方法名。
p / le / level 输出日志事件的级别。
t / thread 输出生成日志事件的线程名。
n 输出平台所依赖的行分割字符。
F / file 输出发出日志请求的 Java 源文件名。
caller{depth}
caller{depthStart..depthEnd}
caller{depth, evaluator-1, ... evaluator-n}
caller{depthStart..depthEnd, evaluator-1, ... evaluator-n}
输出生成日志的调用者所在的位置信息。
L / line 输出发出日志请求所在的行号。
property{key} 输出属性 key 所对应的值。

注意,在拼接 pattren 时,应该考虑使用“有意义的”转换字符,避免产生没必要要的性能开销。具体配置以下:

<!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
       
        <encoder charset="utf-8">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <outputPatternAsHeader>true</outputPatternAsHeader>
        </encoder>
    </appender>

其中, 转换说明符 %-5level 表示日志事件的级别的字符应该向左对齐,保持五个字符的宽度。

filter

appender 除了定义日志的输出目的地和输出格式,其实也能够对日志事件进行过滤输出,例如,仅输出包含指定字符的日志。而这个功能需配置 filter。

LevelFilter

LevelFilter 基于级别来过滤日志事件。修改配置文件以下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

    <!-- 定义变量 -->
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
       
       <!-- 设置过滤器 -->
       <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

运行测试方法,可见,虽然 root logger 的日志等级是 info,但最终只会打印 error 的日志:

logback_LevelFilter

ThresholdFilter

ThresholdFilter 基于给定的临界值来过滤事件。若是事件的级别等于或高于给定的临界,则过滤经过,不然会被拦截。配置以下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

    <!-- 定义变量 -->
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
        
        <!-- 设置过滤器 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
          <level>ERROR</level>
        </filter>
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

运行测试方法,可见,虽然 root logger 的日志等级是 info,但最终只会打印 error 的日志:

logback_ThresholdFilter

EvaluatorFilter

EvaluatorFilter 基于给定的标准来过滤事件。 它采用 Groovy 表达式做为评估的标准。配置以下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

   <!-- 定义变量 -->
   <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
   
   <!-- 控制台输出 -->
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
   
      <target>system.err</target>
      
       <encoder charset="utf-8">
           <pattern>${LOG_PATTERN}</pattern>
       </encoder>
       
       <!-- 设置过滤器 -->
       <filter class="ch.qos.logback.core.filter.EvaluatorFilter">      
         <evaluator class="ch.qos.logback.classic.boolex.GEventEvaluator"> 
           <expression>
              e.level.toInt() >= ERROR.toInt() &amp;&amp; 
              !(e.mdc?.get("req.userAgent") =~ /Googlebot|msnbot|Yahoo/ )
           </expression>
         </evaluator>
         <OnMismatch>DENY</OnMismatch>
         <OnMatch>NEUTRAL</OnMatch>
       </filter>
   </appender>
   
   <root level="info">
       <appender-ref ref="STDOUT" />
   </root>
</configuration>

上面的过滤器引用自官网,规则为:让级别在 ERROR 及以上的日志事件在控制台显示,除非是因为来自 Google,MSN,Yahoo 的网络爬虫致使的错误。

注意,使用 GEventEvaluator 必须引入 groovy 的 jar 包:

<!-- groovy -->
        <dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy</artifactId>
            <version>3.0.0-rc-3</version>
        </dependency>

运行测试方法,输出以下结果:

logback_EvaluatorFilter

EvaluatorFilter 除了支持 Groovy 表达式,还支持使用 java 代码来做为过滤标准,修改配置文件以下:

<configuration scan="true" scanPeriod="10 seconds" debug="true">

    <!-- 定义变量 -->
    <property scope="system" name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
    
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    
       <target>system.err</target>
       
        <encoder charset="utf-8">
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
        
        <!-- 设置过滤器 -->
        <filter class="ch.qos.logback.core.filter.EvaluatorFilter">      
          <evaluator> <!-- defaults to type ch.qos.logback.classic.boolex.JaninoEventEvaluator -->
            <expression>return message.contains("ERROR");</expression>
          </evaluator>
          <OnMismatch>DENY</OnMismatch>
          <OnMatch>NEUTRAL</OnMatch>
        </filter>
    </appender>
    
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

注意,使用 JaninoEventEvaluator 必须导入 janino 包,以下:

<!-- janino -->
        <dependency>
            <groupId>org.codehaus.janino</groupId>
            <artifactId>janino</artifactId>
            <version>3.1.0</version>
        </dependency>

运行测试方法,输出以下结果:

logback_EvaluatorFilter_02

源码分析

logback 很是庞大、复杂,若是要将 logback 全部模块分析完,估计要花至关长的时间,因此,本文仍是和之前同样,仅针对核心代码进行分析,当分析的方法存在多个实现时,也只会挑选其中一个进行讲解。文中没有涉及到的部分,感兴趣的能够自行研究。

接下来经过解决如下几个问题来逐步分析 logback 的源码:

  1. slf4j 是如何实现门面模式的?
  2. logback 如何加载配置?
  3. 获取咱们所需的 logger?
  4. 如何将日志打印到控制台?

slf4j是如何实现门面模式的

slf4j 使用的是门面模式,无论使用什么日志实现,项目代码都只会用到 slf4j-api 中的接口,而不会使用到具体的日志实现的代码。slf4j 究竟是如何实现门面模式的?接下来进行源码分析:

在咱们的应用中,通常会经过如下方式获取 Logger 对象,咱们就从这个方法开始分析吧:

Logger logger = LoggerFactory.getLogger(LogbackTest.class);

进入到 LoggerFactory.getLogger(Class<?> clazz)方法,以下。在调用这个方法时,咱们通常会以当前类的 Class 对象做为入参。固然,logback 也容许你使用其余类的 Class 对象做为入参,可是,这样作可能不利于对 logger 的管理。经过设置系统属性-Dslf4j.detectLoggerNameMismatch=true,当实际开发中出现该类问题,会在控制台打印提醒信息。

public static Logger getLogger(Class<?> clazz) {
        // 获取Logger对象,后面继续展开
        Logger logger = getLogger(clazz.getName());
        // 若是系统属性-Dslf4j.detectLoggerNameMismatch=true,则会检查传入的logger name是否是CallingClass的全限定类名,若是不匹配,会在控制台打印提醒
        if (DETECT_LOGGER_NAME_MISMATCH) {
            Class<?> autoComputedCallingClass = Util.getCallingClass();
            if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
                Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                                autoComputedCallingClass.getName()));
                Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
            }
        }
        return logger;
    }

进入到LoggerFactory.getLogger(String name)方法,以下。在这个方法中,不一样的日志实现会返回不一样的ILoggerFactory实现类:

public static Logger getLogger(String name) {
        // 获取工厂对象,后面继续展开
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        // 利用工厂对象获取Logger对象
        return iLoggerFactory.getLogger(name);
    }

进入到getILoggerFactory()方法,以下。INITIALIZATION_STATE表明了初始化状态,该方法会根据初始化状态的不一样而返回不一样的结果。

static final SubstituteLoggerFactory SUBST_FACTORY = new SubstituteLoggerFactory();
    static final NOPLoggerFactory NOP_FALLBACK_FACTORY = new NOPLoggerFactory(); 
    public static ILoggerFactory getILoggerFactory() {
        // 若是未初始化
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    // 修改状态为正在初始化
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    // 执行初始化
                    performInitialization();
                }
            }
        }
        switch (INITIALIZATION_STATE) {
        // 若是StaticLoggerBinder类存在,则经过StaticLoggerBinder获取ILoggerFactory的实现类
        case SUCCESSFUL_INITIALIZATION:
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        // 若是StaticLoggerBinder类不存在,则返回NOPLoggerFactory对象
        // 经过NOPLoggerFactory获取到的NOPLogger没什么用,它的方法几乎都是空实现
        case NOP_FALLBACK_INITIALIZATION:
            return NOP_FALLBACK_FACTORY;
        // 若是初始化失败,则抛出异常
        case FAILED_INITIALIZATION:
            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
        // 若是正在初始化,则SubstituteLoggerFactory对象,这个对象不做扩展
        case ONGOING_INITIALIZATION:
            return SUBST_FACTORY;
        }
        throw new IllegalStateException("Unreachable code");
    }

以上方法须要重点关注 StaticLoggerBinder这个类,它并不在 slf4j-api 中,而是在 logback-classic 中,以下图所示。其实分析到这里应该能够理解:slf4j 经过 StaticLoggerBinder 类与具体日志实现进行关联,从而实现门面模式

logback_StaticLoggerBinder_01

接下来再简单看下LoggerFactory.performInitialization(),以下。这里会执行初始化,所谓的初始化就是查找 StaticLoggerBinder 这个类是否是存在,若是存在会将该类绑定到当前应用,同时,根据不一样状况修改INITIALIZATION_STATE。代码比较多,我归纳下执行的步骤:

  1. 若是 StaticLoggerBinder 存在且惟一,修改初始化状态为 SUCCESSFUL_INITIALIZATION;
  2. 若是 StaticLoggerBinder 存在但为多个,由 JVM 决定绑定哪一个 StaticLoggerBinder,修改初始化状态为 SUCCESSFUL_INITIALIZATION,同时,会在控制台打印存在哪几个 StaticLoggerBinder,并提醒用户最终选择了哪个 ;
  3. 若是 StaticLoggerBinder 不存在,打印提醒,并修改初始化状态为 NOP_FALLBACK_INITIALIZATION;
  4. 若是 StaticLoggerBinder 存在但 getSingleton() 方法不存在,打印提醒,并修改初始化状态为 FAILED_INITIALIZATION;
private final static void performInitialization() {
        // 查找StaticLoggerBinder这个类是否是存在,若是存在会将该类绑定到当前应用
        bind();
        // 若是检测存在
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            // 判断StaticLoggerBinder与当前使用的slf4j是否适配
            versionSanityCheck();
        }
    }
    private final static void bind() {
        try {
            // 使用类加载器在classpath下查找StaticLoggerBinder类。若是存在多个StaticLoggerBinder类,这时会在控制台提醒并列出全部路径(例如同时引入了logback和slf4j-log4j12 的包,就会出现两个StaticLoggerBinder类)
            Set<URL> staticLoggerBinderPathSet = null;
            if (!isAndroid()) {
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
            
            // 这一步只是简单调用方法,可是很是重要。
            // 能够检测StaticLoggerBinder类和它的getSingleton方法是否存在,若是不存在,分别会抛出 NoClassDefFoundError错误和NoSuchMethodError错误
            // 注意,当存在多个StaticLoggerBinder时,应用不会中止,由JVM随机选择一个。
            StaticLoggerBinder.getSingleton();
            
            // 修改状态为初始化成功
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            // 若是存在多个StaticLoggerBinder,会在控制台提醒用户实际选择的是哪个
            reportActualBinding(staticLoggerBinderPathSet);
            
            // 对SubstituteLoggerFactory的操做,不做扩展
            fixSubstituteLoggers();
            replayEvents();
            SUBST_FACTORY.clear();
            
        } catch (NoClassDefFoundError ncde) {
            // 当StaticLoggerBinder不存在时,会将状态修改成NOP_FALLBACK_INITIALIZATION,并抛出信息
            String msg = ncde.getMessage();
            if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
                Util.report("Defaulting to no-operation (NOP) logger implementation");
                Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
            } else {
                failedBinding(ncde);
                throw ncde;
            }
        } catch (java.lang.NoSuchMethodError nsme) {
            // 当StaticLoggerBinder.getSingleton()方法不存在时,会将状态修改成初始化失败,并抛出信息
            String msg = nsme.getMessage();
            if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
                INITIALIZATION_STATE = FAILED_INITIALIZATION;
                Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
                Util.report("Your binding is version 1.5.5 or earlier.");
                Util.report("Upgrade your binding to version 1.6.x.");
            }
            throw nsme;
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

这里再补充一个问题,slf4j-api 中不包含 StaticLoggerBinder 类,为何能编译经过呢?其实咱们项目中用到的 slf4j-api 是已经编译好的 class 文件,因此不须要再次编译。可是,编译前 slf4j-api 中是包含 StaticLoggerBinder.java 的,且编译后也存在 StaticLoggerBinder.class ,只是这个文件被手动删除了。

logback如何加载配置

前面说过,logback 支持采用 xml、grovy 和 SPI 的方式配置文件,本文只分析 xml 文件配置的方式。

logback 依赖于 Joran(一个成熟的,灵活的而且强大的配置框架 ),本质上是采用 SAX 方式解析 XML。由于 SAX 不是本文的重点内容,因此这里不会去讲解相关的原理,可是,这部分的分析须要具有 SAX 的基础,能够参考个人另外一篇博客: 源码详解系列(三) ------ dom4j的使用和分析(重点对比和DOM、SAX的区别)

logback 加载配置的代码仍是比较繁琐,且代码量较大,这里就不一个个方法地分析了,而是采用类图的方式来说解。下面是 logback 加载配置的大体图解:

logback_joran

这里再补充下图中几个类的做用:

类名 描述
SaxEventRecorder SaxEvent 记录器。继承了 DefaultHandler,因此在解析 xml 时会触发对应的方法,
这些方法将触发的参数封装到 saxEven 中并放入 saxEventList 中
SaxEvent SAX 事件体。用于封装 xml 事件的参数。
Action 执行的配置动做。
ElementSelector 节点模式匹配器。
RuleStore 用于存放模式匹配器-动做的键值对。

结合上图,我简单归纳下整个执行过程:

  1. 使用 SAX 方式解析 XML,解析过程当中根据当前的元素类型,调用 DefaultHandler 实现类的方法,构造 SaxEvent 并将其放入集合 saxEventList 中;
  2. 当 XML 解析完成,会调用 EventPlayer 的方法,遍历集合 saxEventList 的 SaxEvent 对象,当该对象可以匹配到对应的规则,则会执行相应的 Action。

简单看下LoggerContext

如今回到 StaticLoggerBinder.getLoggerFactory()方法,以下。这个方法返回的 ILoggerFactory 其实就是 LoggerContext。

private LoggerContext defaultLoggerContext = new LoggerContext();
    public ILoggerFactory getLoggerFactory() {
        // 若是初始化未完成,直接返回defaultLoggerContext
        if (!initialized) {
            return defaultLoggerContext;
        }
        
        if (contextSelectorBinder.getContextSelector() == null) {
            throw new IllegalStateException("contextSelector cannot be null. See also " + NULL_CS_URL);
        }
        // 若是是DefaultContextSelector,返回的仍是defaultLoggerContext
        // 若是是ContextJNDISelector,则可能为不一样线程提供不一样的LoggerContext 对象
        // 主要取决因而否设置系统属性-Dlogback.ContextSelector=JNDI
        return contextSelectorBinder.getContextSelector().getLoggerContext();
    }

下面简单看下 LoggerContext 的 UML 图。它不只做为获取 logger 的工厂,还绑定了一些全局的 Object、property 和 LifeCycle。

logback_LoggerContext_UML

获取logger对象

这里先看下 Logger 的 UML 图,以下。在 Logger 对象中,持有了父级 logger、子级 logger 和 appender 的引用。

logback_Logger_UML

进入LoggerContext.getLogger(String)方法,以下。这个方法逻辑简单,可是设计很是巧妙,能够好好琢磨下。我归纳下主要的步骤:

  1. 若是获取的是 root logger,直接返回;
  2. 若是获取的是 loggerCache 中缓存的 logger,直接返回;
  3. 循环获取 logger name 中包含的全部 logger,若是不存在就建立并放入缓存;
  4. 返回 logger name 对应的 logger。
public final Logger getLogger(final String name) {

        if (name == null) {
            throw new IllegalArgumentException("name argument cannot be null");
        }

        // 若是获取的是root logger,直接返回
        if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
            return root;
        }

        int i = 0;
        Logger logger = root;

        // 在loggerCache中缓存着已经建立的logger,若是存在,直接返回
        Logger childLogger = (Logger) loggerCache.get(name);
        if (childLogger != null) {
            return childLogger;
        }

        // 若是还找不到,就须要建立
        // 注意,要获取以cn.zzs.logback.LogbackTest为名的logger,名为cn、cn.zzs、cn.zzs.logback的logger不存在的话也会被建立
        String childName;
        while (true) {
            // 从起始位置i开始,获取“.”的位置
            int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
            // 截取logger的名字
            if (h == -1) {
                childName = name;
            } else {
                childName = name.substring(0, h);
            }
            // 修改起始位置,以获取下一个“.”的位置
            i = h + 1;
            synchronized (logger) {
                // 判断当前logger是否存在以childName命名的子级
                childLogger = logger.getChildByName(childName);
                if (childLogger == null) {
                    // 经过当前logger来建立以childName命名的子级
                    childLogger = logger.createChildByName(childName);
                    // 放入缓存
                    loggerCache.put(childName, childLogger);
                    // logger总数量+1
                    incSize();
                }
            }
            // 当前logger修改成子级logger
            logger = childLogger;
            // 若是当前logger是最后一个,则跳出循环
            if (h == -1) {
                return childLogger;
            }
        }
    }

进入Logger.createChildByName(String)方法,以下。

Logger createChildByName(final String childName) {
        // 判断要建立的logger在名字上是否是与当前logger为父子,若是不是会抛出异常
        int i_index = LoggerNameUtil.getSeparatorIndexOf(childName, this.name.length() + 1);
        if (i_index != -1) {
            throw new IllegalArgumentException("For logger [" + this.name + "] child name [" + childName
                            + " passed as parameter, may not include '.' after index" + (this.name.length() + 1));
        }
        // 建立子logger集合
        if (childrenList == null) {
            childrenList = new CopyOnWriteArrayList<Logger>();
        }
        Logger childLogger;
        // 建立新的logger
        childLogger = new Logger(childName, this, this.loggerContext);
        // 将logger放入集合中
        childrenList.add(childLogger);
        // 设置有效日志等级
        childLogger.effectiveLevelInt = this.effectiveLevelInt;
        return childLogger;
    }

logback 在类的设计上很是值得学习, 使得许多代码逻辑也很是简单易懂。

打印日志到控制台

这里以Logger.debug(String)为例,以下。这里须要注意 TurboFilter 和 Filter 的区别,前者是全局的,每次发起日志记录请求都会被调用,且在日志事件建立前调用,然后者是附加的,做用范围较小。由于实际项目中 TurboFilter 使用较少,这里不作扩展,感兴趣可参考这里

public static final String FQCN = ch.qos.logback.classic.Logger.class.getName();
    public void debug(String msg) {
        filterAndLog_0_Or3Plus(FQCN, null, Level.DEBUG, msg, null, null);
    }
    private void filterAndLog_0_Or3Plus(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
                    final Throwable t) {
        // 使用TurboFilter过滤当前日志,判断是否经过
        final FilterReply decision = loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);
        //  返回NEUTRAL表示没有TurboFilter,即无需过滤
        if (decision == FilterReply.NEUTRAL) {
            // 若是须要打印日志的等级小于有效日志等级,则直接返回
            if (effectiveLevelInt > level.levelInt) {
                return;
            }
        } else if (decision == FilterReply.DENY) {
            // 若是不经过,则不打印日志,直接返回
            return;
        }
        // 建立LoggingEvent
        buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
    }

进入Logger.buildLoggingEventAndAppend(String, Marker, Level, String, Object[], Throwable),以下。 logback 中,日志记录请求会被构形成日志事件 LoggingEvent,传递给对应的 appender 处理。

private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params,
                    final Throwable t) {
        // 构造日志事件LoggingEvent
        LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
        // 设置标记
        le.setMarker(marker);
        // 通知LoggingEvent给当前logger持有的和继承的appender
        callAppenders(le);
    }

进入到Logger.callAppenders(ILoggingEvent),以下。

public void callAppenders(ILoggingEvent event) {
        int writes = 0;
        // 通知LoggingEvent给当前logger的持有的和继承的appender处理日志事件
        for (Logger l = this; l != null; l = l.parent) {
            writes += l.appendLoopOnAppenders(event);
            // 若是设置了logger的additivity=false,则不会继续查找父级的appender
            // 若是没有设置,则会一直查找到root logger
            if (!l.additive) {
                break;
            }
        }
        // 当前logger未设置appender,在控制台打印提醒
        if (writes == 0) {
            loggerContext.noAppenderDefinedWarning(this);
        }
    }
    private int appendLoopOnAppenders(ILoggingEvent event) {
        if (aai != null) {
            // 调用AppenderAttachableImpl的方法处理日志事件
            return aai.appendLoopOnAppenders(event);
        } else {
            // 若是当前logger没有appender,会返回0
            return 0;
        }
    }

在继续分析前,先看下 Appender 的 UML 图(注意,Appender 还有不少实现类,这里只列出了经常使用的几种)。Appender 持有 Filter 和 Encoder 到引用,能够分别对日志进行过滤和格式转换。

本文仅涉及到 ConsoleAppender 的源码分析。

logback_Appender_UML

继续进入到AppenderAttachableImpl.appendLoopOnAppenders(E),以下。这里会遍历当前 logger 持有的 appender,并调用它们的 doAppend 方法。

public int appendLoopOnAppenders(E e) {
        int size = 0;
        // 得到当前logger的全部appender
        final Appender<E>[] appenderArray = appenderList.asTypedArray();
        final int len = appenderArray.length;
        for (int i = 0; i < len; i++) {
            // 调用appender的方法
            appenderArray[i].doAppend(e);
            size++;
        }
        // 这个size为appender的数量
        return size;
    }

为了简化分析,本文仅分析打印日志到控制台的过程,因此进入到UnsynchronizedAppenderBase.doAppend(E)方法,以下。

public void doAppend(E eventObject) {
        // 避免doAppend方法被重复调用??
        // TODO 这一步不是很理解,同一个线程还能同时调用两次这个方法?
        if (Boolean.TRUE.equals(guard.get())) {
            return;
        }

        try {
            guard.set(Boolean.TRUE);

            // 过滤当前日志事件是否容许打印
            if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
                return;
            }

            // 调用实现类的方法
            this.append(eventObject);

        } catch (Exception e) {
            if (exceptionCount++ < ALLOWED_REPEATS) {
                addError("Appender [" + name + "] failed to append.", e);
            }
        } finally {
            guard.set(Boolean.FALSE);
        }
    }

进入到OutputStreamAppender.append(E),以下。

protected void append(E eventObject) {
        // 若是appender未启动,则直接返回,不处理日志事件
        if (!isStarted()) {
            return;
        }
        subAppend(eventObject);
    }
    protected void subAppend(E event) {
        // 这里又判断一次??
        if (!isStarted()) {
            return;
        }
        try {
            // 这一步不是很懂 TODO
            if (event instanceof DeferredProcessingAware) {
                ((DeferredProcessingAware) event).prepareForDeferredProcessing();
            }
            
            // 调用encoder的方法将日志事件转化为字节数组
            byte[] byteArray = this.encoder.encode(event);
            // 打印日志
            writeBytes(byteArray);

        } catch (IOException ioe) {
            this.started = false;
            addStatus(new ErrorStatus("IO failure in appender", this, ioe));
        }
    }

看下LayoutWrappingEncoder.encode(E),以下。

public byte[] encode(E event) {
        // 根据配置格式处理日志事件
        String txt = layout.doLayout(event);
        // 将字符转化为字节数组并返回
        return convertToBytes(txt);
    }

后面会调用PatternLayout.doLayout(ILoggingEvent)将日志的消息进行处理,这部份内容我就不继续扩展了,感兴趣能够自行研究。

以上是 logback 的源码基本分析完成,后续有空再做补充。

参考资料

logback中文手册

相关源码请移步:https://github.com/ZhangZiSheng001/logback-demo

本文为原创文章,转载请附上原文出处连接:https://www.cnblogs.com/ZhangZiSheng001/p/12246122.html

相关文章
相关标签/搜索