经历过大大小小的项目,日志组件总是离不开,从一开始跟着别人做项目到自己能从零构建项目架构,从黏贴别人写好的日志组件配置文件到自己能读懂配置文件中的配置含义进而筛选调整,日志组件使用的越来越熟练,但是对其内部的机制却始终没有一个好的理解。
工作经历中有很多这样的情况,使用开源组件时,没有遇到问题则皆大欢喜,遇到问题就抓耳挠腮,各种搜寻资料。由此我便萌生出了一个《解疑释惑》系列,以此记录下对于使用到的各种技术(框架、组件、中间件)的理解。
正所谓磨刀不误砍柴工,唯积跬步而以至千里,这个系列就从日志组件 slf4j + logback 组合开始。
群雄逐鹿
psvm、sout、hello world,这个标志性的演示程序或许就是你第一次输出日志...
日志组件有很多,在写 slf4j + logback 这一对组合之前,我想先梳理下众多日志组件的历史和关系,这有助于理解日志体系与 slf4j + logback。
log4j(1.x) logback log4j2 jul jcl slf4j log4j-to-slf4j jul-to-slf4j log4j-slf4j-impl ... ,这些日志组件是不是多多少少会有眼熟的几个。
log4j(1.x) 是由 Ceki Gülcü 创立,于1999年首次发布,于 2000 年捐赠给 Apache 软件基金会,并迅速成为使用最广泛的日志组件,2015年8月5日 --- Apache Logging Services™ 项目管理委员会 (PMC) 宣布 Log4j™ 1.x 日志组件已达到其生命周期终点 (EOL),不再受到官方支持。[[1]](#[1])[[2]](#[2])
slf4j 与 logback 也是由 Ceki Gülcü 创立,是其离开 Apache 后创建的 QOS.ch Sarl (Switzerland) 公司(下简称QOS)的开源项目,提供定制开发服务。slf4j 是The Simple Logging Facade for Java的缩写,旨在作为各种 java 日志组件 (例如 java.util.logging、logback、log4j) 的门面,提供统一的接口。logback 创立时 log4j(1.x) 尚未停止维护,是具体的日志实现,旨在成为 log4j(1.x) 项目的后继者,接替 log4j(1.x) ,logback 原生实现了slf4j。[[3]](#[3])[[4]](#[4])[[5]](#[5])
log4j2 是 Apache 在终止了 log4j(1.x) 项目之后重新写的一个日志组件,主要有两部分,一个是 log4j api,另一个是 log4j core,log4j api 的定位可以参考 slf4j,log4j core 的定位可以参考 logback。Apache 将这两部分称为 Logging API(日志API)与Logging implementation(日志实现)。log4j2 的配置文件格式与log4j(1.x) 的配置文件格式并不兼容。[[6]](#[6])[[7]](#[7])
jul 是 java util logging 的缩写,是 java 平台1.4 版本开始提供的核心日志工具库。Apache 早期想要推动 log4j 成为 java的日志标准,但是 sun 公司没有纳入,催生了 jul。[[8]](#[8])
jcl 是 Apache Commons Logging(又名 Jakarta Commons Logging)的缩写,是 Apache 提供的一个简单的抽象层,其定位可以参考slf4j。[[9]](#[9])
按照各个日志组件截止到2025年的生命周期,我制作了一个时间轴图,可以看出:
- Ceki Gülcü 创立 logback(后捐献给 Apache 软件基金会),是java日志体系中的先行者,算是奠定了java日志体系的基础。
- jul 和 jcl 的发布可以看出 Sun 公司和 Apache 都想通过定义日志体系的行为接口争夺日志体系的标准。
- Ceki Gülcü 创立了 QOS 公司,并且推出了slf4j 开源项目,随后发布了 logback 开源项目。
- Apache 放弃了 log4j(1.x),重新写了一个 log4j2,通过 log4j api 与 log4j core 的结合,既有日志标准又有日志组件实现。
到这,可以将日志组件分为两类,一类是日志的api接口,其没有具体的日志功能实现,主要是定义日志行为的规范,另一类是具体的日志实现。
日志API :slf4j、commons-logging、log4j 2.x(log4j api)、jcl
日志实现:logback、log4j(1.x)、log4j2(log4j core)、jul
三足鼎立
剩下来的 log4j-to-slf4j、jul-to-slf4j、log4j-slf4j-impl 又该如何归类?
纵观日志组件的发展历史,由 Ceki Gülcü 创立的 log4j(1.x)(后捐给 Apache)奠定了日志组件体系的基础,Apache 和 Sun 公司都想要争夺日志体系的标准,纷纷推出了自己的日志 API 以及日志实现, Ceki Gülcü 跳出 Apache 成立了自己的公司 QOS ,基于他对于日志体系的深刻理解推出了 slf4j 与 logback,以此形成了三路诸侯争霸的局面。
三路诸侯的竞争体现在新的功能特性推出以及性能提升,从而抢占更多的市场份额,一统日志体系标准,那么如何方便用户从其它日志组件迁移到自己的日志组件呢?
分层设计思想
"计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决"这一观点,源自计算机体系结构中的分层设计思想,log4j-to-slf4j、jul-to-slf4j、log4j-slf4j-impl 就是这一思想的体现,Apache 与 QOS 都是通过增加桥接层来让用户以最小的代价迁移日志组件,而不用对现有代码做大的改动,这就形成了你中有我,我中有你的局面。
slf4j 还提供了迁移器帮助从 jcl、log4j api 以及 jul 迁移到 slf4j。有兴趣可以参看:SLF4J Migrator
到这,日志组件就划分成了三类了,日志的api接口、日志实现以及桥接层。下面两张图分别是 Apache 与 QOS 分别对于日志 API、日志实现以及桥阶层的概括图:
拔得头筹
基于2025年8月24日在 mvnrepository 网站 上统计的日志组件流行程度数据看出,slf4j + logback 这一组合位居榜首。
使用方式
梳理完日志组件的历史与关系,下面我想再贴一下slf4j 与 logback 搭配使用的方式,slf4j-api 的版本我选择了1.7.36,logback-classic 的版本我选择了1.2.12。
slf4j 是一个开源项目,slf4j-api 是其一个子模块,下文分析 slf4j 的内部机制可理解为在分析 slf4j-api,见 slf4j 项目顶级POM。
logback 也是开源项目,logback-classic 是其一个子模块,见 logback 项目顶级POM。
slf4j 与 logback 在项目中搭配使用时,我们引入的是 slf4j-api 与 logback-classic 的依赖:
xml
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.12</version>
</dependency>
用法1:
java
public class LogExample {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
public static void main(String[] args) {
log.info("Hello World!");
}
}
用法2:
java
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LogExample {
public static void main(String[] args) {
log.info("Hello World!");
}
}
搭配lombok插件,编译时自动会生成用法1的代码。https://projectlombok.org/api/lombok/extern/slf4j/Slf4j
综观 slf4j
The Simple Logging Facade for Java,slf4j 采用了门面设计模式,我认为门面设计模式的核心就是只定义标准的行为接口,屏蔽子系统的实现细节,使用者与门面交互而不与子系统交互,将使用者与具体实现的子系统耦合度降低。
通过上面的使用方式看,使用者通过 LoggerFactory.getLogger 获取日志对象,调用日志对象中记录日志的方法,具体的日志实现则由 logback 实现。
slf4j 中定义的标准主要包括:ILoggerFactory 、Logger 、LoggingEvent 、Level
-
ILoggerFactory(Logger对象管理器,获取日志对象的行为接口)
javapublic Logger getLogger(String name);
-
Logger(日志对象(日志记录器),记录日志的行为接口)
javapublic void trace(String msg); public void debug(String msg); public void info(String msg); public void warn(String msg); public void error(String msg); ...
-
LoggingEvent(定义日志事件结构接口)
javaLevel getLevel(); Marker getMarker(); String getLoggerName(); String getMessage(); String getThreadName(); Object[] getArgumentArray(); long getTimeStamp(); Throwable getThrowable();
-
Level(日志级别枚举)
javaERROR -> 错误 WARN -> 警告 INFO -> 信息 DEBUG -> 调试 TRACE -> 跟踪
slf4j 的入口
LoggerFactory 是 slf4j 的使用入口,其暴露了三个静态方法:
java
// 根据传入的class获取class名称(css.getName())后调用第二个方法
public static Logger getLogger(Class<?> clazz);
// 根据不同的需求(以name划分)获取对应的Logger对象
public static Logger getLogger(String name);
// 返回实现了ILoggerFactory接口的具体日志实现
public static ILoggerFactory getILoggerFactory();
第三个方法是获得实现了 ILoggerFactory 接口的具体日志实现,其需要管理不同需求(以 name 划分)的 Logger 对象,前面两个方法最终都会调用这个方法获取到 Logger。
初始化状态控制机
跟踪 getILoggerFactory
方法,发现其内部使用状态机来控制初始化流程,一共有五种状态:
java
// 当前初始化状态
static volatile int INITIALIZATION_STATE = UNINITIALIZED;
// 1.尚未初始化
static final int UNINITIALIZED = 0;
// 2.初始化中
static finl int ONGOING_INITIALIZATION = 1;
// 3.初始化失败
static final int FAILED_INITIALIZATION = 2;
// 4.初始化成功
static final int SUCCESSFUL_INITIALIZATION = 3;
// 5.无日志实现,降级到NOP(丢弃日志)
static final int NOP_FALLBACK_INITIALIZATION = 4;
java
public static ILoggerFactory getILoggerFactory() {
// 判断是否处于尚未初始化状态
if (INITIALIZATION_STATE == UNINITIALIZED) {
// 获取LoggerFactory.class锁
synchronized (LoggerFactory.class) {
// 二次判断
if (INITIALIZATION_STATE == UNINITIALIZED) {
// 将状态置为初始化中(避免后续大量的锁开销)
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
// 执行初始化
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
// 初始化成功
case SUCCESSFUL_INITIALIZATION:
// 与日志实现完成绑定,返回日志实现实例
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
// 没有找到日志实现,返回NOP实现(丢弃日志)
return NOP_FALLBACK_FACTORY;
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_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
通过 volatile 修饰了 INITIALIZATION_STATE 状态字段与双重检查锁来保证线程安全,确保只初始化一个日志实例,ONGOING_INITIALIZATION 状态避免初始化期间大量的锁开销。
performInitialization
方法主要做了两件事:静态绑定和版本兼容检查。
java
private final static void performInitialization() {
// 与日志实现完成绑定
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
// 初始化成功后执行版本兼容检查
versionSanityCheck();
}
}
静态绑定
java
private final static void bind() {
try {
// 存储查找到的多个org.slf4j.impl.StaticLoggerBinder的路径
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
// 非安卓情况下
if (!isAndroid()) {
// 查找多个org.slf4j.impl.StaticLoggerBinder路径
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
// 存在多个StaticLoggerBinder,控制台输出警告
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
// 执行日志实现的组件的初始化
StaticLoggerBinder.getSingleton();
// 将状态置为初始化成功
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 如果存在多个 StaticLoggerBinder ,控制台输出真正使用的那个
reportActualBinding(staticLoggerBinderPathSet);
} catch (NoClassDefFoundError ncde) {
String msg = ncde.getMessage();
if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
// 运行时找不到StaticLoggerBinder类,降级为NOP(丢弃日志)
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
} else {
// 其他类找不到,状态置为初始化失败,控制台输出错误信息
failedBinding(ncde);
throw ncde;
}
} catch (java.lang.NoSuchMethodError nsme) {
String msg = nsme.getMessage();
if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
// StaticLoggerBinder版本不兼容,将状态置为初始化失败
INITIALIZATION_STATE = FAILED_INITIALIZATION;
Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
Util.report("Your binding is version 1.5.5 or earlier.");
Util.report("Upgrade your binding to version 1.6.x.");
}
throw nsme;
} catch (Exception e) {
// 其它异常,状态置为初始化失败,控制台输出错误信息
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
} finally {
// 绑定后的清理工作
postBindCleanUp();
}
}
findPossibleStaticLoggerBinderPathSet
方法在运行时通过 ClassLoader 加载多个 org/slf4j/impl/StaticLoggerBinder.class 类,这也是 slf4j 与 logback 能够分离运用到的一个重要技术。
reportMultipleBindingAmbiguity
方法是用来在控制台输出多个 org/slf4j/impl/StaticLoggerBinder.class 的位置,也就是如果存在多个 StaticLoggerBinder 并不会导致项目无法启动运行,多个 jar 包中存在的同全限定名类,具体使用到的类取决于加载顺序。
查看 slf4j-api 项目里是含有 StaticLoggerBinder 这个类的,怎么在运行时就是变成了日志实现 jar 包中的 StaticLoggerBinder 了?
![FireShot Capture 004 - slf4j_slf4j-api_src_main_java_org_slf4j_impl at v_1.7.36 · qo_ - [github.com]](D:\临时文件\Markdown文档\FireShot Capture 004 - slf4j_slf4j-api_src_main_java_org_slf4j_impl at v_1.7.36 · qo_ - [github.com].png)
原来 slf4j-api 在打包发行的时候是会忽略掉这个类,这也是 slf4j 与 logback 分离能够静态绑定的一个重要点:
xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<configuration>
<tasks>
<echo>Removing slf4j-api's dummy StaticLoggerBinder and StaticMarkerBinder</echo>
<delete dir="target/classes/org/slf4j/impl"/>
</tasks>
</configuration>
</plugin>
https://github.com/qos-ch/slf4j/blob/v_1.7.36/slf4j-api/pom.xml
StaticLoggerBinder.getSingleton()
方法主要是是用来完成具体日志实现的初始化,这个在后面的 logback 里面进行解析,这一步完成后将状态置为初始化成功。
reportActualBinding
方法输出实际使用的 ILoggerFactory 实现类。
初始化期间的日志处理
还记得上面 getILoggerFactory
方法中 ONGOING_INITIALIZATION 状态的处理吗?如果在初始化期间有其他线程有日志记录的请求,会返回一个SUBST_FACTORY来执行日志记录:
java
static final SubstituteLoggerFactory SUBST_FACTORY = new SubstituteLoggerFactory();
SubstituteLoggerFactory 是 slf4j-api 提供的一个实现了 ILoggerFactory 的替补日志实现,其核心就是使用了一个 HashMap 保存了多个日志记录器 SubstituteLogger(还记得上面说过的,以 name 划分支持不同场景获取 Logger ?):
java
public class SubstituteLoggerFactory implements ILoggerFactory {
// 初始化期间为false,初始化完成后置为true
boolean postInitialization = false;
// 用来保存多个替补日志记录器
final Map<String, SubstituteLogger> loggers = new HashMap<String, SubstituteLogger>();
// 存储初始化期间的日志事件,SubstituteLoggingEvent也是替补日志事件实现
final LinkedBlockingQueue<SubstituteLoggingEvent> eventQueue = new LinkedBlockingQueue<SubstituteLoggingEvent>();
synchronized public Logger getLogger(String name) {
SubstituteLogger logger = loggers.get(name);
if (logger == null) {
logger = new SubstituteLogger(name, eventQueue, postInitialization);
loggers.put(name, logger);
}
return logger;
}
public List<String> getLoggerNames() {
return new ArrayList<String>(loggers.keySet());
}
public List<SubstituteLogger> getLoggers() {
return new ArrayList<SubstituteLogger>(loggers.values());
}
public LinkedBlockingQueue<SubstituteLoggingEvent> getEventQueue() {
return eventQueue;
}
public void postInitialization() {
postInitialization = true;
}
public void clear() {
loggers.clear();
eventQueue.clear();
}
}
SubstituteLogger 是 slf4j-api 提供的一个实现了 Logger 的替补日志对象:
java
public class SubstituteLogger implements Logger {
private final String name;
// 构造时为空,初始化完成后变成真正的 Logger 对象
private volatile Logger _delegate;
private Boolean delegateEventAware;
private Method logMethodCache;
// 初始化期间的日志对象实例
private EventRecodingLogger eventRecodingLogger;
// 缓存初始化期间的日志记录请求
private Queue<SubstituteLoggingEvent> eventQueue;
// 初始化期间为false
private final boolean createdPostInitialization;
public SubstituteLogger(String name, Queue<SubstituteLoggingEvent> eventQueue, boolean createdPostInitialization) {
this.name = name;
this.eventQueue = eventQueue;
this.createdPostInitialization = createdPostInitialization;
}
public void trace(String msg) {
delegate().trace(msg);
}
public void debug(String msg) {
delegate().debug(msg);
}
public void info(String msg) {
delegate().info(msg);
}
public void warn(String msg) {
delegate().warn(msg);
}
public void error(String msg) {
delegate().error(msg);
}
// 获取 Logger 对象
// 初始化期间为替补日志对象,初始化完成后为真正的日志对象
Logger delegate() {
if(_delegate != null) {
return _delegate;
}
// 降级到 NOP
if(createdPostInitialization) {
return NOPLogger.NOP_LOGGER;
} else {
// 使用 EventRecodingLogger 缓存日志记录请求
return getEventRecordingLogger();
}
}
// 获取初始化期间日志记录请求 Logger 对象
private Logger getEventRecordingLogger() {
if (eventRecodingLogger == null) {
eventRecodingLogger = new EventRecodingLogger(this, eventQueue);
}
return eventRecodingLogger;
}
// 设置真正的日志对象
public void setDelegate(Logger delegate) {
this._delegate = delegate;
}
...
}
EventRecodingLogger 也是 slf4j-api 提供的一个实现了 Logger 接口的替补日志对象:
java
public class EventRecodingLogger implements Logger {
String name;
SubstituteLogger logger;
// 用来缓存初始化期间的日志事件
Queue<SubstituteLoggingEvent> eventQueue;
// as an event recording logger we have no choice but to record all events
final static boolean RECORD_ALL_EVENTS = true;
public EventRecodingLogger(SubstituteLogger logger, Queue<SubstituteLoggingEvent> eventQueue) {
this.logger = logger;
this.name = logger.getName();
this.eventQueue = eventQueue;
}
...
}
做了这么多,其实就是为了减少初始化期间多线程场景下锁的开销以及不丢弃初始化期间产生的日志事件。
接下来我们来看 LoggerFactory 类中的 postBindCleanUp
方法,它是在 bind
方法最后的 finally 块中调用的,主要做了三件事:
java
private static void postBindCleanUp() {
// 将替补日志对象 EventRecodingLogger 中替换为真实 Logger对象
fixSubstituteLoggers();
// 将初始化期间缓存的日志通过真实的 Logger 记录
replayEvents();
// 清理替补日志实例
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
}
java
private static void fixSubstituteLoggers() {
synchronized (SUBST_FACTORY) {
SUBST_FACTORY.postInitialization();
for (SubstituteLogger substLogger : SUBST_FACTORY.getLoggers()) {
// 从实现了ILoggerFactory接口的Logger对象管理器中获取真实的Logger
Logger logger = getLogger(substLogger.getName());
// 将替补日志对象 EventRecodingLogger 中替换为真实 Logger对象
substLogger.setDelegate(logger);
}
}
}
java
private static void replayEvents() {
// 初始化期间缓存的临时日志
final LinkedBlockingQueue<SubstituteLoggingEvent> queue = SUBST_FACTORY.getEventQueue();
final int queueSize = queue.size();
int count = 0;
final int maxDrain = 128;
List<SubstituteLoggingEvent> eventList = new ArrayList<SubstituteLoggingEvent>(maxDrain);
while (true) {
// 按批次从缓存取出临时日志
int numDrained = queue.drainTo(eventList, maxDrain);
if (numDrained == 0)
break;
for (SubstituteLoggingEvent event : eventList) {
// 通过真实的 Logger 记录
replaySingleEvent(event);
if (count++ == 0)
emitReplayOrSubstituionWarning(event, queueSize);
}
eventList.clear();
}
}
java
private static void replaySingleEvent(SubstituteLoggingEvent event) {
if (event == null)
return;
SubstituteLogger substLogger = event.getLogger();
String loggerName = substLogger.getName();
if (substLogger.isDelegateNull()) {
throw new IllegalStateException("Delegate logger cannot be null at this state
}
if (substLogger.isDelegateNOP()) {
// nothing to do
} else if (substLogger.isDelegateEventAware()) {
// 执行日志记录
substLogger.log(event);
} else {
Util.report(loggerName);
}
}
至此,已完成了 slf4j-api 与 logback 的静态绑定工作。
版本兼容性
版本兼容性在 LoggerFactory 类中的 versionSanityCheck
方法里面完成:
java
static private final String[] API_COMPATIBILITY_LIST = new String[] { "1.6", "1.7" };
private final static void versionSanityCheck() {
try {
// 具体日志实现 jar 包里 org.slf4j.impl.StaticLoggerBinder类
// 获取其静态常量字段 REQUESTED_API_VERSION
String requested = StaticLoggerBinder.REQUESTED_API_VERSION;
boolean match = false;
// 具体日志实现中所需的 slf4j-api 版本与 slf4j-api 中支持的版本进行前缀比较
for (String aAPI_COMPATIBILITY_LIST : API_COMPATIBILITY_LIST) {
if (requested.startsWith(aAPI_COMPATIBILITY_LIST)) {
match = true;
}
}
// 如果版本不匹配将会在控制台输出错误信息
if (!match) {
Util.report("The requested version " + requested + " by your slf4j binding is not compatible with "
+ Arrays.asList(API_COMPATIBILITY_LIST).toString());
Util.report("See " + VERSION_MISMATCH + " for further details.");
}
} catch (java.lang.NoSuchFieldError nsfe) {
// 兼容没有静态常量字段 REQUESTED_API_VERSION 的老版本日志实现
// given our large user base and SLF4J's commitment to backward
// compatibility, we cannot cry here. Only for implementations
// which willingly declare a REQUESTED_API_VERSION field do we
// emit compatibility warnings.
} catch (Throwable e) {
// we should never reach here
Util.report("Unexpected problem occured during version sanity check", e);
}
}
java
public class StaticLoggerBinder implements LoggerFactoryBinder {
/**
* Declare the version of the SLF4J API this implementation is compiled
* against. The value of this field is usually modified with each release.
*/
// to avoid constant folding by the compiler, this field must *not* be final
// 不能使用 final 修饰,否则会发生常量折叠,被编译器优化为字面量
// 也就是slf4j-api项目StaticLoggerBinder类中的这个字段不能使用final修饰
public static String REQUESTED_API_VERSION = "1.7.16"; // !final
...
}
上面的实现好处有:
- 保证日志 api 与日志实现分离,也就是 slf4j 与 logback 可以分开维护,独立编译打包
- 通过版本的前缀匹配比较,使得日志实现组件能进行小版本的升级
- StaticLoggerBinder 类里面静态常量 REQUESTED_API_VERSION 不能使用 final 修饰,否则会发生常量折叠,被编译器优化为字面量
- 通过捕捉 NoSuchFieldError 实现了兼容老版本的 logback
MDC
分析完 slf4j 的 ILoggerFactory 、Logger 、 LoggingEvent 、 Level 后,我还想再写一下 slf4j 中一个重要的特性标准:MDC。
MDC 是 Mapped Diagnostic Context 的缩写,翻译成中文叫做映射诊断上下文,设计初衷是为了按照线程管理上下文信息,每个线程可以将信息放置在诊断上下文中,以便在日志记录时能够拿到这些数据,线程切换时,子线程不能自动继承父线程的上下文信息。
org.slf4j.MDC 类是入口,暴露了MDC 操作的各种静态方法,slf4j 中的 MDC 同样通过 MDCAdapter 接口定义标准,具体的实现交由具体的日志组件完成。
参考 LoggerFactory 中 StaticLoggerBinder 的初始化绑定流程,MDC 内部中含有一个静态成员变量 mdcAdapter,类型为 MDCAdapter ,是一个接口,在运行时初始化设置:
java
public class MDC {
static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
// 接口,具体的实现交由运行时初始化绑定
static MDCAdapter mdcAdapter;
private MDC() {
}
...
}
MDC 对外暴露的各种静态方法都是调用 MDCAdapter 的具体实现来完成:
java
public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
}
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
// 由具体的日志实现组件中实现了MDCAdapter接口的实现完成
mdcAdapter.put(key, val);
}
MDCAdapter 接口定义的 MDC 应有的行为:
java
public interface MDCAdapter {
// 放入键值对
public void put(String key, String val);
// 根据键获取
public String get(String key);
// 根据键移除
public void remove(String key);
// 清空
public void clear();
// 获取全量上下文数据
public Map<String, String> getCopyOfContextMap();
// 设置上下文
public void setContextMap(Map<String, String> contextMap);
}
在调用首次 MDC 中的方法时,其内部的 static 代码块进行初始化绑定日志实现中的 MDC 实现,并将其赋值给 mdcAdapter:
java
static {
try {
// 获取日志实现中的MDC实例
mdcAdapter = bwCompatibleGetMDCAdapterFromBinder();
} catch (NoClassDefFoundError ncde) {
// 如果日志实现类中没有MDC实例,回退到NOPMDCAdapter
mdcAdapter = new NOPMDCAdapter();
String msg = ncde.getMessage();
// 控制台输出警告信息,不强制日志实现中必须含有MDC实现
if (msg != null && msg.contains("StaticMDCBinder")) {
Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");
Util.report("Defaulting to no-operation MDCAdapter implementation.");
Util.report("See " + NO_STATIC_MDC_BINDER_URL + " for further details.");
} else {
throw ncde;
}
} catch (Exception e) {
// we should never get here
Util.report("MDC binding unsuccessful.", e);
}
}
看下 bwCompatibleGetMDCAdapterFromBinder
方法中的逻辑:
java
private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError {
try {
// 加载日志实现的StaticMDCBinder获取MDC实现
return StaticMDCBinder.getSingleton().getMDCA();
} catch (NoSuchMethodError nsme) {
// 兼容老版本
// binding is probably a version of SLF4J older than 1.7.14
return StaticMDCBinder.SINGLETON.getMDCA();
}
}
是不是觉得和 LoggerFactory 中的 StaticLoggerBinder 很相像,没错,slf4j 在打包发行时会忽略 slf4j 自带的 StaticMDCBinder ,在运行时静态绑定日志实现组件中的 StaticMDCBinder。slf4j 并不强制日志实现组件中必须含有 MDC 的实现,如果日志实现组件中没有 MDC 的实现,slf4j 则会使用 NOPMDCAdapter,不做处理:
java
public class NOPMDCAdapter implements MDCAdapter {
public void clear() {
}
public String get(String key) {
return null;
}
public void put(String key, String val) {
}
public void remove(String key) {
}
public Map<String, String> getCopyOfContextMap() {
return null;
}
public void setContextMap(Map<String, String> contextMap) {
// NOP
}
}
回顾 slf4j
前面的综观 slf4j 是对 slf4j 有一个整体的认识,上面按职能模块对于 slf4j 进行了细节的分析,现在对 slf4j 进行一次完整的回顾总结,上面的两张图从左到右看,此时对于 slf4j 有了更加清晰的理解。
一点感悟
从技术上,slf4j 的设计值得我们学习,比如,在我们设计 SDK 或者框架基础组件供给他人使用时,或许可以采用 slf4j 的 api 与实现分离的做法,通过抽象层隔离标准与底层技术实现细节,从而为版本迭代提供平滑升级的基础。slf4j 中的桥接适配器模式也为我们解决历史系统兼容问题提供了思路。
从商业上,日志体系中标准的竞争可以看出,规范的制定是生态建设的一个关键要素,兼容不代表妥协,这也是一个竞争中一个重要手段。
技术和商业的结合是必然,通过 Ceki Gülcü 创立的 QOS 公司,推出了slf4j 和 logback 的组合模式,我们看到 slf4j 在技术上保持了中立性,实现的多样化性。标准 + 实现是很成功的一种模式,slf4j 和 logback 也证明了这一点。
-
https://news.apache.org/foundation/entry/apache_logging_services_project_announces ↩︎
-
https://logging.apache.org/log4j/1.x/faq.html#a1.1https://www.qos.ch/ ↩︎
-
https://logging.apache.org/log4j/2.x/migrate-from-log4j1.html ↩︎
-
https://docs.oracle.com/javase/8/docs/api/java/util/logging/package-summary.html#package.description ↩︎
-
https://commons.apache.org/proper/commons-logging/guide.html#Introduction ↩︎