读懂 Slf4j、Log4j2、LogBack 关系

我们可以看到,很多文章都在告诉我,Slf4j 是日志的门面,是接口;而 Log4j2 和 Logback 是日志的实现。但是至于它们是怎么联系到一起,我们经常能看到说,使用 log4j-slf4j-impl 建立桥梁,就可以使得 Slf4j 桥接到 Log4j2 上,就可以使用 Log4j 的实现了。而我们的项目中,打日志都直接基于 Slf4j 接口即可。

那么我们思考多个问题:

  1. 这个绑定操作是怎么实现的呢?
  2. 如果 Slf4j 绑定了多个日志实现,会怎么样呢?

绑定操作是怎么实现的

首先,我们基本都是如下方式使用 Logger 的。

1
public static final Logger logger = LoggerFactory.getLogger(LogForOne.class);

通过查看 LoggerFactory.getLogger 的源代码,最终会找到绑定操作的逻辑。

1
2
3
4
5
6
private final static void performInitialization() {
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}

我们先看 1.7.x 及之前版本,bind 函数的主要逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);

可以看到,其核心逻辑是 StaticLoggerBinder 的单例,这儿隐藏了一个含义是——JVM 在类加载的时候如果遇到了同路径同名的 Class 时,则会被忽略。因此可以理解只会拿到唯一一个 StaticLoggerBinder 实例。就是这么简单粗暴。

当然,如果被选中的日志组件依赖不全,则会出错,并不会另外选择依赖完整的日志实现,毕竟它不是智能的。

依赖不全

在 Slf4j 的 2.0 版本有什么差别

区别点只在于 bind 函数的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
List<SLF4JServiceProvider> providersList = findServiceProviders();
reportMultipleBindingAmbiguity(providersList);
if (providersList != null && !providersList.isEmpty()) {
PROVIDER = providersList.get(0);
// SLF4JServiceProvider.initialize() is intended to be called here and nowhere else.
PROVIDER.initialize();
NITIALIZATION_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();

可以之前的可以明显产生对比,这儿已经不存在 StaticLoggerBinder 单例方式的硬连接了。这种方式相比较之前有明显的好吃,要实现一个日志组件,只需要实现接口即可,不需要必须要在日志实现组件中仅仅是为了接入 Slf4j 二引入 StaticLoggerBinder 单例的实现,并且这个实现与日志组件本身是没有意义的。第二点是,不会再存在实现了 StaticLoggerBinder 但没有实现日志接口的情况了。

1
2
3
4
5
6
7
8
private static List<SLF4JServiceProvider> findServiceProviders() {
ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
List<SLF4JServiceProvider> providerList = new ArrayList<>();
for (SLF4JServiceProvider provider : serviceLoader) {
providerList.add(provider);
}
return providerList;
}

这儿引入了 SPI 机制,只需要实现了 SLF4JServiceProvider 接口,即可被 Slf4j 所发现,当然这儿也是借助了 Java 所提供的 SPI 机制所实现。

一个日志组件怎么才能支持 Slf4j

接入 Slf4j 1.7.x 及之前版本:

  • 实现 Logger 接口
  • 实现 ILoggerFactory 接口
  • 提供 StaticLoggerBinder 实现,提供单例
  • 提供 StaticMarkerBinder 实现,提供单例
  • 提供 StaticMDCBinder 实现,提供单例

接入 Slf4j 2.0:

  • 实现 Logger 接口
  • 实现 ILoggerFactory 接口
  • 实现 SLF4JServiceProvider 接口
  • 实现 MDCAdapter 接口