slf4j

发布时间 2023-05-06 11:00:09作者: JaxYoun

日志门面Slf4j

转载自:https://zhuanlan.zhihu.com/p/394685808

Java日志的恩怨情仇

img

  • 1996年早期,欧洲安全电子市场项目组决定编写它自己的程序跟踪API(Tracing API)。经过不断的完善,这个API终于成为一个十分受欢迎的Java日志软件包,即Log4j(由Ceki创建)。
  • 后来Log4j成为Apache基金会项目中的一员,Ceki也加入Apache组织。后来Log4j近乎成了Java社区的日志标准。据说Apache基金会还曾经建议Sun引入Log4j到Java的标准库中,但Sun拒绝了。
  • 2002年Java1.4发布,Sun推出了自己的日志库JUL(Java Util Logging),其实现基本模仿了Log4j的实现。在JUL出来以前,Log4j就已经成为一项成熟的技术,使得Log4j在选择上占据了一定的优势。
  • 接着,Apache推出了Jakarta Commons Logging,JCL只是定义了一套日志接口(其内部也提供一个Simple Log的简单实现),支持运行时动态加载日志组件的实现,也就是说,在你应用代码里,只需调用Commons Logging的接口,底层实现可以是Log4j,也可以是Java Util Logging。
  • 后来(2006年),Ceki不适应Apache的工作方式,离开了Apache。然后先后创建了Slf4j(日志门面接口,类似于Commons Logging)和Logback(Slf4j的实现)两个项目,并回瑞典创建了QOS公司,QOS官网上是这样描述Logback的:The Generic,Reliable Fast&Flexible Logging Framework(一个通用,可靠,快速且灵活的日志框架)。
  • Java日志领域被划分为两大阵营:Commons Logging阵营和Slf4j阵营。
  • Commons Logging在Apache大树的笼罩下,有很大的用户基数。但有证据表明,形式正在发生变化。2013年底有人分析了GitHub上30000个项目,统计出了最流行的100个Libraries,可以看出Slf4j的发展趋势更好。
  • Apache眼看有被Logback反超的势头,于2012-07重写了Log4j 1.x,成立了新的项目Log4j 2, Log4j 2具有Logback的所有特性。
  • 如今日志框架已经发展为:Slf4j作为API,实现分为logback与log4j(Commons Logging因为效率和API设计等问题,现在逐渐淡出舞台了)

上述日志框架可以分为两类:

  1. 一是提供对外接口的日志门面,包括Slf4j和Commons Logging(JCL)
  2. 二是提供具体实现的日志系统,也可称为日志框架,包括Log4j、JUL、Logback、Log4j2

Slf4j简介
SLF4J(Simple Logging Facade for Java)用作各种日志框架(java.util.logging,logback,log4j,log4j2)的简单外观或抽象,允许最终用户在部署时插入所需的日志框架,是一款Java程序编写的日志门面框架,其本身定义了统一的日志接口,且对不同的日志实现框架进行抽象化,我们的应用只需要跟SLF4J进行沟通,而不需要跟具体实现框架直接沟通,从而调用具体实现框架的相关方法进行日志记录。这样我们可以方便的切换日志的实现框架,且无需改动我们的应用,这也是门面模式的优点。

  • Slf4j可以与客户端解耦

想象一下下面的场景:

有一个别人写的很棒的类库,里面使用的是jdk自带的java.util.logging.Logger这个日志系统,现在你有一个程序需要用到这个类库,并且你自己的程序现在是使用apache的org.apache.log4j.Logger这个日志系统。那么问题来了,如果你的程序导入了这个类库,那么必须支持这两种日志系统,这会很繁琐且不利于管理。但是使用Slf4j就可以屏蔽两者差异,并做到统一管理。

  • Slf4j的API比其他日志系统自带的API更优秀

log4j-api的info函数有两种使用方式:

void info(Object message)
void info(Object message, Throwable t)

假设要输出的是一个字符串,并且字符串中包含变量,则message参数就必须使用字符串相加操作。而Slf4j使用占位符来输入message,并且使用日志级别检查。

boolean isInfoEnabled();
void info(String var1);
void info(String var1, Object var2);
void info(String var1, Object var2, Object var3);
void info(String var1, Object... var2);
void info(String var1, Throwable var2);

img

Slf4j的核心包为slf4j-api,其结构为

img

org.slf4j.Logger接口下定义了我们常用的日志方法,trace、debug、info、warn、error...

slf4j有两个重要概念:slf4j adaptor和slf4j bridge

spring-boot-starter-logging包同时引入了这两者

img

Slf4j绑定器 (SLF4J adaptor)

logback-classic是logback绑定到slf4j的绑定器,Slf4j-api中定义了一个绑定器接口LoggerFactoryBinder,所有slf4j绑定器都需要实现LoggerFactoryBinder接口

因此logback-classic下有个实现类StaticLoggerBinder。借助此类Slf4j实现了静态绑定。下文详细叙述。

Slf4j桥接器(SLF4J bridge)

其他日志门面(jcl)或日志系统(jul、log4j、log4j2)其实也定义了一组API,有些代码(比如我们引入一些jar包,里面的代码)调用了这些API,为了将这些调用统一起来都指向Slf4j API,我们就需要桥接器。

jul-to-slf4j和log4j-to-slf4j是slf4j的桥接器。

官网对log4j-to-slf4j给出的解释 The Log4j to SLF4J bridge allows applications coded to the Log4j API to be routed to SLF4J.The Log4j to SLF4J bridge is dependent on the Log4j API and the SLF4J API.

意思是说应用程序使用log4j api会被路由导向slf4j api。桥接器只依赖 Log4j API 和 SLF4J API.

绑定器和桥接器的区别在于:

  • 绑定器下面有具体的实现,Slf4j接口的具体实现由它指定。

  • 桥接器只是将其他API的调用迁移到Slf4j API,和具体的实现没有关系。

img

同一个日志系统的绑定器和桥接器不能同时引入,会导致循环路由

Slf4j绑定和桥接的更多内容可以见官网描述:http://www.slf4j.org/legacy.html

非springboot下的使用方法

添加slf4j-api-X.jar,添加后就可以使用slf4j的方法了。但这只是个空壳,还没有决定底层使用哪种日志框架来真正写日志,因此虽然不报错,但也不会输出日志,还会产生一个“未绑定日志框架”的警告。通过添加1种(仅1种)绑定包到类路径来决定真正使用的日志框架。

  • 若使用log4j作为日志框架,则只需要添加slf4j-log4j12-Y.jar到类路径中然后重新运行项目即可,slf4j会自动检测。
  • 若使用logback作为日志框架,则只需要添加logback-classic-Y.jar到类路径中然后重新运行项目即可,slf4j会自动检测。

log4j下使用maven引入

1)仅添加slf4j-log4j12,它会自动把log4j-X.jarslf4j-api-Y.jar拉下来。前者提供了log4j实现,后者提供slf4j接口。

<dependency> 
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>1.8.0-beta2</version>
</dependency>

2)添加log4j-X.jarslf4j-api-Y.jar,但是这样要自己匹配好版本,不建议。

springboot下的使用方法

  • 若使用logback作为日志框架,无需添加任何包,spring-boot-starter包引入了spring-boot-starter-logging包,此包又引入了logback-classic(绑定器)和其他桥接器
  • 若使用log4j作为日志框架,则只需要屏蔽掉spring-boot-starter-logging包,并引入spring-boot-starter-log4j包,此包会引入slf4j-log4j12(绑定器)和其他桥接器

img

  • 若使用log4j2作为日志框架,则只需要屏蔽掉spring-boot-starter-logging包,并引入spring-boot-starter-log4j2包,此包会引入log4j-slf4j-impl(绑定器)和桥接器

img

jar包冲突问题(多个绑定器)

当引入的jar包内置了不同的Slf4j绑定器,会出现冲突问题。很多jar包内置了日志框架,spring-boot-starter包引入了spring-boot-starter-logging包,此包又引入了logback-classic(绑定器)。

img

zookeeper包引入slf4j-log4j12(log4j绑定slf4j)包

img

若项目同时引入spring-boot-starter-logging包和zookeeper包,会引发slf4j绑定冲突

使用来去掉zookeeper中冲突的jar包

	   <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>

静态绑定过程

Slf4j旧版本(1.8.0前)使用StaticLoggerBinder强依赖进行静态绑定,新版本(1.8.0后)则采用SPI(Service Provider Interface)机制。

spring-boot-starter-logging和spring-boot-starter-log4j2目前使用的slf4j-api为1.7.31为旧版本,我们先探讨旧版本。

我们通过调用LoggerFactory.getLogger来获取打印器,slf4j对LoggerFactroy的说明如下:

LoggerFactory是为各个日志API生成Logger的帮助类。如log4j,logback,jdk 1.4 logging,也支持NOPLogger,SimpleLogger。

LoggerFactory内部封装了ILoggerFactory接口,ILoggerFactory会被日志系统实现,具体的ILoggerFactory与LoggerFactory在编译期(complile time)绑定。由ILoggerFactory来返回真正的Logger

org.slf4j.spi.LoggerFactoryBinder在slf4j-api包下,其实现类org.slf4j.impl.StaticLoggerBinder则由各个日志框架绑定器实现。

img

img

调用org.slf4j.LoggerFactory.getLogger的全过程如下:

StaticLoggerBinder编译后就确定了。 当所有类被类加载器加载进jvm内存空间后,classLoader+全限定名可以唯一的标识一个类。也就是说Slf4j会在当前classpath下找org/slf4j/impl/StaticLoggerBinder.class

所以实际上是靠StaticLoggerBinder来获取具体日志框架的ILoggerFactory

package org.slf4j;
import org.slf4j.impl.StaticLoggerBinder;


public final class LoggerFactory {
  
  static volatile int INITIALIZATION_STATE = 0;
  private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";

  public static Logger getLogger(Class clazz) {
    return getLogger(clazz.getName());
  }

  public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
  }
  
  /
   * ILoggerFactory instance is bound with this class at compile time.
   * 
   * @return the ILoggerFactory instance in use
   */
  public static ILoggerFactory getILoggerFactory() {
        if (INITIALIZATION_STATE == 0) {
            Class var0 = LoggerFactory.class;
            synchronized(LoggerFactory.class) {
                if (INITIALIZATION_STATE == 0) {
                    INITIALIZATION_STATE = 1;
                  	//初始化绑定对象
                    performInitialization();
                }
            }
        }

        switch(INITIALIZATION_STATE) {
        case 1:
            return SUBST_FACTORY;
        case 2:
            throw new IllegalStateException("org.slf4j.LoggerFactory in failed state. Original exception was thrown EARLIER. See also http://www.slf4j.org/codes.html#unsuccessfulInit");
        case 3:
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        case 4:
            return NOP_FALLBACK_FACTORY;
        default:
            throw new IllegalStateException("Unreachable code");
        }
    }
}

private final static void performInitialization() {
  //与具体的日志框架绑定
  bind();
  ....
}

private static final void bind() {
        String msg;
        try {
            Set<URL> staticLoggerBinderPathSet = null;
            if (!isAndroid()) {
                staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
                reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
            }
						// the next line does the binding
            StaticLoggerBinder.getSingleton();//绑定
            INITIALIZATION_STATE = 3;
          
            reportActualBinding(staticLoggerBinderPathSet);
            fixSubstituteLoggers();
          
            replayEvents();
            SUBST_FACTORY.clear();
        } 
  			....
    }

如果 classpath下存在多个绑定器slf4j会报告,findPossibleStaticLoggerBinderPathSet会将含有全限定名org/slf4j/impl/StaticLoggerBinder.class的包的路径添加到链表中,当发现了多个包后reportMultipleBindingAmbiguity会报告

static Set<URL> findPossibleStaticLoggerBinderPathSet() {
        LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet();

        try {
            ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
            Enumeration paths;
            if (loggerFactoryClassLoader == null) {
                paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
            } else {
                paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
            }

            while(paths.hasMoreElements()) {
                URL path = (URL)paths.nextElement();
                staticLoggerBinderPathSet.add(path);
            }
        } catch (IOException var4) {
            Util.report("Error getting resources from path", var4);
        }

        return staticLoggerBinderPathSet;
    }