# Spring Boot 日志体系源码解析(一):从 LogAdapter 到 SPI 绑定
一、背景
在大型工程项目中,日志是观测服务的利器。从服务上线,到服务运行,到问题定位,都少不了日志的参与。尽管我们平时经常接触日志,但很少会去深究底层代码。本文打算系统性地从源码角度,解析 Spring Boot 的日志系统。
二、从一段堆栈开始
在 Spring Boot 的 Main-Class 开始运行时,日志系统便悄然启动了。下面的堆栈清晰地记录了这一过程:
在这段堆栈中,
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 意味着什么? |
|---|---|---|---|
log4jSpiPresent | org.apache.logging.log4j.spi.ExtendedLogger | log4j-api.jar | Log4j2 的完整 API 体系在类路径上。这通常意味着 Log4j2 的核心引擎 (log4j-core.jar) 也大概率存在。即拥有log4j2的完整 SPI 体系,它有locationAware能力 |
log4jSlf4jProviderPresent | org.apache.logging.slf4j.SLF4JProvider | log4j-to-slf4j.jar | Log4j2 已经表态:“我已准备好成为 SLF4J 门面的一个实现”。这是 Log4j2 对 SLF4J 的投名状。意味着可以经对log4j2的调用桥接为SLF4J的门面的调用 |
slf4jSpiPresent | org.slf4j.spi.LocationAwareLogger | slf4j-api.jar | SLF4J 的完整 SPI 体系在类路径上。这意味着背后的日志实现(如 Logback)功能完整,支持位置感知等高级特性。 |
slf4jApiPresent | org.slf4j.Logger | slf4j-api.jar | SLF4J 的基础门面接口在类路径上。这是最底线的保障,只要你的代码想用 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 机制加载具体的日志引擎实现。
整个初始化链路可以概括为三个决策层次:
- LogAdapter 决策:根据四大侦察变量,选定走哪条 Adapter 路线(Slf4jAdapter / Log4jAdapter / JavaUtilAdapter)。
- 门面绑定:若走 SLF4J 路线,则调用
LoggerFactory.getLogger(),进入 SLF4J 门面的初始化流程。 - SPI 发现:SLF4J 通过
ServiceLoader在 classpath 上找到唯一的SLF4JServiceProvider,确定最终的日志实现。
因此,当需要更换日志引擎时,大多数情况下只需调整 Maven 依赖即可——Spring Boot 的 LogAdapter 和 SLF4J 的 SPI 会替你完成剩余工作。