从门面模式到 SLF4J 及其 getLogger 方法原理

Higurashi-kagome / 2024-12-19 / 原文

基于以下内容总结:从门面模式到 Slf4j、10 分钟讲清楚 Java SLF4J,Java 日志框架的扛把子,从原理到实践

写后端接口的时候,先写一个 Service 接口,这个 Service 接口的实现中可能会调用多个其他 Service 或 Mapper 方法来实现某个业务,对于 Controller,只需要传递参数给 Service 方法就好了,这时供 Controller 调用的 Service 就是一个门面。

门面模式的主要目的是给外界提供更简单的接口。要完成一个任务,往往需要多个子系统协作,不使用门面时,客户端需要调用各子系统的多个方法,操作繁琐容易出错,使用门面时,客户端调用门面的简单接口,不用考虑太多细节,子系统交给门面调用。

门面(外观)模式也能起到统一接口的作用,当有一组完成类似功能的接口时,门面对外暴露一个统一的外形,调用方不需要知道它外形之下的具体实现是什么。很像 Java Interface 提供的作用了,但这里可能粒度更大一些,门面后面往往是一些大粒度的子系统。

简介

试想,原先我需要自己调用多个子系统各自部分或全部的功能接口以完成我的需求:

2018-08-30-15356106508820

而使用门面模式重构后,只需要调用门面提供的统一功能入口即可:

2018-08-30-15356107001587

日志框架中的应用

日志系统应该是最常见的门面模式的应用了,我们以 SLF4J 门面框架为例。

SLF4J(Simple Logging Facade for Java)和 JCL(Jakarta Commons Logging)是两个不同的日志门面系统。

介绍

SLF4J 提供了日志接口,方便程序获取具体日志对象,其自带的简单日志记录实现 slf4j-simple、自带的空实现 slf4j-nop、日志框架 Logback 等,均直接实现了 SLF4J。第三方日志框架如 reload4j、JDK 内置的日志实现 JUL 等并不直接实现 SLF4J,而是由 SLF4J 提供的适配层 slf4j-reload4j、slf4j-jdk14 来实现向 SLF4J 的适配转换。参见下图,图片来自 SLF4J 官网。

2018-08-30-concrete-bindings
翻译
2018-08-30-concrete-bindings

下面是 SLF4J 与各种底层日志框架的依赖关系:

2018-08-30-concrete-bindings

使用

获取日志对象:

private static final Logger logger = LoggerFactory.getLogger(SomeService.class);

记录日志:

logger.debug("message is {}.", message);

这里会引出一个问题,也就是LoggerFactory.getLogger(SomeService.class)是如何获取到具体的实现层实例的。

绑定实际使用的子系统

类加载机制

在 1.7 版本之前,基于类加载机制实现和实现层的绑定。

类加载时,getLogger()尝试获取 Logger 实例:

image-20200606161315662

getILoggerFactory()实例化 StaticLoggerBinder 单例,后者在不同日志框架中有不同实现:

image-20200606162010335

performInitialization()调用bind()绑定日志框架实现:

image-20200606161405254

findPossibleStaticLoggerBinderPathSet尝试在类路径中加载org/slf4j/impl/StaticLoggerBinder.class

image-20200606161405254

如果发现有多个StaticLoggerBinder.class,说明类路径下存在多个日志实现层,reportMultipleBindingAmbiguity方法会给出错误提示(但不会报错)。

image-20200606161405254

StaticLoggerBinder.getSingleton()真正完成和实现层的绑定,虚拟机将尝试加载StaticLoggerBinder这个类。没找到则会报NoClassDefFoundError异常,并被 SLF4J 捕获给出错误消息(并不会报错,SLF4J 会和自带的 NOP 实现绑定):

无任何实现
无任何实现

NOP 即 no operation,也就是不打印任何日志。

如果找到多个则 SLF4J 也不知道最终会使用哪个,只是会给出日志信息:

image-20200606162417530

所以一定要注意排查是否有多个当前日志门面的实现,若有,那么系统整体的日志打印将不可控(不论是性能还是日志位置)。

Log4j2 中的 StaticLoggerBinder:

image-20200606161601087

另:如果下载 SLF4J 的源码,会发现 StaticLoggerBinder 这个类是找不到的。这是因为在编写 SLF4J 框架时,为保证编译通过,会提供一个空实现,而在打包发布的时候,会将其移除,这样虚拟机加载到的才是具体的日志实现层:

image-20200606161601087

SPI 机制

使用类加载机制实现绑定有一些问题:

  • 对于其他框架来说,要实现一个包名为org.slf4j.impl的类;
  • 对于 SLF4J 本身来说,其 jar 包中不能有 StaticLoggerBinder 这个类,打包时需要删除。

所以后面就改为通过 SPI 机制实现了。

img
img
img

performInitialization()调用bind()方法:

img

findServiceProviders()中调用的getServiceLoader()方法:

img

getServiceLoader()方法中调用ServiceLoader.load()方法完成对SLF4JServiceProvider实现类的加载:

img