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

2018 年 12 月 28 日 ImportNew

(点击上方公众号,可快速关注)


来源:五月的仓颉 ,

www.cnblogs.com/xrq730/p/8619156.html


简单回顾门面模式


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


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



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


  • 知道所有子角色的功能和责任

  • 将客户端发来的请求委派到子系统中,没有实际业务逻辑

  • 不参与子系统内业务逻辑的实现


大致上来看,对门面模式的回顾到这里就可以了,开始接下来对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只做两件事情:


  • 提供日志接口

  • 提供获取具体日志对象的方法


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代码:


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


接着我们首先把上面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进行绑定,这个地方sfl4j也在reportActualBinding方法中报告了绑定的是哪个日志框架:


private static void reportActualBinding(Set<URL> binderPathSet) {
    // binderPathSet can be null under Android
    if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
        Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
    }
}


对照上面的截图,看最后一行,确实是”Actual binding is of type…”这句。


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


【关于投稿】


如果大家有原创好文投稿,请直接给公号发送留言。


① 留言格式:
【投稿】+《 文章标题》+ 文章链接

② 示例:
【投稿】《不要自称是程序员,我十多年的 IT 职场总结》:http://blog.jobbole.com/94148/

③ 最后请附上您的个人简介哈~



看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

登录查看更多
0

相关内容

SLF4J,即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。按照官方的说法,SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志System
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【高能所】如何做好⼀份学术报告& 简单介绍LaTeX 的使用
【2020新书】Kafka实战:Kafka in Action,209页pdf
专知会员服务
67+阅读 · 2020年3月9日
《深度学习》圣经花书的数学推导、原理与Python代码实现
【新书】Java企业微服务,Enterprise Java Microservices,272页pdf
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
5大必知的图算法,附Python代码实现
AI100
4+阅读 · 2019年9月10日
浅谈 Kubernetes 在生产环境中的架构
DevOps时代
11+阅读 · 2019年5月8日
支持多标签页的Windows终端:Fluent 终端
Python程序员
7+阅读 · 2019年4月15日
6大最常用的Java机器学习库一览
AI前线
4+阅读 · 2018年10月9日
设计和实现一款轻量级的爬虫框架
架构文摘
13+阅读 · 2018年1月17日
Spark的误解-不仅Spark是内存计算,Hadoop也是内存计算
Arxiv
5+阅读 · 2018年5月1日
Arxiv
7+阅读 · 2018年1月31日
Arxiv
11+阅读 · 2018年1月15日
Arxiv
9+阅读 · 2018年1月4日
VIP会员
Top
微信扫码咨询专知VIP会员