日志-Log4j,Logback,Log4j2和Slf4j日志框架的结构原理

前言

本篇文章记录对Log4jLogbackLog4j2Slf4j日志框架的结构原理的学习。

正文

一. 整体结构

如果单独使用Log4jLogbackLog4j2日志框架来进行日志打印,那么使用方式可以如下所示。

  • Log4j
import org.apache.log4j.Logger;

public class Log4jTest {

    private static final Logger logger_log4j
            = Logger.getLogger(Log4jTest.class);

    public static void main(String[] args) {
        logger_log4j.info("Test Log4j info.");
        logger_log4j.warn("Test Log4j warn.");
        logger_log4j.error("Test Log4j error.");
    }

}

单独使用Log4j时,需要引入的依赖如下。

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.16</version>
</dependency>

只需要引入上述依赖,就可以使用Log4j进行日志打印,但是如果要将Log4jSlf4j进行整合使用,还需要引入下述桥接包。

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>
  • Logback
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogbackTest {

    private static final Logger logger_logback
            = LoggerFactory.getLogger(LogbackTest.class);

    public static void main(String[] args) {
        logger_logback.info("Test Logback info.");
        logger_logback.warn("Test Logback warn.");
        logger_logback.error("Test Logback error.");
    }

}

Logback单独使用时,只需要引入下述依赖。

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>
  • Log4j2
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Log4j2Test {

    private static final Logger logger_log4j2
            = LogManager.getLogger(Log4j2Test.class);

    public static void main(String[] args) {
        logger_log4j2.info("Test Log4j2 info.");
        logger_log4j2.warn("Test Log4j2 warn.");
        logger_log4j2.error("Test Log4j2 error.");
    }

}

单独使用Log4j2时,需要引入的依赖如下。

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.17.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.17.1</version>
</dependency>

同理,如果需要与Slf4j进行整合使用,那么还需要引入下述桥接包。

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.17.1</version>
</dependency>

无论使用哪种日志框架,核心的抽象概念都为LoggerAppenderFormatter,其中Logger提供日志打印行为,Appender配置日志的打印位置,Formatter配置日志的打印格式,这三者的抽象类图可以由下述类图描述。

由于Log4jLog4j2无法满足Slf4j定义的接口,所以为了在不修改已有代码的情况下还能提供对Slf4j定义的接口的实现,Log4jLog4j2还需要引入桥接包才能和Slf4j进行整合使用。整个结构可以用下图进行概括。

二. 源码简析

本小节将从LoggerFactory.getLogger(类.class)出发,分析Slf4j是如何决定使用哪种日志框架的。

LoggerFactorygetLogger()方法如下所示。

public static Logger getLogger(Class<?> clazz) {
    Logger logger = getLogger(clazz.getName());
    
    ......
    
    return logger;
}

public static Logger getLogger(String name) {
    // ILoggerFactory接口的实现类由Log4j,Log4j2的桥接包以及Logback提供
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

getLogger()方法中会调用到getILoggerFactory()方法来获取到ILoggerFactory接口的实现类,Log4jLog4j2的桥接包以及Logback都提供了ILoggerFactory接口的实现类,所以下面继续跟进getILoggerFactory()方法。

public static ILoggerFactory getILoggerFactory() {
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                // 执行绑定逻辑,初始化StaticLoggerBinder
                // Log4j,Log4j2的桥接包以及Logback都会提供StaticLoggerBinder
                performInitialization();
            }
        }
    }
    switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
        // 通过StaticLoggerBinder得到ILoggerFactory的实现类
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    case NOP_FALLBACK_INITIALIZATION:
        return NOP_FALLBACK_FACTORY;
    case FAILED_INITIALIZATION:
        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
    case ONGOING_INITIALIZATION:
        return SUBST_FACTORY;
    }
    throw new IllegalStateException("Unreachable code");
}

getILoggerFactory()方法中会先调用到performInitialization()方法来完成StaticLoggerBinder的初始化,然后再调用StaticLoggerBindergetLoggerFactory()方法得到ILoggerFactory,下面跟进performInitialization()方法。

private final static void bind() {
    try {
        Set<URL> staticLoggerBinderPathSet = null;
        if (!isAndroid()) {
            // 获取所有StaticLoggerBinder的路径
            staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            // 如果存在多个StaticLoggerBinder的路径,则全部打印出来
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
        }
        // 这里执行日志框架的绑定
        StaticLoggerBinder.getSingleton();
        // 将初始化状态置为3,表示成功完成StaticLoggerBinder的初始化,也即成功完成日志框架的绑定
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
        // 打印实际绑定的是哪个日志框架
        reportActualBinding(staticLoggerBinderPathSet);
        fixSubstituteLoggers();
        replayEvents();
        SUBST_FACTORY.clear();
    } catch (NoClassDefFoundError ncde) {
    
        ......
        
    } catch (java.lang.NoSuchMethodError nsme) {
    
        ......
        
    } catch (Exception e) {
    
        ......
        
    }
}

上述bind()方法会从classpath下的jar包中寻找StaticLoggerBinder,然后将所有StaticLoggerBinder的路径存放在集合中,如果存在多个StaticLoggerBinder的路径,则将这些路径信息打印出来,然后完成与StaticLoggerBinder的初始化,一旦StaticLoggerBinder完成初始化,则也完成了与实际的日志框架的绑定,后续通过该日志框架提供的StaticLoggerBinder获取ILoggerFactory,再通过获取到的ILoggerFactory获取到对应日志框架对Logger接口的实现类。

整个调用时序图如下所示。

总结

由于单独使用Log4jLogbacklog4j2日志框架时,每个日志框架的使用各不相同,所以为了减少切换日志框架时造成的代码入侵,采用了Slf4j作为日志框架的统一门面,所以可以认为Slf4j定义了日志框架的接口,每种日志框架提供接口的实现。
同时,由于Log4jLog4j2无法满足Slf4j定义的接口,所以如果要将Log4jLog4j2Slf4j整合使用,还需要引入Log4jLog4j2的桥接包,以达到既能完成与Slf4j整合,还能不修改原有日志框架的代码的功能。

44 声望
23 粉丝
0 条评论
推荐阅读
数据库连接池-Druid数据库连接池源码解析
本文将对Druid数据库连接池的源码进行分析和学习,以了解Druid数据库连接池的工作原理。Druid数据库连接池的基本逻辑几乎全部在DruidDataSource类中,所以本文主要是围绕DruidDataSource的各项功能展开论述。

半夏之沫阅读 1.2k

百度工程师浅谈分布式日志
导读 我们做软件开发时,或多或少的会记录日志。由于日志不是系统的核心功能,常常被忽视,定位问题的时候才想起它。本文由浅入深的探讨不起眼的日志是否重要,以及分布式架构下的日志运维工具应该具备哪些能力,...

百度Geek说3阅读 348

日志-log4j2基于AsyncLogger的异步日志打印
在日志-log4j2基于AsyncAppender的异步日志打印一文中,分析了Log4j2如何基于AsyncAppender来实现异步日志打印,本篇文章将分析Log4j2如何基于AsyncLogger来实现异步日志打印。

半夏之沫阅读 1.1k

日志-log4j2基于AsyncAppender的异步日志打印
在日志-log4j2日志框架源码学习一文中,对Log4j2的整体结构和同步打印流程进行了一个初步学习,本篇文章将对Log4j2异步打印流程进行学习。在同步日志打印中,应用业务逻辑与日志打印在一个线程中,应用后续业务逻...

半夏之沫阅读 1k

WAL的概念与实践分析
在计算机科学中,预写式日志[1](Write-Ahead logging,缩写 WAL)是关系数据库系统中用于提供原子性和持久性(事务ACID属性中的A与D)的一系列技术。在使用WAL的系统中,所有的修改在生效之前都要先写入log文件...

KaiwuDB阅读 837

封面图
如何优雅地记录操作日志
日志对于一个系统来说不可或缺,对于问题的排查,问题的再现有着至关重要的作用。由于不想从日志文件一行行摸索,想追求简单、快捷、方便;只需根据条件就可以检索到相应接口日志,以及是否出现报错的情况。同时...

极简博客阅读 801

封面图
【JAVA】Java 日志打印规范
规范的日志是养成良好编程习惯的开始,也是关键时刻解决严重BUG的救命稻草。程序员开发的过程中可以打印debug日志,在复杂业务中提供日志来排查问题,也可以在出现生产问题的时候快速问题,及时处理。无论如何了...

Xander阅读 718

44 声望
23 粉丝
宣传栏