2.slf4j入口

文章目录

一、故事引入

故事要从下面这段代码说起

java 复制代码
public class App {

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

    public static void main( String[] args ) throws Exception {
        logger.info("abc");
    }
}

然后再加上一段日志配置

logback.xml

xml 复制代码
<configuration >
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %X{mdcKey} %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 定义日志级别和输出位置 -->
    <root level="info">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

pom.xml中的配置如下

xml 复制代码
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>2.0.15</version>
</dependency>

<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-classic</artifactId>
  <version>1.5.15</version>
</dependency>

以上是一个最简单的使用logback实现slf4j的demo, 运行之后我们可以在控制台看到打印如下

java 复制代码
SLF4J(I): Connected with provider of type [ch.qos.logback.classic.spi.LogbackServiceProvider]
2025-01-16 10:03:52 [main]  INFO  per.qiao.App - abc

为什么一句logger.info("abc");就把日志打印出来了, 其中原理有哪些, 那么本系列我们就一起来探究其中的门道。可以打开下载到的slf4j和logback源码项目, 跟着代码走...

二、原理探究

java 复制代码
LoggerFactory.getLogger(App.class);

对应的是org.slf4j.LoggerFactory#getLogger(Class<?> clazz)

java 复制代码
public static Logger getLogger(Class<?> clazz) {
    // 获取class对应的logger对象
    Logger logger = getLogger(clazz.getName());
    // 系统属性: slf4j.detectLoggerNameMismatch
    if (DETECT_LOGGER_NAME_MISMATCH) {
        // 调用当前方法的类
        Class<?> autoComputedCallingClass = Util.getCallingClass();
        // 调用getLogger的类和传入的class是否相等
        if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
            // 
            Reporter.warn(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                            autoComputedCallingClass.getName()));
            Reporter.warn("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
        }
    }
    return logger;
}

这里第一句getLogger是我们要探究的核心;

后面的判断用来校验当前执行getLogger方法的类和传入的Class对象是否是同一个类, 什么意思呢?? , 比如我们当前写代码的类是从别的类中复制而来, 那么可能这个private static final Logger logger = LoggerFactory.getLogger(App.class);就没有修改其中的App.class, 可能还是App1.class, 如果此时你开启了这种校验, 那么就会打印下面的一串警告日志。

开启方法

1、属性配置

java 复制代码
static {
    System.setProperty("slf4j.detectLoggerNameMismatch", "true");
}
private static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main( String[] args ) throws Exception {
	logger.info("abc");
}

2、idea启动项中 Add VM options 中添加-Dslf4j.detectLoggerNameMismatch=true, 也是可以的

-Dlogback.statusListenerClass=STDOUT

3、启动命令

java -Dslf4j.detectLoggerNameMismatch=true per.qiao.App

当然了, 2和3是一个东西

如果我们想要在main方法第一行加上属性设置行不行呢? , 就像下面这样

java 复制代码
public static void main( String[] args ) throws Exception {
	System.setProperty("slf4j.detectLoggerNameMismatch", "true");
	logger.info("abc");
}

其实是不行的哈, 因为main方是static修饰的, 而我们定义的logger也是static修饰的, jvm调用main方法之前会先调用LoggerFactory.getLogger进行日志初始化过程, 导致自己设置的失效; 关于一个类中默认的执行顺序, 大家也可以去了解下。

文归正传, 看下getLogger方法

java 复制代码
public static Logger getLogger(String name) {
    // 获取日志工厂
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}
  1. 拿到工厂
  2. 使用工厂拿到Logger对象

门道就在这个getILoggerFactory

java 复制代码
public static ILoggerFactory getILoggerFactory() {
    return getProvider().getLoggerFactory();
}

static SLF4JServiceProvider getProvider() {
    // 如果未初始化
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        // double check
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                // 初始化中
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                // 执行初始化
                performInitialization();
            }
        }
    }
    // ... 省略代码
}

关于synchronized时的double check使用, 大家一定要了然于胸

直接跳到核心方法bind

java 复制代码
private final static void bind() {
    try {
        // 1.获取SLF4JServiceProvider
        List<SLF4JServiceProvider> providersList = findServiceProviders();
        // 系统打印SLF4JServiceProvider获取的情况
        reportMultipleBindingAmbiguity(providersList);
        // 2.取第一个
        if (providersList != null && !providersList.isEmpty()) {
            PROVIDER = providersList.get(0);
            // SLF4JServiceProvider.initialize() is intended to be called here and nowhere else.
            // 3.初始化
            PROVIDER.initialize();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            // 打印实际使用的provider对象
            reportActualBinding(PROVIDER);
        } else {
            // ...
        }
        postBindCleanUp();
    } catch (Exception e) {
        failedBinding(e);
        throw new IllegalStateException("Unexpected initialization failure", e);
    }
}
  1. spi获取SLF4JServiceProvider
  2. 取第一个SLF4JServiceProvider
  3. 初始化它

这里两个核心方法findServiceProvidersPROVIDER.initialize()

findServiceProviders

java 复制代码
static List<SLF4JServiceProvider> findServiceProviders() {
        List<SLF4JServiceProvider> providerList = new ArrayList<>();

    // 加载当前类的类加载器
    final ClassLoader classLoaderOfLoggerFactory = LoggerFactory.class.getClassLoader();

    // 1.获取系统指定的SLF4JServiceProvider
    SLF4JServiceProvider explicitProvider = loadExplicitlySpecified(classLoaderOfLoggerFactory);
    if (explicitProvider != null) {
        providerList.add(explicitProvider);
        return providerList;
    }

     // 2.spi获取SLF4JServiceProvider
     ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);

    // 添加到集合中返回
    Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();
    while (iterator.hasNext()) {
        safelyInstantiate(providerList, iterator);
    }
    return providerList;
}

这里我们可以看到获取SLF4JServiceProvider有两个方式

  1. 系统指定可以使用哪个; 使用-Dslf4j.provider=SLF4JServiceProvider的类全路径指定, 当你的项目中由于种种原因配置了多个slf4j的实现模块的时候, 这时候你就可以用这个配置指定使用哪个具体实现, 例如slf4j-jdk14.jarlogback-classic.jar同时存在的话, 你可以排除某个依赖或者使用这个配置指定
  2. 使用spi获取获取所有的SLF4JServiceProvider, 只有其中一个生效

spi获取的顺序是按照包顺序获取的, 也就是按照自然排序, 所以第一个就是名字排序靠前的那个包中的

关于spi, 在一些源码中经常被用到, 例如springboot的自动装配, dubbo加载, 它俩用的这个思路, 但是属于spi的变种

java 复制代码
// springboot
SpringFactoriesLoader.loadFactoryNames(type, classLoader)

// duoboo
ExtensionLoader.getExtensionLoader(ExtensionFactory.class)
    					  .getAdaptiveExtension());

最核心的部分就是PROVIDER.initialize

SLF4JServiceProvider#initialize

这个方法有具体的实现模块提供, 本节不介绍

三、SLF4JServiceProvider

slf4j实现模块的入口类, 通过spi加载

java 复制代码
/**
 * 它取代了SLF4J 1.0版本中使用的旧的静态绑定机制。X到1.7.x。
 */
public interface SLF4JServiceProvider {

    // 获取日志工厂实例
    public ILoggerFactory getLoggerFactory();

    // 日志标记工厂
    public IMarkerFactory getMarkerFactory();

    // 支持mdc的
    public MDCAdapter getMDCAdapter();

    // 版本校验的
    public String getRequestedApiVersion();

    // 用来初始化实现模块
    public void initialize();
}

这里最核心的是initialize方法

getMarkerFactory方法, 在打印日志的时候, 有写方法有Marker参数, 例如

java 复制代码
public void info(Marker marker, String msg);
public void info(Marker marker, String format, Object arg);

getMDCAdapter方法, 在使用MDC的时候, 会用到它

四、总结

slf4j提供了操作日志的门面

  1. 日志初始化入口实LoggerFactory.getLogger(App.class);
  2. 可以通过-Dslf4j.provider=SLF4JServiceProvider的类全路径来指定日志最终使用的slf4j实现
  3. 通过spi加载了SLF4JServiceProvider对象
  4. 初始化SLF4JServiceProvider后, 通过它得到具体的Logger对象
相关推荐
NullPointerExpection8 天前
java 中 main 方法使用 KafkaConsumer 拉取 kafka 消息如何禁止输出 debug 日志
java·kafka·log4j·slf4j
小小工匠17 天前
Java - 日志体系_Simple Logging Facade for Java (SLF4J)日志门面_SLF4J集成logback 及 原理分析
logback·slf4j
暮色里de白雪檐17 天前
基于 Slf4j 和 AOP 的自动化方法执行时间日志记录方案
logback·日志·slf4j·aop·日志打印
小小工匠17 天前
Java - 日志体系_Simple Logging Facade for Java (SLF4J)日志门面_SLF4J集成Log4j1.x 及 原理分析
log4j·slf4j
zfj3211 个月前
java日志框架:slf4j、jul(java.util.logging)、 log4j、 logback
java·log4j·logback·java日志框架·slf4j·jul
white camel3 个月前
Java从入门到架构师_日志框架系列
java·logback·log4j2·日志·slf4j
笨手笨脚の3 个月前
【Bug】使用Log4j可以打印日志,使用Slf4j无法打印日志
log4j·日志·slf4j·1024程序员节
艾伦~耶格尔3 个月前
Spring Boot 之 Lombok 使用详解
spring boot·后端·slf4j·lombok·构造器·entity
左直拳4 个月前
设置spring boot禁止日志输出到控制台
spring boot·后端·乱码·logback·slf4j·输出到控制台