一文来了解日志框架的前世今生

一、背景

前段时间公司做营销活动迎接黑色星期五,流量突增,线上服务开启了打印SQL结果集的日志,导致日志过于膨胀,对磁盘的存储空间造成比较大的压力。因此,领导想把结果集关掉,但需要保留打印SQL语句和参数。当晚,孩子哄睡完毕,夜深人静时,下班时间向来不学习的我,心血来潮去学习日志的打印原理。第二天上午,让我看看时,由于有前晚学习的基础,心理有个谱,知道从哪些方向去分析、定位问题的根源。

二、日志框架的前世今生

Spring框架选择使用了JCL作为默认日志输出而Spring Boot 默认选择了SLF4] 结合LogBack

1、第一阶段:System.out与System.err

2001年以前,Java是没有日志库的,打印日志全凭 System.outSystem.err

缺点:

  • 产生大量的IO操作同时在生产环境中无法合理的控制是否需要输出
  • 输出的内容不能保存到文件,只打印在控制台,打印完就过去了,除非一直盯着程序跑
  • 无法定制化,日志粒度不够细

2、第二阶段:Log4j

2001年,一个 ceki Gulcü 的大佬搞了一个日志框架 log4j 后来( log4j成为Apache 项目,Ceki加入Apache组织,Apache还曾经建议Sun引入Log4j到Java的标准库中,但Sun拒绝了.

3、第三阶段:JUL

sun不引入log4j,因为有自己的考虑,2002年2月JDK1.4发布,Sun推出了自己的日志标准库JUL(Java Util Logging),其实很多日志的实现思想沿用log4j。毕竟Log4j先出来很多年了,已经很成熟了此时,因此log4j更站优势,JUL还是在JDK1.5以后性能和可用性才有所提升

4、第四阶段:JCL

Apache: 玩编程,谁玩的过我!你不让我成为JDK标准,我就自己成为日志标准

2002年8月Apache推出了JCL(Jakarta Commons Logging),也就是日志抽象层,想一统日志抽象(就像JDBC一统数据库访问层),让日志产品去实现它的抽象,这样只要你的日志代码依赖的是JCL的接口,你就可以很方便的在Log4j和JUL之间做切换,支持运行时动态加载日志组件的实现,当然也提供一个默认实现Simple Log,( 在 ClassLoader 中进行查找,如果能找到Log4j则默认使用log4j实现,如果没有则使用JUL实现,再没有则使用JCL内部提供的 Simple Log实现 )。

但使用过程中,发现JCL存在以下缺点:

  • 效率较低
  • 容易引发混乱
  • 使用了自定义ClassLoader的程序中,使用JCL会引发内存泄露 由于Log4j比JUL好用,并且成熟,因此Log4j在选择上占据了一定优势

5、第五阶段:Slf4j

2006年巨佬Ceki( Log4j的作者)因为一些原因离开了Apache组织,之后Ceki觉得JCL不好用,自己写了一套新的日志标 准接口规范Slf4j (Simple Logging Facade Java),也可以称为日志门面,且Slf4j是对标JCL的。

但是由于Slf4j出来的较晚,而且还只是一个日志接口,所以之前已经出现的日志产品,如JUL和Log4j都是没有实现这个接口的,光有一个接口,没有实现的产品开发者想用Slf4j也是用不了,于是大佬Ceki Gülcü撸出了一系列的桥接包,使用桥接模式,通过桥接包来帮助Slf4j接口与其他日志库建立关系。代码使用Slf4j接口,就可以实现日志的统一标准化,后续如果想要更换日志实现,只需引入Slf4j与相关的桥接包,再引入具体的日志标准库即可。

但是之前很多Java应用应该依赖的JCL,所以光有日志产品桥接包不够,于是就有了JCL桥接包 此时的桥接包就是分了两种场景:

  1. 之前Java应用用的日志接口(如JCL)
  2. 之前Java应用用的日志产品(如Log4j)

通过SLF4j桥接到具体的日志框架实现

考虑一种场景:

假设你的Java应用使用了Spring的第三方的框架,但是假设Spring默认用JCL,并且最终用的JUL打印的日志,但是你的系统使用了Slf4j作为日志接口,日志产品使用了Log4j,那不出意外的话...你将有两种日志输出,两种日志的打印方式不统一,到时候解决bug的时候就很恼火,而且配置日志的配置文件还需要两份。

于是,Ceki Gülcü:建议大家都选择用Slf4j统一吧,没有事是桥接包解决不了的,又出现JCL桥接到Slf4j的桥接包

通过其他日志框架桥接到slf4j

6、第六阶段:Logback

Ceki巨佬觉得市场上的志产品都不是正统的Slf4j的实现,都是间接实现Slf4j接口也就是说每次都需要配合桥接包,因此在2006年,Ceki巨佬基于Slf4j接口写出了Logback日志标准库,做为Slf4j接口的默认实现,Logback 在功能完整度和性能上超越了所有已有的日志标准库,根本原因还在于,随着用户体量的提升,Log4j无法满足高性能的要求,成为应用的性能瓶颈.

7、第七阶段:Log4j2

在2012年,Apache直接推出新项目Log4j2,不是Log4j1.x升级因为Log4j2是完全不兼容Log4j1.x的。 Log4j2是Log4j的升级版,参考logback的一些优秀设计,还做了分离设计,分为log4j-api和log4j-core,log4j-api是日志接口,log4j-core是日志标准库,并且Apache也为Log4j2提供了各种桥接包。

Log4j2特色:

  • 性能提升: Log4/2包含基于LMAX Disruptor库的下一代异步记录器。在多线程方案中,与Log4 1.x和Logback相比,异步Logger的吞吐量高18倍,延迟降低了几个数量级
  • 自动重载配置:与Lopback一样,Log4 2可以在修改后自动重新加载其配置。与Lgback不同,它在进行重新配置时不会丢失日志事件
  • 无垃圾机制:在稳态日志记梁期间,Lg4 2 在独立应用程序中是无垃圾的,而在Web应用程序中是低垃圾的。这样可以减少垃圾收集器上的压力,并可以提供更好的响应时间性能

三、为什么学习Logback

目前常用的Java日志框架:Logback、log4j、log4j2,JUL等等,为什么选择Logback呢?

Logback是目前主流的日志框架,是springboot推荐的且默认集成的,是在log4j 的基础上重新开发的一套日志框架,是完全实现SLF4J接API(也叫日志门面),实用性更强;架构通用,可以应用于不同的环境。

四、Logback简介

目前logback分为三个模块,logback-core、logback-classic、logback-access.

  • logback-core 模块为其他两个模块奠定了甚础。
  • logback-classic模块 原生实现了SLF4J API,因此您可以轻松地在 logback 和其他日志记录框架(JUL)之间来回切换。
  • logback-access 模块与 Tomcat 和Jetty 等 Servlet 容器集成,以提供 HTTP 访问日志功能,请注意,您可以轻松地在 logback-core 之上构建自己的模块
xml 复制代码
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </dependency>

1、记录器Logger

日志记录器(Logger):控制要输出哪些日志记录语句,对日志信息进行级别控制 生产记录器实例: private static final Logger logger = LoggerFactory.getLogger("名称");

第一个记录器实例 使用缓存在map,单例模式;日志级别:TRACE<DEBUG<INFO<WARN<ERROR;在什么不配置下,默认输出debug及以上级别,且不能输出到文件

java 复制代码
public class Study {
    /**
     * 以下logger,logger2 ,logger3 三种等价
     * 参数name不仅名称,还有等级,上下级的含义
     */
    private static final Logger logger = LoggerFactory.getLogger("com.mall.portal.Study");
    /**
     * 使用类.class,会自动找 类.class.getName()
     */
    private static final Logger logger2 = LoggerFactory.getLogger(Study.class);

    private static final Logger logger3 = LoggerFactory.getLogger(Study.class.getName());

    public static void main(String[] args) {
        logger.trace("琵琶行");
        logger.debug("琵琶行");
        logger.info("琵琶行");
        logger2.warn("琵琶行");
        logger3.error("琵琶行");
    }
}

2、记录器层级结构

记录器底层有类似于java继承关系的层级关系,通过名称确定

3、记录器Logger的属性

  • name :记录器的名称(记录器缓存的map的key)
  • level: 记录器的级别,级别低到高 trace<debug<info<warn<error
  • additivity (可选):是否允许叠加打印日志, true或false
java 复制代码
    static ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.mall.portal.Study");

    public static void main(String[] args) {
        //设置级别
        logger.setLevel(Level.INFO);
        //查看你设置的级别(没有设置级别为空)
        System.out.println(logger.getLevel());
        //实际有效的级别(如果当前记录器设置级别,就是你设置的级别;如果没有设置级别,继承上级记录器或者Root记录器的级别)
        System.out.println(logger.getEffectiveLevel());
        logger.info("琵琶行");
    }

根记录器默认基本是DEBUG

java 复制代码
      static ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);

    static ch.qos.logback.classic.Logger comLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com");

    public static void main(String[] args) {
        System.out.println(rootLogger.getEffectiveLevel());
        rootLogger.setLevel(Level.INFO);
        System.out.println(comLogger.getEffectiveLevel());
    }

4、配置Logger记录器

xml 复制代码
<configuration xmlns="http://ch.qos.logback/xml/ns/logback"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback
                https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd">

    <!--设置根记录器级别为info,默认debug-->
    <root level="INFO"></root>
    <!--配置记录器 com.zhanlijuan 为warn级别 -->
    <logger name="com.zhanlijuan" level="WARN">
    </logger>
</configuration>
java 复制代码
    static ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);

    public static void main(String[] args) {
        System.out.println(rootLogger.getEffectiveLevel());
        ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.zhanlijuan");
        //warn
        System.out.println(logger.getEffectiveLevel());
        //info,继承根记录器
        ch.qos.logback.classic.Logger logger2 = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com");
        System.out.println(logger2.getEffectiveLevel());
    }

5、附加器Appender

记录器会将输出日志的任务交给附加器完成,不同的附加器会将日志输出到不同的地方,比如控制台附加器、文件附加器、网络附加器等等

  • 控制台附加器 ch.qos.logback.core.ConsoleAppender
xml 复制代码
<configuration xmlns="http://ch.qos.logback/xml/ns/logback"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback
                https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd"
>

    <springProperty scope="context" name="appName" source="spring.application.name"/>
    <!-- 日志存放路径 -->
    <property name="log.path" value="logs/sandbox"/>
    <!-- 日志输出格式 -->
    <property name="log.pattern" value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}|${appName}|${PID:- }|%level|%X{traceId}|%X{spanId}|%logger{39}|%method|%line: %msg%n"/>

    <!--控制台附加器-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>

    <!--配置记录器 com.zhanlijuan 为warn级别 -->
    <logger name="com.zhanlijuan" level="WARN" additivity="false">
        <!--将附加器加入到记录器中-->
        <appender-ref ref="STDOUT"/>
    </logger>

    <!--设置根记录器级别为info,默认debug-->
    <root level="INFO">
        <appender-ref ref="STDOUT"></appender-ref>
    </root>

</configuration>
java 复制代码
@Slf4j
public class Study {
    public static Logger zhanLogger =LoggerFactory.getLogger("com.zhanlijuan");

    public static void main(String[] args) {
        zhanLogger.warn("=================");
        log.info("=====================");
    }
}
  • 文件附加器 ch.qos.logback.core.FileAppender

只能生成一个日志文件,当文件很大时,检索日志昆虫,很少使用

xml 复制代码
<configuration xmlns="http://ch.qos.logback/xml/ns/logback"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback
                https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd"
>

    <springProperty scope="context" name="appName" source="spring.application.name"/>
    <!-- 日志存放路径 -->
    <property name="log.path" value="logs/sandbox"/>
    <!-- 日志输出格式 -->
    <property name="log.pattern"
              value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}|${appName}|${PID:- }|%level|%X{traceId}|%X{spanId}|%logger{39}|%method|%line: %msg%n"/>

    <!--控制台附加器-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>

    <!--文件附加器-->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <!--日志的文件名-->
        <file>${log.path}/msg.log</file>
        <!--true原有记录追加;false每次打印日志会覆盖以前的会清空以前的打印,只有本次打印-->
        <append>true</append>
    </appender>

    <!--配置记录器 com.zhanlijuan 为warn级别 -->
    <logger name="com.zhanlijuan" level="WARN" additivity="false">
        <!--将附加器加入到记录器中-->
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE"/>
    </logger>

    <!--设置根记录器级别为info,默认debug-->
    <root level="INFO">
        <appender-ref ref="STDOUT"></appender-ref>
        <appender-ref ref="FILE"></appender-ref>
    </root>
</configuration>
  • 滚动文件附加器 ch.qos.logback.core.rolling.RollingFileAppender

根据一定的滚动策略,生成一个日志文件,归档管理)比如,可根据时间每天、每月生成一个文件,还可根据大小生成文件,,更多使用

滚动策略:

  1. 基于时间的滚动策略 ch.qos.logback.core.rolling.TimeBasedRollingPolicy
xml 复制代码
<configuration xmlns="http://ch.qos.logback/xml/ns/logback"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback
                https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd"
>

    <springProperty scope="context" name="appName" source="spring.application.name"/>
    <!-- 日志存放路径 -->
    <property name="log.path" value="logs/sandbox"/>
    <!-- 日志输出格式 -->
    <property name="log.pattern"
              value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}|${appName}|${PID:- }|%level|%X{traceId}|%X{spanId}|%logger{39}|%method|%line: %msg%n"/>

    <!--控制台附加器-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>

    <!--滚动文件附加器-->
    <appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <!--日志的文件名,当前正常工作的文件-->
        <file>${log.path}/msg.log</file>
        <!--滚动策略-->
        <!--基于时间的滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--满足一定的策略将msg.log文件重命名新的文件进行归档-->
            <!--学习测试按秒生成一个文件,生产常按天生产一个文件-->
            <fileNamePattern>${log.path}/%d{yyyy-MM-dd HH-mm-ss}.log</fileNamePattern>
            <!--归档文件的最大数量,超过会将旧的文件删除,只保留最近的3个文件-->
            <maxHistory>3</maxHistory>
            <!--日志文件的滚动大小,超过会将旧的日志文件删除-->
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
        <!--true原有记录追加;false每次打印日志会覆盖以前的会清空以前的打印,只有本次打印-->
        <append>true</append>
    </appender>

    <!--配置记录器 com.zhanlijuan 为warn级别 -->
    <logger name="com.zhanlijuan" level="WARN" additivity="false">
        <!--将附加器加入到记录器中-->
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="RollingFile"/>
    </logger>

    <!--设置根记录器级别为info,默认debug-->
    <root level="INFO">
        <appender-ref ref="STDOUT"></appender-ref>
        <appender-ref ref="RollingFile"></appender-ref>
    </root>

</configuration>
java 复制代码
    private static final Logger logger = LoggerFactory.getLogger("com.zhanlijuan");
    @SneakyThrows
    public static void main(String[] args){
        while (true){
            logger.warn("琵琶行");
            //100毫秒暂停下
            Thread.sleep(100);
        }
    }
  1. 基于大小和时间的滚动策略 ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy
xml 复制代码
  <!--滚动文件附加器-->
    <appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>%date | %logger | %msg %n</pattern>
        </encoder>
        <!--日志的文件名,当前正常工作的文件-->
        <file>${log.path}/msg.log</file>
        <!--滚动策略-->
        <!--基于时间的滚动策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--满足一定的策略将msg.log文件重命名新的文件进行归档-->
            <!--学习测试按秒生成一个文件,生产常按天生产一个文件-->
            <fileNamePattern>${log.path}/%d{yyyy-MM-dd HH-mm}.%i.log</fileNamePattern>
            <!--归档模式的数量,不是指文件的数量,比如按分钟进行归档,是保留最近3分钟的日志-->
            <maxHistory>3</maxHistory>
            <!--每个归档的日志文件的大小-->
            <maxFileSize>10KB</maxFileSize>
            <!--日志文件的滚动大小,超过会将旧的日志文件删除-->
            <totalSizeCap>50GB</totalSizeCap>
        </rollingPolicy>
        <!--true原有记录追加;false每次打印日志会覆盖以前的会清空以前的打印,只有本次打印-->
        <append>true</append>
    </appender>

6、Pattern标签

Pattern由文字文本和转换说明符的格式控制表达式组成。您可以在其中自有插入任务文字文本。每个转换说明符都以百分号'%',后跟可选的格式修饰符、转换字和大括号之间的可选参数。转换字控制要转换的数据字段,例如记录器名称、级别、日期或线程名称。格式修饰符控制字段宽度、填充以及左对齐或右对齐。

7、自定义附加器

xml 复制代码
    <appender name="myAppender" class="com.mall.ali.study.MyAppender">
        <encoder>
            <pattern>%date | %logger | %msg %n </pattern>
        </encoder>
        <!--自定义文件附加器,生成的文件名-->
        <fileName>logs/sandbox/myAppender.log</fileName>
    </appender>

    <logger name="com.mall.ali.study.MyAppender" level="INFO" additivity="false">
        <!--将附加器加入到记录器中-->
        <appender-ref ref="myAppender"/>
    </logger>
java 复制代码
package com.mall.ali.study;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import ch.qos.logback.core.encoder.Encoder;
import cn.hutool.core.io.FileUtil;
import lombok.Data;
import lombok.SneakyThrows;
import java.io.File;

@Data
public class MyAppender<E> extends UnsynchronizedAppenderBase<E> {
    private Encoder encoder;

    private String fileName;

    @SneakyThrows
    @Override
    protected void append(E eventObject) {
        ILoggingEvent event=(ILoggingEvent)eventObject;
        byte[] encode = this.encoder.encode(event);
        String s = new String(encode, "utf-8");
        System.out.printf("MyAppender>>>>>>>>>>>>>>>>>>>>>"+s);
        File file=new File(this.fileName);
        FileUtil.appendString(s ,file,"utf-8");
    }
}

8、过滤器Filter

过滤器是附加器的一个组件,它用于决定附加器是否输出日志。一个附加器可以包含一个或多个过滤器。每个过滤器都会返回一个玫举值,可选的值: DENY(不输出日志)、 NEUTRAL(中立,不决定是否输出日志)、ACCEPT(输出日志),附加器根据过滤器返回值判断是否输出日志:

过滤器链中,一个过滤器能确定是否打印日志,下一个过滤器不在起作用,即返回neutral,会询问下一个过滤器是否输出日志

  • LevelFilter(级别过滤器):ch.qos.logback.classic.filter.LevelFilter
xml 复制代码
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${appName} |%date | %logger | %msg %n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <!--过滤器配置的级别INFO和我们代码log.info级别一致的情况下,返回标签 onMatch的值-->
            <onMatch>ACCEPT</onMatch>
            <!--过滤器配置的级别INFO和我们代码log.info级别不一致的情况下,返回标签 onMismatch的值-->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
java 复制代码
public class LevelFilter extends AbstractMatcherFilter<ILoggingEvent> {

    Level level;

    @Override
    public FilterReply decide(ILoggingEvent event) {
        if (!isStarted()) {
            return FilterReply.NEUTRAL;
        }

        if (event.getLevel().equals(level)) {
            return onMatch;
        } else {
            return onMismatch;
        }
    }

    public void setLevel(Level level) {
        this.level = level;
    }

    public void start() {
        if (this.level != null) {
            super.start();
        }
    }
}
  • ThresholdFilter(阈值过滤器):ch.qos.logback.classic.filter.ThresholdFilter
xml 复制代码
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${appName} |%date | %logger | %msg %n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!--代码log.info打印的级别,大于等于配置的级别,返回 NEUTRAL,小于返回DENY-->
            <level>INFO</level>
        </filter>
    </appender>
java 复制代码
public class ThresholdFilter extends Filter<ILoggingEvent> {

    Level level;

    @Override
    public FilterReply decide(ILoggingEvent event) {
        if (!isStarted()) {
            return FilterReply.NEUTRAL;
        }

        if (event.getLevel().isGreaterOrEqual(level)) {
            return FilterReply.NEUTRAL;
        } else {
            return FilterReply.DENY;
        }
    }
   }

多个过滤器

xml 复制代码
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${appName} |%date | %logger | %msg %n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!--代码log.info打印的级别,大于等于配置的级别,返回 NEUTRAL,小于返回DENY-->
            <level>INFO</level>
        </filter>
        <!--阈值过滤器返回NEUTRAL,询问级别过滤器是否打印日志-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <!--过滤器配置的级别INFO和我们代码log.info级别一致的情况下,返回标签 onMatch的值-->
            <onMatch>ACCEPT</onMatch>
            <!--过滤器配置的级别INFO和我们代码log.info级别不一致的情况下,返回标签 onMismatch的值-->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

9、自定义过滤器

实现抽象类 ch.qos.logback.core.filter.Filter

xml 复制代码
      <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${appName} |%date | %logger | %msg %n</pattern>
        </encoder>
        <filter class="com.mall.ali.study.MyFilter">
            <!--打印的日志信息中包含 【琵琶行】 就输出,否则不输出-->
            <keywords>琵琶行</keywords>
        </filter>
    </appender>
java 复制代码
package com.mall.ali.study;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
import lombok.Data;

/**
 * 1、打印的日志信息中包含 【琵琶行】 就输出,否则不输出
 */
@Data
public class MyFilter extends Filter<ILoggingEvent> {

    private String keywords;

    @Override
    public FilterReply decide(ILoggingEvent event) {
        if(event.getMessage().contains(keywords)){
            return FilterReply.ACCEPT;
        }
        return FilterReply.DENY;
    }
}

10、记录器可叠加性

记录器的层级结构,当使用一个记录器打印日志时,上级所有的记录器也会输出日志

当使用com.zhanlijuan记录器打印日志时,com.zhanlijuan会打印一条日志,com记录器会打印一条日志,根记录器也会打印一条日志,以下例子打印两条日志,是由于com记录器没有配置附加器,如果配置了com记录器附加器会打印3条日志(这个自行验证)

五、mybatis日志打印原理

1、认识logging目录

mybatis的logging目录,除了jdbc目录,还有7个子目录,每一个子目录代表一种日志打印工具

2、适配器模式

由于日志框架发展史,java中有很多日志框架Log4j、JUL、Logback、Log4j2都有按照不同级别打印日志类似功能,但是却没有统一的接口,不像JDBC一开始就制定了数据库操作接口规范,最后才出现统一的日志门面Slf4j,提供一系列标准接口。Mybatis内部定义了一套统一的日志接口规范 org.apache.ibatis.logging.Log 供上层使用,但是常用的日志框架的对外接口各不相同。Mybatis为了复用和集成这些第三方日志组件,在其日志模块中,提供了多种Adapter,支持对应的不同版本的日志框架,将这些第三方日志组件对外接口适配成org.apache.ibatis.logging.Log,这样Myabtis 就可以通过Log接口调用第三方日志了。

适配器是将一个类的接口转换成客户端所期望的另一个接口,从而便原本因为接口不匹配而无法一起工作的两个类能在一起工作。通俗讲:有一个Source类,拥有一个方法,待适配,目标接口是Target,通过Adapter类,将 Source的功能扩展到Target里,主要应用在源接口和目标接口已经存在,不易修改源代码,支持新版本和老版本的兼容,新增扩展性

  • 适配器模式角色划分

源(Adaptee) :需要被适配的对象或类型 org.apache.ibatis.logging.Log

适配器(Adapter) :通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。 org.apache.ibatis.logging.slf4j.Slf4jImpl

目标(Target) :期待得到的目标,客户端所期望的接口,面向日志门面编程 org.slf4j.Logger

3、工厂模式实现适配器实例的创建

工厂模式是创建型设计模式的一种,提供了统一的接口按需创建对象,目地隐藏对外创建类的细节与过程,遵循迪米特原则,面向工厂完成对象的构建工作。

LogFactory创建适配器的工厂

  • 在static代码块中根据逐个引入日志打印工具jar包中的日志类,先判断如果全局变量logConstructor为空,则加载并获取相应的构造器,如果可以获取到则赋值给全局变量logConstructor。
  • 如果全局变量logConstructor不为空,则不继续获取构造器
  • 根据getLog方法获取Log实例
  • 通过Log实例的具体日志方法打印日志
java 复制代码
package org.apache.ibatis.logging;
import java.lang.reflect.Constructor;
/**
 * mybatis 日志工厂,隐藏创建日志实例实现的细节
 */
public final class LogFactory {
  /**
   * Marker to be used by logging implementations that support markers.
   */
  public static final String MARKER = "MYBATIS";
  /**
   * 记录当前使用的第二方日志组件所对应的适配器的构造方法
   */
  private static Constructor<? extends Log> logConstructor;
  /*
   * 首先在类加载时,会执行所有的默认开源日志组件,会尝试着去寻找对应的适配者,是否引入了对应的日志框架的jar包,如果我们工程中引入了对应的日志框架的jar包
   * 那么其对应的适配器的构造器就能被正确的获取,随后就能在mybatis中利用适配器来实现日志功能了。
   * 日志加载顺序优先级:slf4J → commonsLoging → Log4J2 → Log4J → JdkLog->useNoLogging(禁用日志功能),直到找到一个。
   */
  static {
    // 逐个尝试,判断使用哪个Log的实现类,即初始化logConstructor属性,为空去执行对应的useXXXLogging方法,创造日志打印工具实列
    tryImplementation(LogFactory::useSlf4jLogging);
    tryImplementation(LogFactory::useCommonsLogging);
    tryImplementation(LogFactory::useLog4J2Logging);
    tryImplementation(LogFactory::useLog4JLogging);
    tryImplementation(LogFactory::useJdkLogging);
    //logConstructor为空,禁用日志功能,源码Do Nothing
    tryImplementation(LogFactory::useNoLogging);
  }

  /**
   * 尝试启用日志组件方法,首先会检测 logConstructor 字段是否为空,
   * 1.如果不为空,则表示已经成功确定当前使用的日志框架,直接返回;
   * 2.如果为空,则在当前线程中执行传入的 Runnable.run() 方法,尝试确定当前使用的日志框架
   */
  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        //执行调用方法(也就是static "::"的调用)
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  /**
   * 根据传入的实现类,获取对应的构造函数candidate,并根据candidate获取log实例,正常执行完,并赋值给logConstructor
   */
  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      // 尝试初始化对应的适配器,适配器中引入了其他日志框架的API,如果没有导入对应的jar包就会出现异常,导致当前适配器初始化失败
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      //打印级别开启,输出使用的适配器
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      //设置logConstructor,一旦设上,表明找到相应的log的jar包了,那后面别的log就不找了
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }

  private LogFactory() {
    // disable construction
  }
  
  public static Log getLog(Class<?> clazz) {
    return getLog(clazz.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }

  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }

  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }

  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }

  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }

  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }

  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }
  public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }
}

注意:mybatis 提供的日志工具除jdbc包外有7个,但是静态代码块加载的只有6个,但是工厂里面定义了useXXXLogging有8个,对比发现静态代码块加载少了useStdOutLogging ,工厂定义多啦useCustomLogging

useStdOutLogging :使用的是StdOutImpl,因为它是通过JDK自带的System类的方法打印日志的,无需引入额外的jar包,所以不参与static代码块中的判断 useCustomLogging:提供一个扩展功能,static块提供的6种打印日志的方法不满足要求时,传入一个实现了mybatis统一的接口的实现类,实现自定义的扩展

通过配置文件指定的日志实现类,配置文件在Mybatis初始化的过程中被读入,通过XMLConfigBuilder调用loadCustomLogImpl->Configuration.setLogImpl->LogFactory.useCustomLogging完成日志实现类的初始化,直接调用setImplementation(clazz);方法无条件覆盖掉logConstructor,从而实现配置文件的最高优先级。

4、JDK动态代理实现了SQL日志输出

在mybatis中对执行jdbc操作日志记录的本质是创建了相关核心对象的代理对象来完成日志的操作,然后调用相关的目标对象来完成数据库的操作处理

BaseJdbcLogger

抽象父类提供可复用功能

ConnectionLogger

代理 Connection 对象,继承了 BaseJdbcLogger 并实现了 InvocationHandler 接口,封装了真正的 Connection 对象,负责打印连接信息和sql语句,并创建PreparedStatementLogger、StatementLogger代理对象

java 复制代码
package org.apache.ibatis.logging.jdbc;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.reflection.ExceptionUtil;
/**
 * Connection proxy to add logging.
 * 增强类,实现InvocationHandler接口,进行方法的增强
 */
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler {
  /**
   * 需要代理的目标对象
   */
  private final Connection connection;

  /**
   * 调用父类构造器用传入的Log 对象和 SQL 层次进行初始化。
   */
  private ConnectionLogger(Connection conn, Log statementLog, int queryStack) {
    super(statementLog, queryStack);
    this.connection = conn;
  }

  /**
   *
   * 代理对象需执行的方法,实现目标方法的执行和功能增强
   * @param proxy 代理实例,可通过newProxyInstance 创建代理实例
   * @param method 执行目标方法,invoke方法执行
   * @param params 被调用方法的参数数组
   */
  @Override
  public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
    try {
      //如果是调用Object继承过来的方法,调用toString,hashCode,equals等
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }
      //如果调用的是preparedStatement()方法、prepareCall()方法或createStatement()方法,则创建PreparedStatement对象后,为其创建代理对象并返回该代理对象
      if ("prepareStatement".equals(method.getName()) || "prepareCall".equals(method.getName())) {
        //开启debug级别,打印sql执行的语句
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeExtraWhitespace((String) params[0]), true);
        }
        //调用底层封装的Connection对象的prepareStatement()方法,得到PreparedStatement对象
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        //返回PreparedStatement对象的代理对象,让PreparedStatement也具备打印日志的功能
        return PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
      }
      //思路同prepareStatement
      if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        //返回Statement对象的代理实例,让Statement也具备打印日志的功能
        return StatementLogger.newInstance(stmt, statementLog, queryStack);
      }
      //其他方法则直接调用底层Connection对象的相应方法,没有创建代理对象
      return method.invoke(connection, params);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

  /**
   * 调用者生成Connection代理对象
   */
  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    //创建Connection代理实例对象
    return (Connection) Proxy.newProxyInstance(cl, new Class[] { Connection.class }, handler);
  }

  public Connection getConnection() {
    return connection;
  }

}

PreparedStatementLogger

代理PreparedStatement对象,负责打印参数信息,并创建ResultSetLogger代理对象

StatementLogger 原理与PreparedStatementLogger类似

ResultSetLogger

代理ResultSet对象,负责打印数据结果集

相关推荐
东风t西瓜8 小时前
飞书项目与多维表格双向同步
后端
初次攀爬者8 小时前
Kafka的Rebalance基础介绍
后端·kafka
ServBay8 小时前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
IronixPay8 小时前
Telegram Bot 接入 USDT 支付完整教程
后端
IronixPay8 小时前
Next.js + USDT:15 分钟给你的 SaaS 加上加密货币支付
后端
董员外9 小时前
LangChain.js 快速上手指南:Tool的使用,给大模型安上了双手
前端·javascript·后端
会员源码网9 小时前
使用`mysql_*`废弃函数(PHP7+完全移除,导致代码无法运行)
后端·算法
洛森唛10 小时前
ElasticSearch查询语句Query String详解:从入门到精通
后端·elasticsearch
用户83071968408210 小时前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
小兔崽子去哪了10 小时前
Java 自动化部署
java·后端