04、slf4j(日志门面)

举报
长路 发表于 2022/11/28 08:35:01 2022/11/28
【摘要】 文章目录前言一、认识slf4j1.1、slf4j概述1.2、第三方jar包1.3、切换日志框架详略图1.4、相关注意点二、实际应用2.1、配合自身简单日志实现(slf4j-simple)2.2、配置logback日志实现2.3、配置Log4j日志实现(需适配器)2.4、配置JUL日志实现(需适配器)2.4、添加slf4j-nop依赖(日志开关)三、原理分析3.1、初始绑定日志实现原理四、桥接旧的日

@[toc]


前言

本篇博客主要介绍现如今主流的日志门面技术slf4jSpringboot中推荐使用该日志门面技术。其他日志框架内容可见日志专栏。

所有博客文件目录索引(包含日志框架系列学习):博客目录索引(持续更新)



一、认识slf4j

1.1、slf4j概述

官网:http://www.slf4j.org/

用户手册:http://www.slf4j.org/manual.html

slf4j(Simple Logging Facade For Java):为所有的日志框架提供了一套标准、规范的API框架,主要是提供了接口,具体的实现交由对应的日志框架,例如Log4jLogbackLog4j2等。其自己本身也提供了简单的日志实现(slf4j-simple)。

现如今对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配置上具体的实现框架,中间使用桥接器来完成桥接。

介绍其中两个类:日志实例Logger以及LogFactory.getLogger()(工厂)获取日志实例。



1.2、第三方jar包

想要使用slf4j日志门面,需要使用第三方jar包,下面为pom.xml的对应坐标:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>

若是想要之后切换日志框架,最好先引入指定版本的slf4j-api(尽管之后引入的对应的slf4j日志实现框架中有对应api依赖),来进行统一的API管理。



1.3、切换日志框架详略图

我们去到slf4j官网的用户手册网页即可查看到下图:

application下面的SLF4J API表示slf4j的日志门面,包含三种情况:

  • ①若是只导入slf4j日志门面没有导入对应的日志实现框架,那么日志功能将会是默认关闭的,不会进行日志输出的。
  • ②蓝色图里Logbackslf4j-simpleslf4j-nop出来的比较晚就遵循了slf4jAPI规范,也就是说只要导入对应的实现就默认实现了对应的接口,来实现开发。
  • ③对于中间两个日志实现框架log4j(slf4j-log4j12)、JUL(slf4j-jdk14)由于出现的比slf4j早,所以就没有遵循slf4j的接口规范,所以无法进行直接绑定,中间需要加一个适配层(Adaptation layer),通过对应的适配器来适配具体的日志实现框架,其对应的适配器其实就间接的实现了slf4j-api的接口规范。

注意:在图中对于logback需要引入两个jar包,不过在maven中有一个传递的思想,当配置logback-classic时就会默认传递core信息,所以我们只需要引入logback-classicjar包即可。



1.4、相关注意点

在使用slf4j日志门面的过程中,若是引入了两个日志实现框架会报以下错误,并会默认实现第一个引入的日志实现:

image-20210307220735026

  • 这里是同时配置simple以及logback情况

注意:以pom.xml中配置顺序有关!!!



二、实际应用

2.1、配合自身简单日志实现(slf4j-simple)

若想使用自身的日志实现框架,需要引入第三方jarslf4j-simple(slf4j自带实现类):

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.5</version>
</dependency>
  • 其中该坐标包含了对应的slf4j-api的依赖,可以不用手动导入slf4j-api
  • image-20210307221415150

测试程序

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

public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {
        System.out.println(LOGGER.getName());//xyz.changlu.LogTest
        //1、打印日志记录
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");

        //2、占位符输出
        String name = "changlu";
        int age = 20;
        LOGGER.info("报错,name:{},age:{}",name,age);

        //3、打印堆栈信息
        try {
            int i = 5/0;
        }catch (Exception e){
            LOGGER.error("报错",e);
        }

    }
}

image-20210307213141794

  • 默认日志等级为INFO,能够实现占位符输出,并且可以在日志等级方法中传入异常实例,来打印对应的日志信息。

注意点:若是我们只使用日志门面而没有导入指定的日志实现框架,调用Logger实例并调用日志方法会出现以下错误:

image-20210307212325551



2.2、配置logback日志实现

引入logback-classic的jar包,其中包含有slf4j-api以及logback-core的依赖,所以只需要引入该依赖即可:

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

image-20210307222202760

测试程序

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

public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {
        System.out.println(LOGGER.getName());//xyz.changlu.LogTest
        //1、打印日志记录
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");

        //2、占位符输出
        String name = "changlu";
        int age = 20;
        LOGGER.info("报错,name:{},age:{}",name,age);

        //3、打印堆栈信息
        try {
            int i = 5/0;
        }catch (Exception e){
            LOGGER.error("报错",e);
        }

    }
}

image-20210307222352497



2.3、配置Log4j日志实现(需适配器)

①首先添加日志框架实现依赖

之前在1.3中介绍,对于Log4jJUL这些比较早出现的日志实现框架需要有对应的适配层,在这里我们引入对应的适配器slf4j-log412的依赖坐标:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>
  • slf4j-log4j12坐标中实际就包含了Log4j以及slf4j-api依赖,所以我们添加该坐标即可。

image-20210307224435135

②添加log4j.properties配置文件

# rootLogger日志等级为trace,输出到屏幕上
log4j.rootLogger = trace,console

# console
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern= [%-5p]%r %l %d{yyyy-MM-dd HH:mm:ss:SSS} %m%n

测试程序

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

public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {
        System.out.println(LOGGER.getName());//xyz.changlu.LogTest
        //1、打印日志记录
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");

        //2、占位符输出
        String name = "changlu";
        int age = 20;
        LOGGER.info("报错,name:{},age:{}",name,age);

        //3、打印堆栈信息
        try {
            int i = 5/0;
        }catch (Exception e){
            LOGGER.error("报错",e);
        }

    }
}

image-20210307225625195



2.4、配置JUL日志实现(需适配器)

对于slf4j日志门面实现JUL日志框架需要使用是适配器来实现slf4j的日志接口,我们直接添加对应适配器依赖如下:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.5.6</version>
</dependency>
  • JUL是我们jdk自带的日志框架,所以不需要额外引入jar包,引入slf4j-jdk14坐标,其中就包含了slf4j-api的依赖,所以我们只需要引入一个坐标即可。

image-20210307225832423

测试程序

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

public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {
        System.out.println(LOGGER.getName());//xyz.changlu.LogTest
        //1、打印日志记录
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");

        //2、占位符输出
        String name = "changlu";
        int age = 20;
        LOGGER.info("报错,name:{},age:{}",name,age);

        //3、打印堆栈信息
        try {
            int i = 5/0;
        }catch (Exception e){
            LOGGER.error("报错",e);
        }

    }
}

image-20210307230305344



2.4、添加slf4j-nop依赖(日志开关)

当添加了slf4j-nop坐标后,其相当于一个日志开关,导入实现以后就不会使用任何实现框架:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-nop</artifactId>
    <version>1.7.27</version>
</dependency>

image-20210307231206025

测试程序

public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
    }
}

image-20210307230924201

  • 默认就关闭了slf4j的日志框架使用。


三、原理分析

3.1、初始绑定日志实现原理

slf4j-api中我们通常使用下面的方法来获取logger实例:

public static final Logger LOGGER = LoggerFactory.getLogger(LogTest.class);

获得的实例实际上跟我们导入jar包有关,那么它是如何进行初始配置的呢?看下面源码:

public final class LoggerFactory {
    
   public static Logger getLogger(Class<?> clazz) {
        //1、调用一个重载方法,传入类名
        Logger logger = getLogger(clazz.getName());//见2
        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;
    }
    
    //2、根据类名来获取logger实例
    public static Logger getLogger(String name) {
        //获取ILoggerFactory接口的实现类(接口方法是getLogger())
        ILoggerFactory iLoggerFactory = getILoggerFactory();//见3
        return iLoggerFactory.getLogger(name);
    }
    
    //3、获取ILoggerFactory的实例
    public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            synchronized (LoggerFactory.class) {
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    //执行初始化方法
                    performInitialization();//见4
                }
            }
        }
        ...
    }
    
    //4、执行初始化操作
    private final static void performInitialization() {
        bind();//见5
        if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
            versionSanityCheck();
        }
    }
    
    //5、绑定操作
    private final static void bind() {
        try {
            Set<URL> staticLoggerBinderPathSet = null;
            if (!isAndroid()) {
                //下面两行比较关键,这行是查找可能的静态日志执行器路径使用Set来接收
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();//见6
                //查看set中是否超过1个路径,若是则进行窗口输出提示信息
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);//见7
            }
            //
            StaticLoggerBinder.getSingleton();
            INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
            reportActualBinding(staticLoggerBinderPathSet);
        } catch (NoClassDefFoundError ncde) {
            String msg = ncde.getMessage();
            ....
    }
    
    //6、这里是查找有关org/slf4j/impl/StaticLoggerBinder.class路径,都放置到set中返回
    static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        // use Set instead of list in order to deal with bug #138
        // LinkedHashSet appropriate here because it preserves insertion order
        // during iteration
        Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
        try {
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration<URL> paths;
            if (loggerFactoryClassLoader == null) {
                //从类加载器中进行查找是否有org/slf4j/impl/StaticLoggerBinder.class路径
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }
            //这里依旧是根据查找到的路径继续往下延伸查找并添加到Set中去
            while (paths.hasMoreElements()) {
                URL path = paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
        } catch (IOException ioe) {
            Util.report("Error getting resources from path", ioe);
        }
        return staticLoggerBinderPathSet;
    }
}

52行的查找org/slf4j/impl/StaticLoggerBinder.class路径操作存放到Set中返回。

看到上面方法6中是不是有个疑惑,查找org/slf4j/impl/StaticLoggerBinder.class这个相关路径有什么用,与我们要加载对应的日志实现有什么关系呢?

我们在本次源码过程中引入两个日志实现框架slf4j-log4j12slf4j-jdk14(这两个都是slf4j为较早出现的日志设置的适配器),引入jar包之后,我们尝试搜索一下StaticLoggerBinder这个类:

image-20210308190649628

好家伙原来slf4j实现的相关适配器的名称都叫StaticLoggerBinder啊,我们继续看下去,看下jdk14的吧(就是JUL):

image-20210308190946045

该工厂类中的实例loggerFactory是获取了一个JDK14的工厂类,那么我们继续看向适配器中内容:

image-20210308191117164

重写了getLogger()方法,其中实例化了JULlogger实例,调用有参构造传入并且创建了一个JDK14的适配器,我们再看下这个适配器中都做了些什么:

image-20210308191422884

好家伙其中包含了各个日志等级的方法,其中都包含了JUL的日志操作。


53行中调用的reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);来报告多个绑定日志实现框架

public final class LoggerFactory {
    
    //7、来进行报告含有多个日志框架的路径
    private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
        //该方法判断是否set中数量>1
        if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {//见8
            //若是超过1个的话就会报该问题(就是我们之前1.4中的注意点报错)
            Util.report("Class path contains multiple SLF4J bindings.");
            for (URL path : binderPathSet) {
                Util.report("Found binding in [" + path + "]");
            }
            Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
        }
    }
    
    //8、判断set中的容量大小是否>1
    private static boolean isAmbiguousStaticLoggerBinderPathSet(Set<URL> binderPathSet) {
        return binderPathSet.size() > 1;
    }
}

该方法就是来检测是否有多个日志实现框架导入,若是有则报出提示信息。



四、桥接旧的日志实现框架

介绍桥接器

直接举场景来说明:对于一些老项目直接使用的是Log4jJUL的日志实现框架,并没有使用到日志门面来进行管理日志框架,当项目需要迭代升级时,我们想把原先的日志实现框架切换为logback,此时会出现一个问题,若是我们直接将对应的日志jar包更改为logback,那么项目中会出现大量报错,因为原先引入的包是import org.apache.log4j.Logger;,此时就会出现问题,我们需要重新修改大量的代码,需要耗费大量的时间与精力。

解决方案:在slf4j中可以使用桥接器从而让我们不用修改一行代码实现日志框架的切换。在slf4j中附带了几个桥接木块,这些模块对于log4jJCLJULAPI调用重定向(其实就是全限定名与原来的完全相同)。

下图包含了对应的解决方案:

image-20210308195930231

  • 可用log4j-over-slf4j.jar替代Log4j


4.1、log4j-over-slf4j桥接器使用

解决过程

问题描述

模拟场景:老项目直接使用的是org.apache.log4j.Logger,现今项目迭代升级,需要使用Logback日志框架。

我们首先将log4jjar包移除,之后引入logback-classic依赖坐标,此时就会出现下方情况:

image-20210308200946916

image-20210308200929298


解决方案:使用桥接器log4j-over-slf4j

引入坐标依赖:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>

image-20210308201611701

  • 不用修改任何代码即可替换日志框架。


原理分析

image-20210308202026344

  • 从导入的包来看,其中的全限定类名与原本Log4j的一毛一样,接着看是如何达到无缝衔接的。

同样是下方的获取logger实例方法:

import org.apache.log4j.Logger;

public class LogTest {
    
    public static final Logger LOGGER = Logger.getLogger(LogTest.class);
}

查看源码

//即引入的log4j-over-slf4j坐标
package org.apache.log4j;
public class Logger extends Category {

    //1、
    public static Logger getLogger(Class clazz) {
        return getLogger(clazz.getName());//见2
    }
    
    //2、
    public static Logger getLogger(String name) {
        return Log4jLoggerFactory.getLogger(name);//见3
    }
    
    //4、有参构造
    protected Logger(String name) {
        super(name);//调用的是Category的有参构造 去5
    }

}

//log4j的工厂类
class Log4jLoggerFactory {
    
    //3、获取logger实例
    public static Logger getLogger(String name) {
        Logger instance = (Logger)log4jLoggers.get(name);
        if (instance != null) {
            return instance;
        } else {
            //重要的点来了:注意看这个方法
            Logger newInstance = new Logger(name);//回到上面的Logger类中的4方法
            Logger oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
            return oldInstance == null ? newInstance : oldInstance;
        }
    }
}

//Loger类的父类
public class Category {
    
    protected Logger slf4jLogger;
    
    //5、有参构造
    Category(String name) {
        this.name = name;
        //注意这个方法,LoggerFactory.getLogger()获取的是slf4j的对应Logger
        this.slf4jLogger = LoggerFactory.getLogger(name);
        if (this.slf4jLogger instanceof LocationAwareLogger) {
            this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
        }

    }
}
  • 简单来说就是slf4j的开发者提供了一个与Log4j的全限定类名相同的一个包,其中的方法名称与Log4j的都相同,在getLogger()方法中实际获取到了slf4j对应的Logger实例。

如下图log4j是灰色表示的是移除掉,使用log4j-over-slf4j

image-20210308203251582



4.2、jul、jcl桥接器

当使用单独的日志实现框架想要替换成如logback日志框架时可使用对应的桥接器来进行替代:

<!-- jul -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<!--jcl -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>

替换了之后别忘了引入对应要替换的日志实现框架。



三个slf4j日志实现框架与桥接器不能同时使用

以下的jar包不能同时出现:

  • log4j-over-slf4j.jar(桥接器)和slf4j-log4j12.jar
  • jcl-over-slf4j.jarslf4j-jcl.jar
  • jul-to-slf4j.jarslf4j-jdk14.jar

为什么不能同时出现呢?我们借第一组进行查看:

①首先看下相应的依赖导入

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>

image-20210308204328104

  • log4j-over-slf4j坐标依赖

image-20210308204440558

image-20210308204451506

  • slf4j-log4j12坐标依赖,注意该包的全限定类名与log4j的权限定类名一致。
    • image-20210308204634637

②我们运行下程序查看一下

import org.apache.log4j.Logger;

public class LogTest {
    //获取Logger实例
    public static final Logger LOGGER = Logger.getLogger(LogTest.class);

    public static void main(String[] args) {
        LOGGER.error("error");
    }
}

image-20210308204932139

为什么会出现这种情况呢,看下面原理图一下子懂了:

image-20210308205018513

  • 其实说白了就是log4jlog4j-over-slf4j的权限定包名都是相同的,当在slf4j-log4j12的适配器中若是找到log4j-over-slf4j中的log4j时此时就会出现无限死循环,也就导致栈溢出了。

前后引入jar包位置改变不会报错

不过我发现了一个问题:如果pom.xml中的坐标前后位置,两个jar包都导入了也不会报错

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
  • 先引入slf4j-log4j12再引入log4j-over-slf4j运行就不会报错,可能是在slf4j-log4j12的适配器中找Logger时,pom.xmllog4j首先被加载到了所以就不会报错了。

说明:尽管这样引入不会报错,我们也一定不要这样子引入桥接器与slf4j的日志实现框架,这样也没必要。



总结

1、对于slf4j切换日志框架我们实际上就只需要引入slf4j提供的各个日志实现依赖即可(对应坐标其中包含了slf-api以及对应实现框架依赖)。

2、对于一开始就没有进行使用日志门面而只是单单使用日志框架的项目,若是想要不修改代码进行切换日志框架,我们就要考虑使用slf4j桥接器来进行日志框架切换。

3、slf4j提供的日志框架实现尽量不要与桥接器同时使用,否则极有可能会报错!



参考资料

[1] 视频:2020年Java进阶教程,全面学习多种java日志框架

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。