深度剖析slf4j源码

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 深度剖析slf4j源码

01 引言

博主在前面的文章里主要阐述了Java日常开发中常用到的日志框架以及logback日志框架实现,有兴趣的童鞋可以参阅:

日志在日常开发中我们是必定会接触到的,似乎我们只知道使用logger.infologger.error等去打印日志,然后就不去深入了解了。

这也是本文的目的,一起来走走它的源码,揭开它的神秘面纱。

02 slf4j 使用案例

在阅读源码前,我们看看slf4j的使用案例。

2.1 引入slf4j接口

使用slf4j的方法很简单,我们只需要在项目里面添加依赖(这里以maven项目为例子),然后直接使用即可。

使用的版本根据官网建议的稳定版本(即2.0.9版本):https://www.slf4j.org/download.html

新建maven项目之后,添加依赖:

然后随便写点测试代码:

/**
 * slf4j测试
 *
 * @author : YangLinWei
 * @createTime: 2023/10/8 09:44
 * @version: 1.0.0
 */
public class Slf4jTest {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
        logger.info("Current Timestamp: {}", System.currentTimeMillis());
    }
}

运行之后,会发现无法打印日志:

根据错误提示,我们知道没有找到对应的SLF4J实现,在前面的章节,我们知道SLF4J只是接口,这当然是无法打印日志的,所以我们还需要引入对应的日志框架实现:

上述是官网的实现SLF4J的日志框架实现,这里我们以logback来继续讲解。

2.2 引入实现slf4j的日志框架

目前项目只依赖了slf4j-api,继续依赖logback框架:

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

再次运行,可以看到正常打印日志了:

如果依赖多个日志实现呢?比如这里继续依赖log4j2:

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>2.0.9</version>
</dependency>

再次运行,可以看到日志虽然有打印,但是会报红:

这种情况是不是在我们日常的开发中很常见?其实这里的意思指的是有多个实现的日志框架,找到的有“ch.qos.logback.classic.spi.LogbackServiceProvider”、“org.slf4j.reload4j.Reload4jServiceProvider”,然后使用的是“ch.qos.logback.classic.spi.LogbackServiceProvider”。

这种情况的解决方式,可以参考:https://www.slf4j.org/codes.html#multiple_bindings。其实就是移除依赖,或依赖的包如果存在不需要的日志框架,直接排除即可,如下图:

也就是说,只能保留一种实现SLF4J的日志框架,如果存在多个,会报红。同时这也是SLF4J设计的巧妙之处,我们如果要替换框架,只需要修改日志框架的实现即可,java代码无需修改。

这种情况,博主在前面的文章里也有写过具体的解决方式:《SpringBoot 日志终极解决方案》

2.3 其它

SpringBoot项目也引入了日志的starter(即:spring-boot-starter-logging),进入里面的pom文件,可以看到它的内容下:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starters</artifactId>
    <version>2.1.17.RELEASE</version>
  </parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-logging</artifactId>
  <version>2.1.17.RELEASE</version>
  <name>Spring Boot Logging Starter</name>
  <description>Starter for logging using Logback. Default logging starter</description>
  <url>https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-starters/spring-boot-starter-logging</url>
  <organization>
    <name>Pivotal Software, Inc.</name>
    <url>https://spring.io</url>
  </organization>
  <licenses>
    <license>
      <name>Apache License, Version 2.0</name>
      <url>https://www.apache.org/licenses/LICENSE-2.0</url>
    </license>
  </licenses>
  <developers>
    <developer>
      <name>Pivotal</name>
      <email>info@pivotal.io</email>
      <organization>Pivotal Software, Inc.</organization>
      <organizationUrl>https://www.spring.io</organizationUrl>
    </developer>
  </developers>
  <scm>
    <connection>scm:git:git://github.com/spring-projects/spring-boot.git</connection>
    <developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git</developerConnection>
    <url>https://github.com/spring-projects/spring-boot</url>
  </scm>
  <issueManagement>
    <system>Github</system>
    <url>https://github.com/spring-projects/spring-boot/issues</url>
  </issueManagement>
  <dependencies>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.3</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-to-slf4j</artifactId>
      <version>2.11.2</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jul-to-slf4j</artifactId>
      <version>1.7.30</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

依赖配置表明,spring-boot-starter-logging 依赖同时包括了 LogbackLog4j 2SLF4J 的相应依赖项,Logback作为默认的日志实现,这是SpringBoot的默认配置,当然也可以配置Log4j 2作为默认日志实现,这里就不再详述了。

备注:jul-to-slf4j 这个依赖项是为了集成 Java Util Logging (JUL) 和 SLF4J。其中JUL 是 Java 标准库中的日志框架,通过这个依赖,它将被桥接到 SLF4J,从而允许你在应用程序中使用 SLF4J 接口来记录 JUL 的日志消息。

03 slf4j 源码剖析

3.1 ServiceLoader

通过阅读slf4j的文档(https://www.slf4j.org/manual.html),可以知道从2.0.0版本开始,slf4j是通过ServiceLoader机制来寻找其具体的日志实现的。

关于ServiceLoader,这里不展开描述,有兴趣的童鞋可以查看文档:https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html

ok,直接进入slf4j的源码分析。

3.2 源码分析

源码地址:https://github.com/qos-ch/slf4j

clone源码到本地,我们只需要看slf4j-api这块的源码:

再来看看前面使用log4j的代码:

/**
 * slf4j测试
 *
 * @author : YangLinWei
 * @createTime: 2023/10/8 09:44
 * @version: 1.0.0
 */
public class Slf4jTest {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
        logger.info("Current Timestamp: {}", System.currentTimeMillis());
    }
}

可以看到Logger对象是通过LoggerFactory工厂去获取的,进入方法查看:

/*** 
 * 返回一个与传递的类参数相对应的名称的日志记录器,使用静态绑定的 {@link ILoggerFactory} 实例。
 *
 * <p>
 * 如果clazz参数与由SLF4J内部计算的调用者名称不同,那么只有在系统属性slf4j.detectLoggerNameMismatch设置为true时,才会打印日志名称不匹配警告。
 * 默认情况下,此属性未设置,即使存在日志名称不匹配,也不会打印警告。
 *
 * @param clazz 返回的日志记录器将以clazz的名称命名
 * @return 日志记录器
 *
 * @author : YangLinWei
 * @createTime: 2023/10/8 11:06
 * @version: 1.0.0
 */
public static Logger getLogger(Class<?> clazz) {
    // 获取日志记录器
    Logger logger = getLogger(clazz.getName());
    // 其它校验
    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;
}

继续查看getLogger方法(获取日志记录器):

/** 
 * 返回一个根据名称参数命名的日志记录器,使用静态绑定的 {@link ILoggerFactory} 实例。
 *
 * @param name 日志记录器的名称。
 * @return 日志记录器
 * 
 * @author : YangLinWei
 * @createTime: 2023/10/8 11:06
 * @version: 1.0.0
 */
public static Logger getLogger(String name) {
    // 获取ILoggerFactory实例
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

继续查看getILoggerFactory方法:

/**
 * 返回当前正在使用的 {@link ILoggerFactory} 实例。
 *
 * ILoggerFactory 实例在编译时与此类绑定。
 * 
 * @return 正在使用的 ILoggerFactory 实例
 * 
 * @author : YangLinWei
 * @createTime: 2023/10/8 11:06
 * @version: 1.0.0
 */
public static ILoggerFactory getILoggerFactory() {
    // 可以看到,这里准备开始获取对应的日志实现了
    return getProvider().getLoggerFactory();
}

点击查看getProvider方法:

/**
 * 获取正在使用的provider
 * @author : YangLinWei
 * @createTime: 2023/10/8 11:06
 * @version: 1.0.0
 * 
 */
static SLF4JServiceProvider getProvider() {
    if(INITIALIZATION_STATE == UNINITIALIZED) { // provider未初始化,进行初始化操作
        synchronized(LoggerFactory.class) {
            if(INITIALIZATION_STATE == UNINITIALIZED) {
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                performInitialization(); // 这里进行初始化操作
            }
        }
    }
    switch(INITIALIZATION_STATE) { // 根据状态,返回不同的内容
        case SUCCESSFUL_INITIALIZATION:
            return PROVIDER;
        case NOP_FALLBACK_INITIALIZATION:
            return NOP_FALLBACK_SERVICE_PROVIDER;
        case FAILED_INITIALIZATION:
            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
        case ONGOING_INITIALIZATION:
            // support re-entrant behavior.
            // See also http://jira.qos.ch/browse/SLF4J-97
            return SUBST_PROVIDER;
    }
    throw new IllegalStateException("Unreachable code");
}

到这里,可以查看到时如何初始化不同的日志实现了,进入performInitialization方法:

/**
 * 初始化Provider
 *
 * @author : YangLinWei
 * @createTime: 2023/10/8 11:16
 * @version: 1.0.0
 */
private final static void performInitialization() {
    bind();
    if(INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
        versionSanityCheck();
    }
}
/**
 * 绑定Provider
 *
 * @author : YangLinWei
 * @createTime: 2023/10/8 11:17
 * @version: 1.0.0
 */
private final static void bind() {
    try {
        List<SLF4JServiceProvider> providersList = findServiceProviders(); // 查找可以用的provider日志实现
        reportMultipleBindingAmbiguity(providersList);
        if(providersList != null && !providersList.isEmpty()) { // 如果provider日志实现为空,设置装填并报告
            PROVIDER = providersList.get(0);
            // SLF4JServiceProvider.initialize() is intended to be called here and nowhere else.
            PROVIDER.initialize();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(providersList);
        } else { 
            INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
            Util.report("No SLF4J providers were found.");
            Util.report("Defaulting to no-operation (NOP) logger implementation");
            Util.report("See " + NO_PROVIDERS_URL + " for further details.");
            Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
        }
        postBindCleanUp();
    } catch (Exception e) {
        failedBinding(e);
        throw new IllegalStateException("Unexpected initialization failure", e);
    }
}

可以看到,这里的日志,就是前面没有添加日志实现依赖时的打印内容,继续进入findServiceProviders方法看看:

/**
 * 查找并返回 SLF4JServiceProvider的列表。
 *
 * 这个方法用于查找并加载 SLF4JServiceProvider,以便在运行时提供日志记录功能。
 * 它会优先尝试加载通过配置明确指定的服务提供者,然后再使用 Java 的 ServiceLoader 查找其他服务提供者。
 *
 * @return SLF4J 服务提供者的列表
 * 
 * @author : YangLinWei
 * @createTime: 2023/10/8 11:17
 * @version: 1.0.0
 */
static List<SLF4JServiceProvider> findServiceProviders() {
    List<SLF4JServiceProvider> providerList = new ArrayList<>();
    //使用加载当前类的类加载器来搜索Service。
    final ClassLoader classLoaderOfLoggerFactory = LoggerFactory.class.getClassLoader();
    // 尝试加载通过配置明确指定的ServiceProvider
    SLF4JServiceProvider explicitProvider = loadExplicitlySpecified(classLoaderOfLoggerFactory);
    if (explicitProvider != null) {
        providerList.add(explicitProvider);
        return providerList;
    }
    // 使用 Java 的 ServiceLoader 查找其他服务提供者
    ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);
    Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();
    while (iterator.hasNext()) {
        safelyInstantiate(providerList, iterator);
    }
    return providerList;
}

通过阅读以上的代码,可以知道slf4j通过ServiceLoader加载不同的日志框架,而不同的日志框架都需要实现SLF4JServiceProvider接口,也就是所谓的“门面模式”。

3.3 SLF4JServiceProvider

继续查看SLF4JServiceProvider有哪些接口:

/**
 * 这个接口基于java.util.ServiceLoader的范式。
 * 
 * 它取代了在 SLF4J 版本 1.0.x 到 1.7.x 中使用的旧的静态绑定机制。
 *
 * @author : YangLinWei
 * @createTime: 2023/10/8 11:17
 * @version: 1.0.0
 */
public interface SLF4JServiceProvider {
    /**
     * 返回org.slf4j.LoggerFactory类应该绑定到的ILoggerFactory实例。
     * 
     * @return ILoggerFactory实例
     */
    public ILoggerFactory getLoggerFactory();
    /**
     * 返回org.slf4j.MarkerFactory 类应该绑定到的IMarkerFactory实例。
     * 
     * @return IMarkerFactory实例
     */
    public IMarkerFactory getMarkerFactory();
    /**
     * 返回MDC应该绑定到的MDCAdapter实例。
     * 
     * @return MDCAdapter实例
     */
    public MDCAdapter getMDCAdapter();
    /**
     * 返回日志实现支持的 SLF4J 的最大 API 版本。
     *
     * 例如:"2.0.1"
     *
     * @return 字符串形式的 API 版本
     */
    public String getRequestedApiVersion();
    /**
     * 初始化日志后端。
     * 
     * 注意:这个方法预期只会被LoggerFactory类调用,不应该从其他地方调用。
     * 
     */
    public void initialize();
}

例如:logback对应的实现如下

后续的细节不再赘述了。

04 文末

本文主要是讲解SLF4J的使用以及源码流程,博主主要是想表达的是slf4j是用的是ServiceLoader机制来动态加载不同的日志实现的,这也是实现门面设计模式的一个常用技巧。

相信大家还记得之前流行的log4j漏洞吧?如果使用了本文的门面模式,直接替换依赖就完美解决了,而无需再去修改代码。

log4j漏洞公告相关新闻:https://s.tencent.com/research/report/144

希望本文能帮助到大家,谢谢大家的阅读,本文完!

目录
相关文章
|
设计模式 Java 程序员
日志框架Slf4j作用及其实现原理
日志框架Slf4j作用及其实现原理
136 0
|
3月前
|
存储 监控 Java
Java日志通关(三) - Slf4j 介绍
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第三篇。
|
3月前
|
JavaScript Java API
Java日志通关(二) - Slf4j+Logback 整合及排包
作者日常在与其他同学合作时,经常发现不合理的日志配置以及五花八门的日志记录方式,后续作者打算在团队内做一次Java日志的分享,本文是整理出的系列文章第二篇。
|
6月前
|
消息中间件 Java API
【JavaEE进阶】 关于⽇志框架(SLF4J)
【JavaEE进阶】 关于⽇志框架(SLF4J)
|
设计模式 安全 Java
深度剖析slf4j源码
深度剖析slf4j源码
231 3
|
Java 数据处理 API
java日志框架详解-slf4j
java日志框架详解-slf4j
157 0
|
设计模式 缓存 Java
logback之 AsyncAppender 的原理、源码及避坑建议
AsyncAppender 接收日志,放入其内部的一个阻塞队列,专开一个线程从阻塞队列中取数据(每次一个)丢给链路下游的Appender 如 FileAppender,如此可把日志写盘 变成 日志写内存,减少写日志的 RT。
1165 0
logback之 AsyncAppender 的原理、源码及避坑建议
|
Java API 开发者
Slf4j使用原理|学习笔记
快速学习Slf4j使用原理
Slf4j使用原理|学习笔记
|
分布式计算 IDE Java
【Java开发实战】「开发实战专题」Lombok插件开发实践必知必会操作!
【Java开发实战】「开发实战专题」Lombok插件开发实践必知必会操作!
176 0
|
前端开发 IDE Java
Lombok原理探析
## 前言 对于一个Java开发者来说,Lombok应该是使用最多的插件之一了,他提供了一系列注解来帮助我们减轻对重复代码的编写,例如实体类中大量的setter,getter方法,各种IO流等资源的关闭、try…catch…finally模版等,虽然可以通过IDE的快捷帮我们生成这些方法,但这些冗长的代码仍会影响代码的简洁性与可阅读性。 如今,随着使用者数量越来越多,Lombok甚至成为IDEA的
262 1