深度剖析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

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

目录
相关文章
|
数据采集 分布式计算 监控
DataX教程(03)- 源码解读(超详细版)
DataX教程(03)- 源码解读(超详细版)
3505 0
DataX教程(03)- 源码解读(超详细版)
|
数据库 OceanBase 索引
在OceanBase数据库中,REPLACE INTO和insert update在效率上可能有所不同
【2月更文挑战第30天】在OceanBase数据库中,REPLACE INTO和insert update在效率上可能有所不同
698 1
|
消息中间件 存储 监控
Flume+Kafka整合案例实现
Flume+Kafka整合案例实现
424 1
|
设计模式 Java 程序员
日志框架Slf4j作用及其实现原理
日志框架Slf4j作用及其实现原理
276 0
解决:下列软件包有未满足的依赖关系: libc6-dev : 破坏: binutils (< 2.38) 但是 2.35.1-7 正要被安装E: 错误,pkgProblemResolver::Re
解决:下列软件包有未满足的依赖关系: libc6-dev : 破坏: binutils (< 2.38) 但是 2.35.1-7 正要被安装E: 错误,pkgProblemResolver::Re
1905 0
mkdir: cannot create directory `**': No such file or directory
在mkdir时报错的解决方案,在网上找了很多文章都没有说清楚原因。
770 0
|
Java Spring 容器
springcloud项目中指定扫描路径
springcloud项目中指定扫描路径
414 7
|
20天前
|
存储 人工智能 运维
从“看得见”到“能决策”:Operation Intelligence 重构企业智能运维新范式
从 Observability 到 Operation Intelligence,日志服务 SLS 与云监控 2.0 协力之下,为企业打造高效、稳定、智能运营的数字化中枢,让复杂系统变得可视、可管、可优。
|
3月前
|
消息中间件 存储 缓存
超全面Java中的队列(Queue)
Java中的`Queue`接口位于`java.util`包,继承自`Collection`,用于存储待处理的元素,通常遵循FIFO原则。它包含`add`、`offer`、`poll`等方法,支持多种实现类,如`LinkedList`、`PriorityQueue`、`ArrayDeque`、`ConcurrentLinkedQueue`及`BlockingQueue`系列。
390 0
|
6月前
|
算法 物联网 Swift
Qwen3 X ModelScope工具链: 飞速训练 + 全面评测
Qwen于近日发布了Qwen3系列模型,包含了各个不同规格的Dense模型和MoE模型。开源版本中,Dense模型基本沿用了之前的模型结构,差别之处在于对于Q和K两个tensor增加了RMSNorm;MoE模型去掉了公共Expert,其他结构基本与前一致。在模型大小上,涵盖了从0.6B到32B(Dense)和235B(MoE)不同的尺寸。
804 15