优雅的使用slf4j
前言
Java各种日志框架中,大的趋势是在从经典的common-logging & log4j逐渐过渡到现代的slf4j & logback。然而,项目引用各种各样的第三方库,或许使用了多种日志框架,造成项目日志系统的混乱,严重的甚至导致项目无法启动。
分析
为了避免日志系统的混乱,我们假定统一使用slf4j & logback(当然可以选其他方案)。
如果我们直接暴力的排除其他日志框架,可能导致第三方库在调用日志接口时抛出ClassNotFound异常,这里就需要用到日志系统桥接器。
日志系统桥接器说白了就是一种偷天换日的解决方案。
比如log4j-over-slf4j,即log4j -> slf4j的桥接器,这个库定义了与log4j一致的接口(包名、类名、方法签名均一致),但是接口的实现却是对slf4j日志接口的包装,即间接调用了slf4j日志接口,实现了对日志的转发。
但是,jul-to-slf4j是个意外例外,毕竟JDK自带的logging包排除不掉啊,其实是利用jdk-logging的一个Handler机制,在root logger上install一个handler,将所有日志劫持到slf4j上。
日志系统桥接器是个巧妙的解决方案,有些库的作者在引用第三方库的时候,也碰到了日志系统混乱的问题,并顺手用桥接器解决了,只不过碰巧跟你桥接的目标不一样,桥接到了log4j。想想一下:
- log4j -> slf4j,slf4j -> log4j两个桥接器同时存在会出现什么情况?
互相委托,无限循环,堆栈溢出。 - slf4j -> logback,slf4j -> log4j两个桥接器同时存在会如何?
两个桥接器都会被slf4j发现,在slf4j中定义了优先顺序,优先使用logback,仅会报警,发现多个日志框架绑定实现;
但有一些框架中封装了自己的日志facade,如果其对绑定日志实现定义的优先级顺序与slf4j不一致,优先使用log4j,那整个程序中就有两套日志系统在工作。
上面一波分析之后,我们得出结论,为达到统一使用slf4j & logback的目的,必须要做4件事:
- 引入slf4j & logback日志包;
- 排除common-logging、log4j日志包;
- 引入jdk-logging -> slf4j、common-logging -> slf4j、log4j -> slf4j桥接器;
- 排除slf4j -> jdk-logging、slf4j -> common-logging、slf4j -> log4j桥接器。
ps:log4j分1、2,应该认为是两个不同的日志框架,上面为篇幅原因省略了。
如果再严禁一点,还要排除掉slf4j-simple、slf4j-nop两个框架,不过这两个一般没人用。
下面这幅图来自slf4j官方文档,描述了桥接器的工作原理。
来自开源中国的一篇博文,也比较详细的分析了各个桥接器的工作原理,奉上传送门:https://my.oschina.net/pingpangkuangmo/blog/410224
Gradle实战
Gradle作为更现代的项目管理工具,实现上述步骤只需:
buildscript {
// 定义全局变量
ext {
slf4j_version = '1.7.25'
logback_version = '1.2.3'
}
}
// 全局排除依赖
configurations {
// 支持通过group、module排除,可以同时使用
all*.exclude group: 'commons-logging' // common-logging
all*.exclude group: 'log4j' // log4j1
all*.exclude group: 'org.apache.logging.log4j' // log4j2
all*.exclude module: 'slf4j-jdk14' // slf4j -> jdk-logging
all*.exclude module: 'slf4j-jcl' // slf4j -> common-logging
all*.exclude module: 'slf4j-log4j12' // slf4j -> log4j1、2
}
引入依赖
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 "ch.qos.logback:logback-classic:${logback_version}"
}
Gradle的依赖管理十分灵活,有篇博客介绍了其依赖管理的更多特性,传送门:
http://www.zhaiqianfeng.com/2017/03/love-of-gradle-dependencies-1.html
Maven实战
在步骤1、3依赖引入方面Maven没有什么问题,但是在步骤2、4依赖排除方面,相比Gradle,Maven就有点儿棘手了,因为Maven虽然支持依赖排除,但默认只支持逐个包排除,默认是没有全局依赖排除机制的,需要想一些额外的办法。
<project>
[...]
<properties>
<slf4j.version>1.7.25</slf4j.version>
<logback.version>1.2.3</logback.version>
</properties>
<dependencies>
<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>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback_version}</version>
</dependency>
</dependencies>
</project>
[...]
maven-enforcer-plugin插件
要扩展Maven的功能,貌似只有用插件了。maven-enforcer-plugin可以定义一些规则,其中bannedDependencies功能可以用于设置依赖黑名单,在打包过程中一旦检测到黑名单内的依赖被引入,可以发出警告甚至中断打包操作。
<project>
[...]
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M2</version>
<executions>
<execution>
<id>enforce-banned-dependencies</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>commons-logging</exclude>
<exclude>log4j</exclude>
<exclude>org.apache.logging.log4j</exclude>
<exclude>*:slf4j-jdk14</exclude>
<exclude>*:slf4j-jcl</exclude>
<exclude>*:slf4j-log4j12</exclude>
</excludes>
</bannedDependencies>
</rules>
<!-- true 匹配到黑名单中断打包,false仅发出警告 -->
<fail>true</fail>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
[...]
</project>
通过插件增强Maven功能,是Maven最标准的做法,但是,这仅仅是防御式的,一旦检测到黑名单依赖,还需要手工逐个排除。
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更优雅!