Erlo

从SLF4J源码角度分析阿里开发手册日志规约

时间:2021-05-26   阅读:37次   来源:开源中国
页面报错
点赞

欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习

1 日志规约

阿里巴巴开发手册日志规约章节有一条强制规定:应用中不可直接使用日志系统(Log4j、Logback)API,而应依赖使用日志框架SLF4J中的API。使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);

我们在使用日志框架过程中会发现,日志框架种类很多如slf4j、log4j、logback等等,在引入依赖时很容易混淆。那么这些框架是什么关系、应该如何使用就是本文需要回答的问题。

2 实例分析

在编写代码之前我们首先了解slf4j全称,我认为这会对理解这个框架有所帮助:

Simple Logging Facade for Java

全称含义就是Java简单日志门面,我们知道有一种设计模式称为门面模式,其本质是化零为整,通过一个对象将散落在各处的功能整合在一起,这样外部只要通过与这个对象交互,由该对象选择具体实现细节。slf4j就是这样一个门面,应用程序只需要和slf4j进行交互,slf4j选择使用哪一个日志框架的具体实现。

从SLF4J源码角度分析阿里开发手册日志规约

 

2.1 slf4j-jdk14

(1) 引入依赖

<dependencies>
  
  <dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-apiartifactId>
    <version>1.7.30version>
  dependency>
  
  
  <dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-jdk14artifactId>
    <version>1.7.30version>
  dependency>
deendencies>

(2) 代码实例

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
    private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        logger.info("info message");
        System.out.println("LogTest");
        logger.error("error message");
    }
}

(3) 输出日志

LogTest
三月 14, 2021 11:39:14 上午
com.my.log.test.jdk14.LogTest main

信息: info message
三月 14, 2021 11:39:14 上午
com.my.log.test.jdk14.LogTest main

严重: error message

2.2 slf4j-simple

(1) 引入依赖

<dependencies>
  
  <dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-apiartifactId>
    <version>1.7.30version>
  dependency>
  
  
  <dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-simpleartifactId>
    <version>1.7.30version>
  dependency>dependencies>

(2) 代码实例

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
    private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        logger.info("info message");
        System.out.println("LogTest");
        logger.error("error message");
    }

(3) 输出日志

[main] INFO com.my.log.test.simple.LogTest - info message
LogTest
[main] ERROR com.my.log.test.simple.LogTest - error message

2.3 logback

(1) 引入依赖

<dependencies>
  
  <dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-apiartifactId>
    <version>1.7.30version>
  dependency>
  
  
  <dependency>
    <groupId>ch.qos.logbackgroupId>
    <artifactId>logback-coreartifactId>
    <version>1.2.3version>
  dependency>
  <dependency>
    <groupId>ch.qos.logbackgroupId>
    <artifactId>logback-classicartifactId>
    <version>1.2.3version>
  dependency>
depedencies>

(2) 代码实例

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
    private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        logger.info("info message");
        System.out.println("LogTest");
        logger.error("error message");
    }
}

(3) 输出日志

11:40:53.406 [main] INFO com.my.log.test.logbck.LogTest - info message
LogTest
11:40:53.410 [main] ERROR com.my.log.test.logbck.LogTest - error message

2.4 slf4j-log4j12

(1) 引入依赖

<dependencies>
  
  <dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-apiartifactId>
    <version>1.7.30version>
  dependency>
  
  
  <dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-log4j12artifactId>
    <version>1.7.30version>
  dependency>
dependencies>

(2) 代码实例

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogTest {
    private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
    public static void main(String[] args) {
        logger.info("info message");
        System.out.println("LogTest");
        logger.error("error message");
    }
}

(3) 日志配置

<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
  <appender name="myConsoleAppender" class="org.apache.log4j.ConsoleAppender">
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="[%d{dd HH:mm:ss,SSS} %-5p] [%t] %c{2} - %m%n" />
    layout>
    
    <filter class="org.apache.log4j.varia.LevelRangeFilter">
      <param name="levelMin" value="debug" />
      <param name="levelMax" value="error" />
      <param name="AcceptOnMatch" value="true" />
    filter>
  appender>
  <root>
    <priority value="debug" />
    <appender-ref ref="myConsoleAppender" />
  root>
log4j:configuration>

(4) 输出日志

[14 11:41:39,198 INFO ] [main] log4j.LogTest - info message
LogTest
[14 11:41:39,201 ERROR] [main] log4j.LogTest - error message

3 源码分析

我们发现上述实例中Java代码并没有变化,只是将引用具体日志框架实现进行了替换,例如依赖从simple替换为log4j,具体日志服务实现就替换成了log4j,这到底是怎么实现的?我们通过阅读源码回答这个问题。

3.1 阅读准备

(1) 源码地址

目前最新版本2.0.0-alpha2-SNAPSHOT

https://github.com/qos-ch/slf4j

(2) 项目结构

我们从项目结构可以看出一些信息:门面是api模块,具体实现包括jdk14、log4j12、simple模块,需要注意logback是同一个作者的另一个项目不在本项目。

从SLF4J源码角度分析阿里开发手册日志规约

 

(3) 阅读入口

package org.slf4j;
public class NoBindingTest {
    public void testLogger() {
        Logger logger = LoggerFactory.getLogger(NoBindingTest.class);
        logger.debug("hello" + diff);
        assertTrue(logger instanceof NOPLogger);
    }
}

3.2 源码分析

LoggerFactory.getLogger

public final class LoggerFactory {
    public static Logger getLogger(Class clazz) {
        Logger logger = getLogger(clazz.getName());
        if (DETECT_LOGGER_NAME_MISMATCH) {
            Class autoComputedCallingClass = Util.getCallingClass();
            if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
                Util.report(String.format("Detected logger name mismatch. Given name: "%s"; computed name: "%s".", logger.getName(),
                                          autoComputedCallingClass.getName()));
                Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
            }
        }
        return logger;
    }
}

getLogger(clazz.getName())

public final class LoggerFactory {
    public static Logger getLogger(String name) {
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        return iLoggerFactory.getLogger(name);
    }
}

getILoggerFactory()

public final class LoggerFactory {
    public static ILoggerFactory getILoggerFactory() {
        return getProvider().getLoggerFactory();
    }
}

getProvider()

public final class LoggerFactory {
    static SLF4JServiceProvider getProvider() {
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    performInitialization();
                }
            }
        }
        switch (INITIALIZATION_STATE) {
        case SUCCESSFUL_INITIALIZATION:
            return PROVIDER;
        case NOP_FALLBACK_INITIALIZATION:
            return NOP_FALLBACK_FACTORY;
        case FAILED_INITIALIZATION:
            throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
        case ONGOING_INITIALIZATION:
            return SUBST_PROVIDER;
        }
        throw new IllegalStateException("Unreachable code");
    }
}

performInitialization()

public final class LoggerFactory {
    private final static void performInitialization() {
        bind();
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
    }
}

bind()

public final class LoggerFactory {
    private final static void bind() {
        try {
            // 核心代码
            List providersList = findServiceProviders();
            reportMultipleBindingAmbiguity(providersList);
            if (providersList != null && !providersList.isEmpty()) {
             PROVIDER = providersList.get(0);
             PROVIDER.initialize();
             INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
                reportActualBinding(providersList);
            }
            // 省略代码
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }
}

findServiceProviders()

这是加载具体日志实现的核心方法,使用SPI机制加载所有SLF4JServiceProvider实现类:

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

SPI(Service Provider Interface)是一种服务发现机制,本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件加载实现类,这样可以在运行时动态为接口替换实现类,通过SPI机制可以为程序提供拓展功能。本文以log4j为例说明使用SPI功能的三个步骤:

(a) 实现接口

public class Log4j12ServiceProvider implements SLF4JServiceProvider

(b) 配置文件

文件位置:
src/main/resources/META-INF/services/

文件名称:
org.slf4j.spi.SLF4JServiceProvider

文件内容:
org.slf4j.log4j12.Log4j12ServiceProvider

(c) 服务加载

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

只要各种日志实现框架按照SPI约定进行代码编写和配置文件声明,即可以被LoggerFactory加载,slf4j会获取第一个作为实现。

public final class LoggerFactory {
    private final static void bind() {
        try {
            // 使用SPI机制加载具体日志实现
            List providersList = findServiceProviders();
            reportMultipleBindingAmbiguity(providersList);
            if (providersList != null && !providersList.isEmpty()) {
                // 获取第一个实现
                PROVIDER = providersList.get(0);
                PROVIDER.initialize();
                INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
                reportActualBinding(providersList);
            }
            // 省略代码
        } catch (Exception e) {
            failedBinding(e);
            throw new IllegalStateException("Unexpected initialization failure", e);
        }
    }
}

分析到这里我们的问题应该可以得到解答:假设我们项目只引入了slf4j和log4j,相当于只有log4j这一个具体实现,那么本项目就会使用log4j框架。如果将log4j依赖换为logback,那么项目在不改动代码的情况下会使用logback框架。

4 文章总结

本文我们从阿里开发手册日志规约出发,首先分析了如何使用不同的日志框架,然后我们从问题出发(不修改代码即可替换具体日志框架)进行slf4j源码阅读,从源码中我们知道实现核心是SPI机制,这个机制可以动态加载具体日志实现。关于SPI源码分析请参看笔者文章DUBBO系列(1)什么是SPI机制 ,希望本文对大家有所帮助。

欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习

相关推荐

提交留言

评论留言

还没有评论留言,赶紧来抢楼吧~~

吐槽小黑屋()

* 这里是“吐槽小黑屋”,所有人可看,只保留当天信息。

  • Erlo吐槽

    Erlo.vip2021-07-29 13:34:45Hello、欢迎使用吐槽小黑屋,这就是个吐槽的地方。
  • 返回顶部

    给这篇文章打个标签吧~

    棒极了 糟糕透顶 好文章 PHP JAVA JS 小程序 Python SEO MySql 确认