文章

一起学Java(12)-[日志篇]教你分析SLF4J源码,掌握SLF4J如何与Logback无缝集成的原理

继续完成上篇(一起学Java(11)-[日志篇]教你分析SLF4J源码,掌握Logger接口实现类加载原理)留给自己的任务,研究Logback是如何和SLF4J无缝集成的。

在之前的SLF4J源码研究中(教你分析SLF4J源码,掌握Logger接口实现类加载原理)我们已经知道SLF4J中利用java.util.ServiceLoader 机制寻找SLF4JServiceProvider的实现类,从而得到构造Logger实例的工厂类。那么我们就先看看java.util.ServiceLoader是个什么东西。

一、java.util.ServiceLoader定位和原理

java.util.ServiceLoader 是 Java 提供的一个服务发现机制,它允许程序在运行时动态地加载和使用实现了某个接口或抽象类的服务实现。这个机制在 Java 的模块化系统和插件系统中非常有用。正好符合Log框架的场景定位。

ServiceLoader 通过一种称为服务提供者接口(SPI, Service Provider Interface的机制工作。服务提供者接口是一个接口或抽象类,而服务实现类是该接口或抽象类的具体实现。

1. 服务声明

服务提供者接口在 JAR 包中的 META-INF/services 目录下声明。该目录下的每个文件名对应一个服务接口,全路径名,文件内容是该接口的一个或多个实现类的全路径名。

例如,假设你有一个接口 com.example.MyService,那么你需要在 META-INF/services 目录下创建一个名为 com.example.MyService 的文件,文件内容如下:

1
2
com.example.impl.MyServiceImpl1
com.example.impl.MyServiceImpl2

这表示 MyService 有两个实现类:MyServiceImpl1MyServiceImpl2

2. 加载服务

使用 ServiceLoader 加载服务的代码通常如下:

1
2
3
4
ServiceLoader<MyService> serviceLoader = ServiceLoader.load(MyService.class);
for (MyService service : serviceLoader) {
    service.doSomething(); // doSomething是MyService接口中定义的方法
}

ServiceLoader.load() 方法会查找并实例化所有在 META-INF/services/com.example.MyService 文件中列出的服务实现类,后面就是实现类的使用了,也就不难理解了。

以上述理论为基础,来看看Logback中的实现。

二、Logback代码解析,如何实现SLF4JServiceProvider接口

展开logback-classic源码包,找到META-INF/services文件夹,果然有名为org.slf4j.spi.SLF4JServiceProvider的文件

logback-classic中META-INF/services目录下的文件

内容如下:

1
ch.qos.logback.classic.spi.LogbackServiceProvider

即Logback提供的SLF4JServiceProvider接口的实现类。这样,根据上文的分析(教你分析SLF4J源码,掌握Logger接口实现类加载原理),在SLF4JLoggerFactory中便可以通过java.util.ServiceLoader获取到Logback的SLF4JServiceProvider实现,并在bind函数中执行后续的Logger初始化操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private final static void bind() {
        try {
            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();
                INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
                reportActualBinding(providersList);
            } else {
                INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
                Reporter.warn("No SLF4J providers were found.");
                Reporter.warn("Defaulting to no-operation (NOP) logger implementation");
                Reporter.warn("See " + NO_PROVIDERS_URL + " for further details.");

                Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
            }
            postBindCleanUp();
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }

在bind函数源码中可以看到,如果项目中发现了多个SLF4JServiceProvider的实现类,SLF4J只会取第一个进行初始化。

至此,SLF4J如何与Logback无缝集成的原理也就大致了解清楚了。对于Logback而言,原生实现了SLF4JServiceProvider接口,自然也就不需要桥接层了。对于像Log4j 2这种没有原生实现SLF4JServiceProvider的框架,其实我们也能大致猜到其桥阶层的实现原理:按照java.util.ServiceLoader原理向上实现SLF4J的SLF4JServiceProvider接口,向下调用对应的日志实现层框架,下一步我们就看一下Log4j 2的桥接方式,来印证一下我们的猜想。

本文由作者按照 CC BY 4.0 进行授权