Toolaop切面编程

优雅的使用slf4j

2018-08-23  本文已影响523人  517001e7cb6e

前言

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。想想一下:

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

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

ps:log4j分1、2,应该认为是两个不同的日志框架,上面为篇幅原因省略了。
如果再严禁一点,还要排除掉slf4j-simple、slf4j-nop两个框架,不过这两个一般没人用。

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

slf4j.png
来自开源中国的一篇博文,也比较详细的分析了各个桥接器的工作原理,奉上传送门: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更优雅!

上一篇下一篇

猜你喜欢

热点阅读