java日志框架的前世今生

一、聊聊java中混乱的日志体系

‍♀️波妞:先提个问题,你知道哪些日志框架?
‍♀️波妞:呃。。。我说说搜我听过的吧,比如log4j、jul、jcl、slf4j、log4j2、simple log、logback,已经有7个了,但是肯定不止7个。
‍♂️宗介:卧槽,那在项目中我到底该用哪个,看得我脑壳痛,它们有啥子关系没得嘛?
‍♀️波妞:别着急,下面让我慢慢说。

二、先打印几个日志看看

1. 使用log4j输出一下日志
  • 先导入log4j的依赖
    
      
        log4j
        log4j
        1.2.17
      
    
    
  • 然后在classpath下加上log4j的配置文件log4j.properties,日志级别设置的是info
    log4j.rootLogger=info,stdout
    # 输出到控制台
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.target=System.out
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %p [%t] %C.%M(%L) | %m%n
    
  • 主程序
    import org.apache.log4j.Logger;
    
    public class Log4j {
        public static void main(String[] args) {
            Logger logger = Logger.getLogger("lof4j");
            logger.info("log4j");
        }
    }
    
  • 运行程序,看到控制台输出了日志


    log4j控制台输出效果,嗯,是白色
2. 使用jul(java.util.logging)输出一下日志
  • 因为juljdk自带的日志包,所以不需要添加依赖,直接干,主程序如下,注意导包导的是java.util.logging.Logger
    import java.util.logging.Logger;
    
    public class Jul {
        public static void main(String[] args) {
            Logger logger = Logger.getLogger("jul");
            logger.info("jul");
        }
    }
    
  • 运行程序,查看控制台


    jul控制台输出效果,发现它时红色滴
3.使用jcl输出一下日志
  • 先导入jcl的依赖
    
      
          commons-logging
          commons-logging
          1.2
      
    
    
  • 上主程序,这次不是直接通过new得到一个logger了,而是使用工厂去拿一个logger,这也是有原因的哈
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    
    public class Jcl {
        public static void main(String[] args) {
            Log log = LogFactory.getLog("jcl");
            log.info("jcl");
        }
    }
    
  • 运行看控制台


    咦,红色?怎么和jul的效果那么像

    deug发现的确是jul logger
  • log4j的依赖加上试试,加上后的依赖变成了下面的样子

    
      
          commons-logging
          commons-logging
          1.2
      
    
      
          log4j
          log4j
          1.2.17
      
      
    
  • 再次运行,看看控制台是什么样子


    ???白色?怎么又变的和log4j的效果一样了,小朋友有很多问号啊

    打个断点进一步验证,发现的确是log4j logger
  • 聪明的你可能已经猜到了,jcl根本不是一个日志实现(或者说日志产品),而是一个日志抽象层,主程序里面那个LogFactory.getLog("jcl")拿到是才一个具体的日志产品的对象。所以我们猜测jcl的结构是下面这样的

  • 为了进一步证实我们的猜想,我们看看源码,在LogFactoryImpl这个类中发现一段关键代码

    • 定义了一个成员变量classesToDiscover
      private static final String[] classesToDiscover = {
              LOGGING_IMPL_LOG4J_LOGGER,  // org.apache.commons.logging.impl.Log4JLogger
              "org.apache.commons.logging.impl.Jdk14Logger",
              "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
              "org.apache.commons.logging.impl.SimpleLog"
      };
      
    • 代码片段
      for(int i=0; i
    • 所以玄机就在这里,LogFactoryImpl中先定义了一个成员变量classesToDiscover,它是一个String数组,里面包含了log4jjullogger的全限定名。然后会在代码里面遍历这个数组,每次拿到一个全限定名并调用createLogFromClass尝试去实例化对象,如果没实例化成功则会返回null,继续使用下一个全类名去实例化对象。可以看到数组的第二个元素是Jdk14Logger,它封装的是jul里面的logger,而juljdk自带的库,所以99.99%的情况下,如果你没导入log4jjar包,最终返回的loggerjullogger。如果导入了log4jjar包,那么在遍历数组第一个元素后就会得到logger对象了,此时得到的就是log4jlogger对象。这就是前面的例子中为什么没加log4j依赖时,使用的jullogger,加了log4j依赖后使用的是log4jlogger原因。最后需要注意juljava.util.logging包的简称,是jdk4正式发布的日志库,可以说是老妖怪。而Jdk13LumberjackLogger应该是对jdk1.3及以前版本的东西的封装(没有深究过,),可以说是个超级老妖怪。你肯定不会去用jdk3去做开发,所以不用在意Jdk13LumberjackLogger。至于SimpleLog,这个是jcl的默认实现,简单但是功能不够强大,很少用,所以也不用在意啦。
4. 小总结

通过上面的demo,我们发现jcl是一个日志抽象层。我们面向抽象编程,可以让我们的程序可扩展,可以无缝切换日志产品,比如你今天用的log4j,明天想用jul了,那么直接把项目中的log4j依赖排除掉即可。但是有个问题,看了源码发现jcl只支持4个日志产品,还有两个是没用的,也就说jcl只支持log4jjul。现在市面上这么多日志产品,还有什么logback呀、nop呀什么的。我如果要用loback,想面向抽象编程jcl也不支持呀。的确是的,因为jcl已经很久没有更新了,没有对新的日志框架做适配。但是好消息是有个后起之秀来代替它,它就是slf4j

三、下面我们开始来说说java中日志系统的前世今生吧

起源:System.out和System.err

这应该是最早的记录日志的方式,不灵活、不能根据实际需要进行一些选项配置

Ⅰ、1996年,Log4j横空出世

1996年初,E.U.SEMPER(欧洲安全电子市场)项目决定编写自己的跟踪API,最后该API演变为Log4jLog4j日志软件包一经推出就备受欢迎,当然这里必须要提到一个人,他是Log4j的主要贡献者,这个大佬叫Ceki Gülcü,可能应该叫巨佬。。。后面你就明白了。后来Log4j成为了Apache基金会项目中的一员,同时Log4j的火爆,让Log4j一度成为业内日志标杆。(据说‍♂️Apache基金会还曾经建议️‍♀️Sun引入Log4jjava的标准库中,但是️‍♀️sun拒绝了,️‍♀️sun可能是有自己的私心)。

Ⅱ、2002年2月,JUL诞生

果然️‍♀️sun是有私心的(我自己的亲儿子怎么能用你撸出来的东西),于是在20022Java1.4发布,️‍♀️sun正式推出了自己的日志库Java Util Logging,其实很多日志的实现思想也都是仿照Log4j,毕竟Log4j先出来很多年了,已经很成熟了此时,这两个日志工具打架,显然Log4j是更胜一筹。它们打架其实就是互相竞争,️‍♀️sun心里可能在想,不就是做个日志工具嘛,谁不会!当然好景不长。

Ⅲ、2002年8月,JCL(Jakarta Commons Logging)出生,想一统江湖

终于,‍♂️Apache发言了, 玩编程,谁玩的过我!你不让我成为jdk标准,我就把自己变成日志的标准,哼!!!!于是JUL刚出来不久,20028月‍♂️Apache又推出了日志接口Jakarta Commons Logging,也就是日志抽象层,当然也提供了一个默认实现Simple Log,这野心很大,想一统日志抽象(就像以前的JDBC一统数据库访问层),让日志产品去实现它的抽象,这样只要你的日志代码依赖的是JCL的接口,你就可以很方便的在实现了jcl接口的日志产品之间做切换,当时日志领域大概是这样的结构,当然也还是很容易理解的,也很优雅。

但是好景不长,在使用过程中,虽然现在日志系统在JCL的统一下很优雅,很美好,但大家发现了JCL还不够好,有些人甚至认为JCL造成的问题比解决的问题还多...【抱怨地址】

Ⅳ、2005年,slf4j(Simple Logging Facade for Java)出世,最后证明,后来者居上

大佬Ceki Gülcü(也就是Log4j的作者)由于一些原因离开了‍♂️Apache,之后觉得jcl不好,于是于2005年自己撸出一个新工程,也就是一套新日志接口(也叫java简单日志门面):slf4jSimple Logging Facade for Java),感觉粗来了么。。。这战争的硝烟 ,明显这个Slf4j是剑指jcl啊,但是后面确实也证明了slf4j是要比jcl的战斗力更强。

Ceki Gülcü心想,和我玩接口,我一个人就是一支军队!玩死你们。

slf4j绑定器:绑定器是什么?

由于slf4j出生的较晚,而且还只是一个日志接口,所以之前已经出现的日志产品,如JCLLog4j都是没有实现这个接口的,所以尴尬的是光有一个接口,没有实现的产品也是很憋屈啊,就算开发者想用Slf4j也是用不了,这时候,巨佬Ceki Gülcü发话了。

Ceki Gülcü:咳咳....,别急,我早帮你们想好了,要让️‍♀️ sun或者‍♂️Apache这两个家伙来实现我的接口,太南啦,老铁,但。。。我帮你们实现,不就完了么。。。

于是巨佬Ceki Gülcü撸了个绑定器(巨佬Ceki Gülcü的slf4j官网说的是slf4j binding,就叫它绑定器吧),他是这样说的,只需要在你的类路径下替换不同的绑定器,就可以实现日志框架的切换。比如要将jul替换成log4j,只需要用slf4j-log4j-xxx.jar替换slf4j-jdk14-xxx.jar(xxx表示版本号)。下面三张图,仔细品味下哦,特别是第二张。

官网截图
官网给的图,仔细看看哦,不要看到英文就溜,我英文也很渣,但是也用有道神器把它看懂了的
对上图做个小总结,slf4j的基本结构就是酱紫
slf4j桥接器:桥接器是什么?
巨佬Ceki Gülcü这样介绍桥接器的

巨佬Ceki Gülcü的意思就是,你的组件可能依赖slf4j以外的日志组件,并且这些日志组件不能改变成slf4j,这样就会产生日志冲突,在你的应用中就会出现两种及以上的日志,造成日志混乱。对于这种场景,巨佬Ceki Gülcü提供了slf4j桥这个东西,让其他框架的日志可以重定向到slf4j,这样就完美解决了日志冲突问题。

下面举一个栗子,先喝杯水:
假设你的应用中使用的日志框架是jul,而spring使用的jcljcl又使用的log4j,如下图。

日志框架冲突场景

现在有两种解决方式:

  • 一是使用log4j,因为你可以改自己的代码,所以可以直接更换slf4j绑定器,将应用使用的日志框架切换到log4j

    将绑定器从 slf4j-jdk14 换成 slf4j-log4j 就ok了

  • 二是如果不想妥协,我就想使用juc,这个时候,你又改不了人家spring的代码,让spring不使用log4j,怎么办,桥接器登场

    jcl-over-slf4j桥接器中重写了log4j,也就是说jcl-over-slf4j有一个山寨版的log4j,这个log4j会重定向到slf4j,最终使用juc。

官网给出的slf4j桥接包的结构图,仔细品味哦
看了上面的介绍后是不是觉得,有木有jio得巨佬Ceki Gülcü真的是个巨佬了,他一个人干赢了️‍♀️ sun和‍♂️Apache,让他们做自己的小弟。

Ceki Gülcü心想:挖鼻屎,和我斗,我一个人就能爆发出你们两个加起来乘上99999的力量。

Logback(2006年)

巨佬Ceki Gülcü觉得,使用slf4j,要么需要加上绑定器,要么需要家还是那个桥接包,因为那些日志产品都不是slf4j的亲生儿子,所以他觉得应该让slf4j有个亲儿子,于是在2006年logback诞生了,logback完美实现了slf4j。由于log4j也是自己写的,他专门写了篇文章建议在你的应用中首先考虑使用logback而不是log4j,并指出logback相比log4j更好的原因--->文章地址,具体优点体现在性能更好、更安全可靠、更节约内存等等,具体可以去看文章,这里不多费口舌了。

先对上面提到的东西做个小总结,这里面只画出了常用的日志产品,也就是log4j、jul和logback
小总结
Ⅴ、2012年,战火还未结束,天降log4j2

在2012年,‍♂️Apache直接推出新项目,不是对log4j1.x升级,而是新项目log4j2,因为log4j2是完全不兼容log4j1.x

并且很微妙的,log4j2几乎涵盖logback所有的特性(这不是对着干是啥~而且还有抄袭的嫌疑。。。哈哈哈),更甚者的Log4j2也搞了分离的设计,分化成log4j-apilog4j-core,这个log4j-api也是日志接口,log4j-core才是日志产品。。。

emmm,我看到这,我都有点崩溃了

现在我们可有了3个日志接口(jclslf4jlog4j2),以及4个日志产品(log4jjulsimple-loglogback)。。。当然‍♂️Apache也知道该做啥,为了让大家可以接入自己的Log4j2,那不就是绑定器嘛,或者要不就是桥接器(在官网只看到了桥接器,没有看到绑定器,我猜应该是‍♂️Apache太懒了),‍♂️Apache也麻溜的推出了它的桥接包。

。。。最后的图大概是这样。嗯,画完这张图我掉了99999根头发。

Ⅵ、现在是2020年,各个公司使用不同的日志框架,没具体统计过,我觉得是slf4j占主导地位

四、前上面说了那么多,现在说一点注意事项和总结

先说注意事项:
  • 如果系统系统采用slf4j,那么需要导入slf4j-api和对应的绑定器。需要说的是这个绑定器,比如导入了slf4j-log4j12的依赖,slf4j-log4j12会自动导入log4j的依赖。所以需要排除掉应用自己导入的log4j依赖
  • 如果使用需要将jul或者jcl桥接到log4j2,除了需要导入log4j-apilo4j-core,还需要导入log4j-jullog-jcl桥接包;而如果想将log4j桥接到log4j2,则只需要导入lo4j-corelog4j-1.2-api即可。
  • 如果要将jul桥接到log4j2,需要设置系统属性,属性名为java.util.logging.manager,属性值为org.apache.logging.log4j.jul.LogManager。可以通过两种方式设置,一是添加vm参数-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager,二是在调用LoggerLoggerManager之前执行System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
    import java.util.logging.Logger;
    
    public class Jul {
        public static void main(String[] args) {
            System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")。
            Logger logger = Logger.getLogger("jul");
            logger.info("jul");
        }
    }
    
  • 如果要将jul桥接到slf4j,也需要做些特殊处理将jul的日志路由到slf4j


    介绍了两种方法:
    第一种是编程的方式,如下代码所示
    import org.slf4j.bridge.SLF4JBridgeHandler;
    import java.util.logging.Logger;
    
    public class Jul {
        public static void main(String[] args) {
            // 移除已经存在的handler
            SLF4JBridgeHandler.removeHandlersForRootLogger();
            // 安装SLF4JBridgeHandler
            SLF4JBridgeHandler.install();
            Logger logger = Logger.getLogger("jul");
            logger.info("jul");
        }
    }
    

    *第二种方式是修改JAVA_HOME\jre\lib\logging.properties


  • 循环依赖
    比如应用中加上了jul-to-slf4j和slf4j-jdk14的依赖,jul会重定向到slf4j,slf4j又绑定的jul,所以这样就产生了循环依赖。
    下面列出一些不能会禅心循环依赖的jar依赖
    slf4j中
    jul-to-slf4j 和 jul-to-slf4j
    log4j-over-slf4j 和 slf4j-log4j12
    jcl-over-slf4j 和 slf4j-jcl
    log4j2中
    log4j-to-slf4j 和 log4j-slf4j-impl

  • 还有许多细节,比如性能呀什么的,文中没有提到,乡亲们感兴趣的话可以去看官网哦:slf4j官网、log4j2官网

总结:
  • 对于库的创造者(上帝),不写接口造成的后果就是------>看到上面画的日志的各种桥接的图你心碎吗,我反正碎了。如果一开始就定义了接口规范,我想我们的日志框架不会那么混乱。
  • 对于库的使用者,一定要面向抽象编程,这样的应用才会健壮,易于扩展。
  • 没有什么问题是加一个适配器层(比如各种桥接器、绑定器,他们都是适配器)解决不了的,解决不了,那就两个。

最后感谢各位客官‍⚖️‍‍♂️‍♀️阅读此文章,文中可能有表述错误或不准确的地方,或者各种错别字,请留言指正

你可能感兴趣的:(java日志框架的前世今生)