优雅的使用slf4j

前言

SLF4J: Class path contains multiple SLF4J bindings.
SLF4J: Found binding in ...

这段提示是不是很眼熟?好像每次启动项目都会报一下,但似乎又没啥影响。
但是,某天多引一个库后,项目就真的再也起不来了......

好吧,是时候正面Java中混乱的日志系统了。

JVM的是一个开放包容的平台,正因如此,才造就了今日繁荣的JVM生态,但凡事有利有弊,比如这百花齐放的日志系统,相处的似乎就不那么愉快。

提到日志系统,我们先来罗列一下几个有名的Java日志框架:

  • log4j
  • commons-logging
  • jdk-logging
  • slf4j
  • logback
  • log4j2

上面这几个是在众多日志框架中脱颖而出的,还有一些比较小众的日志框架,比如jboss-logging,gwt-log先暂时忽略。

这些日志框架可以说是在Java不同阶段的代表,比如log4j和log4j2,光从名字看就有千丝万缕的联系,明显后者是一个进化版。

话说回来,框架多那是好事儿啊,我还能比较比较,看看哪个和我口味,选一个用不就得了。但是,相比于其他库,日志框架是比较特殊的:

  1. 日志系统几乎是所有库都会用到的一个功能,每个库由于早期的技术选型和开发者喜好等原因,可能使用了不同的日志框架。我们平时需要什么库,Maven仓库搜一波,贴过来就用,那叫一个爽啊,殊不知,这样间接的引入了多少种不同的日志框架。

  2. 日志系统往往会尽可能早的进行初始化,并且由于日志桥接器和日志门面系统的存在,会尝试做一些绑定和劫持工作(后文会提到),一旦引入多个日志框架,轻则会导致程序中有好几套日志系统同时工作,日志输出混乱,重则会导致项目日志系统初始化死锁,项目无法启动。

嗯,那么咋办呢?首先简单分析一下上面几个框架,先确定最终要使用的框架。

上面几个日志框架简单分为两类:

  • 日志门面 commons-logging,slf4j
  • 日志实现 log4j,jdk-logging,logback,log4j2

这也符合Java的面向对象设计理念,将接口与实现相分离。

日志门面系统的出现其实已经很大程度上缓解了日志系统的混乱,很多库的作者也已经意识到了日志门面系统的重要性,不在库中直接使用具体的日志实现框架。
PS:其实很多库都会自己造一个类似slf4j的日志门面系统,并且绑定实现的优先级不一样。

其实说是在做选择,但事实上没得选择,slf4j作为现代的日志门面系统,已经成为事实的标准,并且为其他日志系统做了十足的兼容工作。

我们能做的就是选一个日志实现框架。
logback,log4j2是现代的高性能日志实现框架,两者都很给力,看喜好了。

分析

我们这里以统一使用slf4j & logback为例分析。

如果我们直接暴力的排除其他日志框架,可能导致第三方库在调用日志接口时抛出ClassNotFound异常,这里就需要用到日志系统桥接器

日志系统桥接器说白了就是一种偷天换日的解决方案。
比如log4j-over-slf4j,即log4j -> slf4j的桥接器,这个库定义了与log4j一致的接口(包名、类名、方法签名均一致),但是接口的实现却是对slf4j日志接口的包装,即间接调用了slf4j日志接口,实现了对日志的转发。
但是,jul-to-slf4j是个意外例外,毕竟JDK自带的logging包排除不掉啊,其实是利用jdk-logging的Handler机制,在root logger上install一个handler,将所有日志劫持到slf4j上。要使得jul-to-slf4j生效,需要执行

 SLF4JBridgeHandler.removeHandlersForRootLogger();
 SLF4JBridgeHandler.install();

spring boot中的日志初始化模块已经包括了该逻辑,故无需手动调用。在使用其他框架时,建议在入口类处的static{ }区执行,确保尽早初始化。

日志系统桥接器是个巧妙的解决方案,有些库的作者在引用第三方库的时候,也碰到了日志系统混乱的问题,并顺手用桥接器解决了,只不过碰巧跟你桥接的目标不一样,桥接到了log4j。想想一下:

  • log4j -> slf4j,slf4j -> log4j两个桥接器同时存在会出现什么情况?
    互相委托,无限循环,堆栈溢出。
  • slf4j -> logback,slf4j -> log4j两个桥接器同时存在会如何?
    两个桥接器都会被slf4j发现,在slf4j中定义了优先顺序,优先使用logback,仅会报警,发现多个日志框架绑定实现;
    但有一些框架中封装了自己的日志facade,如果其对绑定日志实现定义的优先级顺序与slf4j不一致,优先使用log4j,那整个程序中就有两套日志系统在工作。

上面一波分析之后,我们得出结论,为达到统一使用slf4j & logback的目的,必须要做4件事:

  1. 引入slf4j & logback日志包和slf4j -> logback桥接器;
  2. 排除common-logging、log4j、log4j2日志包;
  3. 引入jdk-logging -> slf4j、common-logging -> slf4j、log4j -> slf4j、log4j2 -> slf4j桥接器;
  4. 排除slf4j -> jdk-logging、slf4j -> common-logging、slf4j -> log4j、slf4j -> log4j2桥接器。

ps:log4j2桥接器由log4j2提供,其他桥接器由slf4j提供。
如果再严谨一点,还要排除掉slf4j-simple、slf4j-nop两个框架,不过这两个一般没人用。

下面这幅图来自slf4j官方文档,描述了桥接器的工作原理。

slf4j.png

来自开源中国的一篇博文,也比较详细的分析了各个桥接器的工作原理,奉上传送门:https://my.oschina.net/pingpangkuangmo/blog/410224

上述提到了这么多日志系统的桥接器,但似乎没有提到logback -> slf4j的桥接器,如果我们日志实现系统选择log4j2,怎么处理logback?

其实logback在设计上,天生绑定sfl4j,可以认为从根源上避免了直接被使用,自然也不需要logbak -> slf4j的桥接器。

Gradle实战

Gradle作为更现代的项目管理工具,实现上述步骤只需:

buildscript {
    // 定义全局变量
    ext {
        slf4j_version = '1.7.25'
        log4j2_version = '2.11.1'
        logback_version = '1.2.3'
    }
}
// 全局排除依赖
configurations {
    // 支持通过group、module排除,可以同时使用
    all*.exclude group: 'commons-logging', module: 'commons-logging' // common-logging
    all*.exclude group: 'log4j', module: 'log4j' // log4j
    all*.exclude group: 'org.apache.logging.log4j', module: 'log4j-core' // slf4j -> log4j2
    all*.exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' // log4j2
    all*.exclude group: 'org.slf4j', module: 'slf4j-jdk14' // slf4j -> jdk-logging
    all*.exclude group: 'org.slf4j', module: 'slf4j-jcl' // slf4j -> common-logging
    all*.exclude group: 'org.slf4j', module: 'slf4j-log4j12' // slf4j -> log4j
}
// 引入依赖
dependencies {
    // log
    compile "org.slf4j:slf4j-api:$slf4j_version"
    compile "org.slf4j:jul-to-slf4j:$slf4j_version"
    compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
    compile "org.slf4j:log4j-over-slf4j:$slf4j_version"
    compile "org.apache.logging.log4j:log4j-api:$log4j2_version"
    compile "org.apache.logging.log4j:log4j-to-slf4j:$log4j2_version"
    compile "ch.qos.logback:logback-classic:$logback_version"
}

如果选择log4j2作为日志实现框架

buildscript {
    // 定义全局变量
    ext {
        slf4j_version = '1.7.25'
        log4j2_version = '2.11.1'
        logback_version = '1.2.3'
    }
}
// 全局排除依赖
configurations {
    // 支持通过group、module排除,可以同时使用
    all*.exclude group: 'commons-logging', module: 'commons-logging' // common-logging
    all*.exclude group: 'log4j', module: 'log4j' // log4j
    all*.exclude group: 'ch.qos.logback', module: 'logback-core' // logback
    all*.exclude group: 'ch.qos.logback', module: 'logback-classic' // slf4j -> logback
    all*.exclude group: 'org.slf4j', module: 'slf4j-jdk14' // slf4j -> jdk-logging
    all*.exclude group: 'org.slf4j', module: 'slf4j-jcl' // slf4j -> common-logging
    all*.exclude group: 'org.slf4j', module: 'slf4j-log4j12' // slf4j -> log4j
}
// 引入依赖
dependencies {
    // log
    compile "org.slf4j:slf4j-api:$slf4j_version"
    compile "org.slf4j:jul-to-slf4j:$slf4j_version"
    compile "org.slf4j:jcl-over-slf4j:$slf4j_version"
    compile "org.slf4j:log4j-over-slf4j:$slf4j_version"
    compile "org.apache.logging.log4j:log4j-core:$log4j2_version"
    compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j2_version"
}

Gradle的依赖管理十分灵活,有篇博客介绍了其依赖管理的更多特性,传送门:
http://www.zhaiqianfeng.com/2017/03/love-of-gradle-dependencies-1.html

Maven实战

在步骤1、3依赖引入方面Maven没有什么问题,但是在步骤2、4依赖排除方面,相比Gradle,Maven没有直接提供全局依赖排除机制,我们需要借助一些方法间接达到目的。

Provided Scope

<project>
  [...]
  <properties>
    <slf4j.version>1.7.25</slf4j.version>
    <commons-logging.version>1.2</commons-logging.version>
    <log4j.version>1.2.17</log4j.version>
    <log4j2.version>2.11.1</log4j2.version>
    <logback.version>1.2.3</logback.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>${commons-logging.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>${log4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>${log4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
      <version>${log4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-jdk14</artifactId>
      <version>${slf4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-jcl</artifactId>
      <version>${slf4j.version}</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>${slf4j.version}</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jul-to-slf4j</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>log4j-over-slf4j</artifactId>
      <version>${slf4j.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>${log4j2.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-to-slf4j</artifactId>
      <version>${log4j2.version}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${logback_version}</version>
    </dependency>
  </dependencies>
  [...]
</project>

version99仓库

我们来分析一下Maven依赖的工作原理,在一个依赖库被直接或间接引入多次时,并且版本不一致,maven在解析依赖的时候,有两个仲裁原则:

  • 路径最短优先原则
  • 优先声明原则

首先遵循路径最短优先原则,即直接引入最优先,传递依赖层级越浅,越优先。若依然无法仲裁,则遵循优先声明原则,在pom中声明靠前的优先。

既然了解了这个规则,那就可以巧妙的利用一下,如果我们在pom的最开始,引入了一个虚包,则该包其他的依赖全部失效,也就达到了全局排除依赖的目的。

slf4j的文档中也提到了该方案,并且提供了一个version99仓库,里面有几个用于排除其他日志框架的虚包。

<project>
  [...]
  <repositories>
    <--! 首先添加version99仓库 -->
    <repository>
      <id>version99</id>
      <url>http://version99.qos.ch/</url>
    </repository>
  </repositories>
  <--! 直接引入依赖,放置在最前 -->
  <dependencies>
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
      <version>99-empty</version>
    </dependency>
    <dependency>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging-api</artifactId>
      <version>99-empty</version>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>99-empty</version>
    </dependency>
  </dependencies>

  <--! 通过dependencyManagement强制指定依赖版本也可达到同样效果 -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>99-empty</version>
      </dependency>
      <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging-api</artifactId>
        <version>99-empty</version>
      </dependency>
      <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>99-empty</version>
      </dependency>
    </dependencies>
  </dependencyManagement>
  [...]
</project>

这个version99仓库是slf4j提供的一个静态Maven仓库,里面只有这3个虚包,是不能满足其他要求的,我们可以照葫芦画瓢,制作其他虚包上传到Nexus。
当然,发挥一下脑洞,可以分析一下Maven下载依赖的机制,编程实现一个动态的Maven仓库,请求任何empty版本的依赖包都返回一个虚包。

这里奉上一个传送门:
https://github.com/erikvanoosten/version99

嗯,还是Gradle更优雅!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容