SLF4J:我这可是日志的门面啊

1,046 阅读4分钟

众所周知,slf4j 是日志的门面。在阿里的《Java 开发手册》中有相关的记载:

那么,他是如何完成这个事情的呢?

根据官网的介绍,我们跑个 Demo

HelloWorld.java

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 日志测试类
 *
 * @author xisha
 * @date 2022/5/9 7:33 下午
 */
public class HelloWorld {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(HelloWorld.class);
        logger.info("Hello World");
    }
}

pom.xml

<dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.36</version>
        </dependency>
    </dependencies>

运行结果

注意,这个地方除了依赖 slf4j-simple 之外,还可以依赖 slf4j-log4j12 、slf4j-reload4j,slf4j-jdk14,slf4j-nop,slf4j-jcl,logback-classic 等等,相当于每个 依赖都是一个实现。

下载源码,可以看到每个model 都依赖了 slf4j-api

官网上的图也表明 slf4j-api 应该是个核心包

从上面两个图,再根据我多年来写bug的经验盲猜一波,这这个应该是有个什么什么东西注册上去了,然后 LoggerFactory 去拿的时候再把这个注册过的 Logger 拿出来

打个断点进去,那么很明显


StaticLoggerBinder --> JVM 注册

LogFactory --> 反射出 StaticLoggerBinder

通过 StaticLoggerBinder 获取 ILoggerFactory

通过 ILoggerFactory 获取 Logger

代码很简单,我就不帖了,听上去很简单啊这,你以为故事到这里就结束了么? 桥豆麻袋!StaticLoggerBinder 是在哪里定义的? 可以看到,StaticLoggerBinder 在 slf4j-api 是有定义的,且每个实现里也都定义了一个。那么问题来了,我同时引入了 slf4j-api 和一个具体的实现,这玩意儿难道不会冲突么?

但是在我的 Demo 工程里搜的的话,只有实现包里有 StaticLoggerBinder 这个类。哦豁?这是什么骚操作?slf4j-api 里的 StaticLoggerBinder 去哪里了呢?还是说我的源码下载的有问题?

奥~ 看起来好像是被这个框架给删掉了

但是这是个啥玩意儿?

一番搜索之后了解到这是个ant的插件,phase 表示的是生命周期,这个生命周期我就不展开了(毕竟我也不大懂),我们只需要知道 process-classes 是在 compile 之后执行的,这段代码的含义大概是 处理 classes 的时候(在编译之后),把 target/classes/org/slf4j/impl 目录下的文件都删掉的意思。

而我们的 slf4j-api 里的 StaticLoggerBinder ,也正好在这个包下面,你说巧不巧? 狗头.jpg

删掉之后,具体实现类里的相关实现就能趁虚而入,占领高地了。学到了学到了。

你以为故事到这里就结束了么?

你看红框框起来的那行代码

 // the next line does the binding
StaticLoggerBinder.getSingleton();

其他的代码都很好理解,但是这个是个什么鬼……

get 了一个单例,返回值也没用上,这个又是为啥呢,怎么就 dose the binding 了呢?这里就涉及到亿点点的类加载知识了。首先我们看一段Demo代码

// StatisticClass.java
public class StatisticClass {

    static {
        // 输出1
        System.out.println("StatisticClass init!");
    }

    private static final StatisticClass SINGLETON = new StatisticClass();
    
    public static StatisticClass getSingleton() {
        return SINGLETON;
    }
    
    private StatisticClass() {
    }
}

// Demo.java
public class Demo {
    public static void main(String[] args) {
        // 注释一
        // StatisticClass statisticClass;
        
        // 注释二
        //StatisticClass.getSingleton();
    }
}

来一道面试题,问在不打开注释一和注释二,只打开注释一,只打开注释二的三种情况下,输出1的结果分别是怎么样的?

如果你看过《深入理解java虚拟机》的话,对这个问题应该并不陌生,答案我就不贴了,简单说就是:我们的静态变量 SINGLETON = new StatisticClass() 应该是在类加载的初始化这步完成的。但是如果没有地方显示的执行 StatisticClass 的方法,那么StatisticClass是不会被初始化的。

从上面的例子我们就可以理解 slf4j 里那句 the next line does the binding 的含义了。

到这里似乎问题就结束了。但是这里又又又有个问题啊,这里他为啥不用接口呢?就 StaticLoggerBinder 这个类,为啥不设计成接口呢?这里明显是搞成接口更合理啊,想到这里,我是不是可以提一个issue,然后改吧改吧,混一波知名项目的PR?

太简单了少年……看最新的分支,这个确实已经设计成接口了,代码也很简单,我就不贴了,大概是下面这么个逻辑:

之前的findClass 也改成了如下代码的 findServiceProviders:

    private static List<SLF4JServiceProvider> findServiceProviders() {
        ServiceLoader<SLF4JServiceProvider> serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class);
        List<SLF4JServiceProvider> providerList = new ArrayList<SLF4JServiceProvider>();
        for (SLF4JServiceProvider provider : serviceLoader) {
            providerList.add(provider);
        }
        return providerList;
    }

奇奇怪怪的 StaticLoggerBinder.getSingleton(); 也改成了 PROVIDER.initialize();

好了,故事到这里就大概结束了,虽然给知名项目提PR的机会没有了,但是好歹是学到了一波知识,这波不亏,不亏不亏~