浅谈Java日志框架slf4j作用及其实现原理

 更新时间:2018年03月23日 11:02:53   作者:五月的仓颉  
日志记录是应用程序运行中必不可少的一部分。这篇文章主要介绍了浅谈Java日志框架slf4j作用及其实现原理,SLF4J是一个日志框架抽象层,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

SLF4J是一个日志框架抽象层,底下绑定具体的日志框架,比如说Log4J,Logback,Java Logging API等。SLF4J也有自身的默认实现,但是我们还是主要以日志框架抽象层的身份使用SLF4J。

要使用SLF4J,得包含对"org.slf4j:slf4j-api"的依赖。

简单回顾门面模式

slf4j是门面模式的典型应用,因此在讲slf4j前,我们先简单回顾一下门面模式,

门面模式,其核心为外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用。用一张图来表示门面模式的结构为:

门面模式的核心为Facade即门面对象,门面对象核心为几个点:

  1. 知道所有子角色的功能和责任
  2. 将客户端发来的请求委派到子系统中,没有实际业务逻辑
  3. 不参与子系统内业务逻辑的实现

大致上来看,对门面模式的回顾到这里就可以了,开始接下来对SLF4J的学习。

我们为什么要使用slf4j

我们为什么要使用slf4j,举个例子:

我们自己的系统中使用了logback这个日志系统

我们的系统使用了A.jar,A.jar中使用的日志系统为log4j

我们的系统又使用了B.jar,B.jar中使用的日志系统为slf4j-simple

这样,我们的系统就不得不同时支持并维护logback、log4j、slf4j-simple三种日志框架,非常不便。

解决这个问题的方式就是引入一个适配层,由适配层决定使用哪一种日志系统,而调用端只需要做的事情就是打印日志而不需要关心如何打印日志,slf4j或者commons-logging就是这种适配层,slf4j是本文研究的对象。

从上面的描述,我们必须清楚地知道一点:slf4j只是一个日志标准,并不是日志系统的具体实现。理解这句话非常重要,slf4j只提做两件事情:

  1. 提供日志接口
  2. 提供获取具体日志对象的方法

slf4j-simple、logback都是slf4j的具体实现,log4j并不直接实现slf4j,但是有专门的一层桥接slf4j-log4j12来实现slf4j。

为了更理解slf4j,我们先看例子,再读源码,相信读者朋友会对slf4j有更深刻的认识。

slf4j应用举例

上面讲了,slf4j的直接/间接实现有slf4j-simple、logback、slf4j-log4j12,我们先定义一个pom.xml,引入相关jar包:

<!-- 原文:五月的仓颉http://www.cnblogs.com/xrq730/p/8619156.html -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>org.xrq.log</groupId>
   <artifactId>log-test</artifactId>
   <version>1.0.0</version>
   <packaging>jar</packaging>

   <name>log-test</name>
   <url>http://maven.apache.org</url>

   <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   </properties>

   <dependencies>
    <dependency>
      <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <version>4.11</version>
       <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.25</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.2.3</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.25</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.17</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.21</version>
    </dependency>
   </dependencies>
</project>

写一段简单的Java代码:

@Test
public void testSlf4j() {
  Logger logger = LoggerFactory.getLogger(Object.class);
  logger.error("123");
 }

接着我们首先把上面pom.xml的第30行~第49行注释掉,即不引入任何slf4j的实现类,运行Test方法,我们看一下控制台的输出为:

看到没有任何日志的输出,这验证了我们的观点:slf4j不提供日志的具体实现,只有slf4j是无法打印日志的。

接着打开logback-classic的注释,运行Test方法,我们看一下控制台的输出为:

看到我们只要引入了一个slf4j的具体实现类,即可使用该日志框架输出日志。

最后做一个测验,我们把所有日志打开,引入logback-classic、slf4j-simple、log4j,运行Test方法,控制台输出为:

和上面的差别是,可以输出日志,但是会输出一些告警日志,提示我们同时引入了多个slf4j的实现,然后选择其中的一个作为我们使用的日志系统。

从例子我们可以得出一个重要的结论,即slf4j的作用:只要所有代码都使用门面对象slf4j,我们就不需要关心其具体实现,最终所有地方使用一种具体实现即可,更换、维护都非常方便。

slf4j实现原理

上面看了slf4j的示例,下面研究一下slf4j的实现,我们只关注重点代码。

slf4j的用法就是常年不变的一句"Logger logger = LoggerFactory.getLogger(Object.class);",可见这里就是通过LoggerFactory去拿slf4j提供的一个Logger接口的具体实现而已,LoggerFactory的getLogger的方法实现为:

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;
}

从第2行开始跟代码,一直跟到LoggerFactory的bind()方法:

private final static void bind() {
  try {
    Set<URL> staticLoggerBinderPathSet = null;
    // skip check under android, see also
    // http://jira.qos.ch/browse/SLF4J-328
    if (!isAndroid()) {
      staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
      reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
    }
    // the next line does the binding
    StaticLoggerBinder.getSingleton();
    INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
    reportActualBinding(staticLoggerBinderPathSet);
    fixSubstituteLoggers();
    replayEvents();
    // release all resources in SUBST_FACTORY
    SUBST_FACTORY.clear();
  } catch (NoClassDefFoundError ncde) {
    String msg = ncde.getMessage();
    if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
      INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
      Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
      Util.report("Defaulting to no-operation (NOP) logger implementation");
      Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
    } else {
      failedBinding(ncde);
      throw ncde;
    }
  } catch (java.lang.NoSuchMethodError nsme) {
    String msg = nsme.getMessage();
    if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
      INITIALIZATION_STATE = FAILED_INITIALIZATION;
      Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
      Util.report("Your binding is version 1.5.5 or earlier.");
      Util.report("Upgrade your binding to version 1.6.x.");
    }
    throw nsme;
  } catch (Exception e) {
    failedBinding(e);
    throw new IllegalStateException("Unexpected initialization failure", e);
  }
}

这个地方第7行是一个关键,看一下代码:

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) {
      paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
    } else {
      paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
    }
    while (paths.hasMoreElements()) {
      URL path = paths.nextElement();
      staticLoggerBinderPathSet.add(path);
    }
  } catch (IOException ioe) {
    Util.report("Error getting resources from path", ioe);
  }
  return staticLoggerBinderPathSet;
}

这个地方重点其实就是第12行的代码,getLogger的时候会去classpath下找STATIC_LOGGER_BINDER_PATH,STATIC_LOGGER_BINDER_PATH值为"org/slf4j/impl/StaticLoggerBinder.class",即所有slf4j的实现,在提供的jar包路径下,一定是有"org/slf4j/impl/StaticLoggerBinder.class"存在的,我们可以看一下:

我们不能避免在系统中同时引入多个slf4j的实现,所以接收的地方是一个Set。大家应该注意到,上部分在演示同时引入logback、slf4j-simple、log4j的时候会有警告:

这就是因为有三个"org/slf4j/impl/StaticLoggerBinder.class"存在的原因,此时reportMultipleBindingAmbiguity方法控制台输出语句:

private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
  if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
    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.");
  }
}

那网友朋友可能会问,同时存在三个"org/slf4j/impl/StaticLoggerBinder.class"怎么办?首先确定的是这不会导致启动报错,其次在这种情况下编译期间,编译器会选择其中一个StaticLoggerBinder.class进行绑定。

最后StaticLoggerBinder就比较简单了,不同的StaticLoggerBinder其getLoggerFactory实现不同,拿到ILoggerFactory之后调用一下getLogger即拿到了具体的Logger,可以使用Logger进行日志输出。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

相关文章

  • JavaWeb 使用Session实现一次性验证码功能

    JavaWeb 使用Session实现一次性验证码功能

    这篇文章主要介绍了JavaWeb 使用Session实现一次性验证码功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • java整合微信支付功能详细示例

    java整合微信支付功能详细示例

    这篇文章主要给大家介绍了关于java整合微信支付功能的相关资料,支付是一个复杂且测试起来需要的配置特别复杂的模块,文中给出了详细的示例代码,需要的朋友可以参考下
    2023-07-07
  • 浅谈使用setBounds()方法需要注意的地方

    浅谈使用setBounds()方法需要注意的地方

    下面小编就为大家带来一篇浅谈使用setBounds()方法需要注意的地方。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • Java程序执行的全流程

    Java程序执行的全流程

    这篇文章主要介绍了Java程序执行的全流程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • springboot项目部署到k8s上的方法步骤

    springboot项目部署到k8s上的方法步骤

    本文主要介绍了springboot项目部署到k8s上的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • MyBatis中criteria的or(或查询)语法说明

    MyBatis中criteria的or(或查询)语法说明

    这篇文章主要介绍了MyBatis中criteria的or(或查询)语法说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • springBoot详解集成Swagger流程

    springBoot详解集成Swagger流程

    Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化 Restful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法、参数和模型紧密集成到服务器端的代码,允许API来始终保持同步
    2022-06-06
  • SpringBoot整合七牛云上传图片的示例代码

    SpringBoot整合七牛云上传图片的示例代码

    本文就来介绍了SpringBoot整合七牛云上传图片的示例代码,用户在前端上传图片后,交由后端处理,上传至七牛云,感兴趣的可以了解一下
    2023-10-10
  • SpringBoot工程搭建打包、启动jar包和war包的教程图文详解

    SpringBoot工程搭建打包、启动jar包和war包的教程图文详解

    这篇文章主要介绍了SpringBoot工程搭建打包、启动jar包和war包的教程,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • 详解Spring Boot Profiles 配置和使用

    详解Spring Boot Profiles 配置和使用

    本篇文章主要介绍了详解Spring Boot Profiles 配置和使用,具有一定的参考价值,有兴趣的可以了解一下
    2017-06-06

最新评论