Spring Boot日志体系源码解析(一)

54 阅读6分钟

# Spring Boot 日志体系源码解析(一):从 LogAdapter 到 SPI 绑定

一、背景

在大型工程项目中,日志是观测服务的利器。从服务上线,到服务运行,到问题定位,都少不了日志的参与。尽管我们平时经常接触日志,但很少会去深究底层代码。本文打算系统性地从源码角度,解析 Spring Boot 的日志系统。

二、从一段堆栈开始

在 Spring Boot 的 Main-Class 开始运行时,日志系统便悄然启动了。下面的堆栈清晰地记录了这一过程:

动画2.gif 在这段堆栈中,SpringApplication.run(FirstWebProjectApplication.class, args) 的调用,触发了 SpringApplication 类的初始化。类初始化时,静态变量开始赋值,静态代码块开始执行。以下代码得以执行:

private static final Log logger = LogFactory.getLog(SpringApplication.class);

下一步,调用 LogFactory 的 getLog 函数:

// 代码1
public static Log getLog(String name) {
    return LogAdapter.createLog(name);
}

在代码1中,对 createLog 这个静态方法的调用,会触发 LogAdapter 类的初始化,导致 LogAdapter 中静态变量赋值和静态代码块执行。本篇文章着重讲解 LogAdapter 的静态代码块,以及最终日志实现类的绑定机制。

三、LogAdapter的作用:决策用哪个Adapter去createLog

// 代码2
// log4jSpiPresent表示org.apache.logging.log4j.spi.ExtendedLogger类是否能加载
private static final boolean log4jSpiPresent = isPresent("org.apache.logging.log4j.spi.ExtendedLogger");  
  
// log4jSlf4jProviderPresent表示org.apache.logging.slf4j.SLF4JProviderr类是否能加载 
private static final boolean log4jSlf4jProviderPresent = isPresent("org.apache.logging.slf4j.SLF4JProvider");  
 
// slf4jSpiPresent表示org.slf4j.spi.LocationAwareLogger类是否能加载 
private static final boolean slf4jSpiPresent = isPresent("org.slf4j.spi.LocationAwareLogger");  
  
// slf4jApiPresent表示org.slf4j.Logger类是否能加载 
private static final boolean slf4jApiPresent = isPresent("org.slf4j.Logger");  
  
  
private static final Function<String, Log> createLog;  
  
static {  
    if (log4jSpiPresent) {  
        if (log4jSlf4jProviderPresent && slf4jSpiPresent) {  
            // log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;  
            // however, we still prefer Log4j over the plain SLF4J API since  
            // the latter does not have location awareness support.  
            createLog = Slf4jAdapter::createLocationAwareLog;  
        }  
        else {  
            // Use Log4j 2.x directly, including location awareness support  
            createLog = Log4jAdapter::createLog;  
        }  
    }  
    else if (slf4jSpiPresent) {  
        // Full SLF4J SPI including location awareness support  
        createLog = Slf4jAdapter::createLocationAwareLog;  
    }  
    else if (slf4jApiPresent) {  
        // Minimal SLF4J API without location awareness support  
        createLog = Slf4jAdapter::createLog;  
    }  
    else {  
        // java.util.logging as default  
        // Defensively use lazy-initializing adapter class here as well since the  
        // java.logging module is not present by default on JDK 9. We are requiring  
        // its presence if neither Log4j nor SLF4J is available; however, in the  
        // case of Log4j or SLF4J, we are trying to prevent early initialization  
        // of the JavaUtilLog adapter - for example, by a JVM in debug mode - when eagerly  
        // trying to parse the bytecode for all the cases of this switch clause.  
        createLog = JavaUtilAdapter::createLog;  
    }  
}

以上四个变量分别表示对应类是否能加载。它们存在的意义,整理如下:

日志系统四大侦查变量全解析

变量名检查的类类所在的 JAR 包这个变量为 true 意味着什么?
log4jSpiPresentorg.apache.logging.log4j.spi.ExtendedLoggerlog4j-api.jarLog4j2 的完整 API 体系在类路径上。这通常意味着 Log4j2 的核心引擎 (log4j-core.jar) 也大概率存在。即拥有log4j2的完整 SPI 体系,它有locationAware能力
log4jSlf4jProviderPresentorg.apache.logging.slf4j.SLF4JProviderlog4j-to-slf4j.jarLog4j2 已经表态:“我已准备好成为 SLF4J 门面的一个实现”。这是 Log4j2 对 SLF4J 的投名状。意味着可以经对log4j2的调用桥接为SLF4J的门面的调用
slf4jSpiPresentorg.slf4j.spi.LocationAwareLoggerslf4j-api.jarSLF4J 的完整 SPI 体系在类路径上。这意味着背后的日志实现(如 Logback)功能完整,支持位置感知等高级特性。
slf4jApiPresentorg.slf4j.Loggerslf4j-api.jarSLF4J 的基础门面接口在类路径上。这是最底线的保障,只要你的代码想用 SLF4J 写日志,这个变量就必然为 true

带着对四大侦察变量的理解,我们来解析代码2中的决策逻辑:
1     如果log4jSpiPresent为true,说明log4j2整个SPI体系存在;
1.1  那么再看log4jSlf4jProviderPresent是否为true,若为true,说明log4j2到slf4j的桥接存在(即业务代码调用的是log4j2的api,但桥接到slf4j的门面调用上),这说明,具体的日志实现是可以通过SPI修改的,那么继续看slf4jSpiPresent,如果slf4jSpiPresent为true,说明有完整SLF4J SPI,那么调用Slf4jAdapter的createLocationAwareLog函数创建logger;
1.2  如果log4jSlf4jProviderPresent和slf4jSpiPresent不全为true,则不满足业务代码为log4j2,却使用桥接方式调用slf4j的条件,故直接使用log4j2的原生API
2     如果log4jSpiPresent为false,查看slf4jSpiPresent是否为true。
2.1  如果slf4jSpiPresent为true,说明拥有完整SLF4J SPI,那么调用Slf4jAdapter的createLocationAwareLog函数创建logger。
2.2 如果slf4jSpiPresent为false,那么查看slf4jApiPresent。
2.2.1 如果slf4jApiPresent为true,说明有slf4j门面,但无位置感知功能,调用Slf4jAdapter的createLog函数创建logger
2.2.2 如果slf4jApiPresent为false,说明没有slf4j门面,既没有log4j2,也没有slf4j,故使用JUL创建logger
流程图如下图所示

flowchart TD
    A[开始: LogAdapter 静态初始化] --> B{log4jSpiPresent?<br>即: log4j-api.jar 是否存在}
    
    B -- 是 --> C{log4jSlf4jProviderPresent<br> 且 slf4jSpiPresent?<br>即: 既有桥接器, 又有完整SLF4J SPI}
    
    C -- 是 --> D[选用 Slf4jAdapter<br>createLocationAwareLog<br>走SLF4J门面 + SPI, 拥有位置感知]
    C -- 否 --> E[选用 Log4jAdapter<br>createLog<br>直接调用Log4j2原生API,<br>完全绕过SLF4J门面和SPI]
    
    B -- 否 --> F{slf4jSpiPresent?<br>即: 是否有完整SLF4J SPI}
    
    F -- 是 --> G[选用 Slf4jAdapter<br>createLocationAwareLog<br>标准SLF4J+Logback组合, 拥有位置感知]
    F -- 否 --> H{slf4jApiPresent?<br>即: 是否至少有基础SLF4J API}
    
    H -- 是 --> I[选用 Slf4jAdapter<br>createLog<br>走SLF4J门面+SPI, 但无位置感知]
    H -- 否 --> J[选用 JavaUtilAdapter<br>createLog<br>兜底方案, 使用JDK自带JUL]

至此,静态代码块为我们选定了正确的 Adapter。在 Spring Boot 默认环境下,使用的是 SLF4J 门面 + Logback 日志实现(Logback 实现由 SPI 机制加载)。

四、Logger 的创建:从门面到 SPI 绑定

LogAdapter里面定义了一个静态类Slf4jAdapter

LogAdapter.java
// 代码3
private static class Slf4jAdapter {  
  
    public static Log createLocationAwareLog(String name) {  
        Logger logger = LoggerFactory.getLogger(name);  
        return (logger instanceof LocationAwareLogger locationAwareLogger ?  
        new Slf4jLocationAwareLog(locationAwareLogger) : new Slf4jLog<>(logger));  
    }  
  
    public static Log createLog(String name) {  
        return new Slf4jLog<>(LoggerFactory.getLogger(name));  
    }  
}

代码3第6行通过 LoggerFactory.getLogger(name) 得到 Logger。接下来进入 SLF4J 门面的初始化流程:

LoggerFactory.java
// 代码4
public static Logger getLogger(String name) {  
    ILoggerFactory iLoggerFactory = getILoggerFactory();  
    return iLoggerFactory.getLogger(name);  
}

public static ILoggerFactory getILoggerFactory() {  
    return getProvider().getLoggerFactory();  
}

static SLF4JServiceProvider getProvider() {  
    if (INITIALIZATION_STATE == UNINITIALIZED) {  
        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");  
}

getProvider 中,如果状态为 UNINITIALIZED,则加锁并调用 performInitialization 初始化。其内部调用 bind() 方法,最终到达核心:

LoggerFactory.java
//代码5
private final static void performInitialization() {  
    bind();  
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {  
        versionSanityCheck();  
    }  
}


private final static void bind() {  
try {  
    List<SLF4JServiceProvider> providersList = findServiceProviders();  
    reportMultipleBindingAmbiguity(providersList);  
    if (providersList != null && !providersList.isEmpty()) {  
        PROVIDER = providersList.get(0);  
    earlyBindMDCAdapter();  
    // SLF4JServiceProvider.initialize() is intended to be called here and nowhereelse.  
    PROVIDER.initialize();  
    INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;  
    // 省略部分代码
    
static List<SLF4JServiceProvider> findServiceProviders() {  
    List<SLF4JServiceProvider> providerList = new ArrayList<>();  
  
    final ClassLoader classLoaderOfLoggerFactory = LoggerFactory.class.getClassLoader();  
  
    //省略部分代码
    ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);  
  
    Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();  
    while (iterator.hasNext()) {  
        safelyInstantiate(providerList, iterator);  
    }  
    return providerList;  
}

private static ServiceLoader<SLF4JServiceProvider> getServiceLoader(final ClassLoader classLoaderOfLoggerFactory) {  
    ServiceLoader<SLF4JServiceProvider> serviceLoader;  
    serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class, classLoaderOfLoggerFactory);  
//部分代码省略 
    return serviceLoader;  
}

findServiceProviders 通过 Java 标准的 SPI 机制(ServiceLoader.load)加载所有 SLF4JServiceProvider 的实现类。bind() 方法取列表中的第一个作为 PROVIDER,并调用 PROVIDER.initialize() 完成引擎的初始化。

五、总结

本文以一段堆栈为引,解析了 LogAdapter 如何根据 classpath 上 JAR 包的存在性,自动选择 Adapter 策略;以及 SLF4J 门面如何通过 SPI 机制加载具体的日志引擎实现。

整个初始化链路可以概括为三个决策层次:

  1. LogAdapter 决策:根据四大侦察变量,选定走哪条 Adapter 路线(Slf4jAdapter / Log4jAdapter / JavaUtilAdapter)。
  2. 门面绑定:若走 SLF4J 路线,则调用 LoggerFactory.getLogger(),进入 SLF4J 门面的初始化流程。
  3. SPI 发现:SLF4J 通过 ServiceLoader 在 classpath 上找到唯一的 SLF4JServiceProvider,确定最终的日志实现。

因此,当需要更换日志引擎时,大多数情况下只需调整 Maven 依赖即可——Spring Boot 的 LogAdapter 和 SLF4J 的 SPI 会替你完成剩余工作。