优雅的使用slf4j

作者: 517001e7cb6e | 来源:发表于2018-08-23 10:44 被阅读523次

前言

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件事:

  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更优雅!

相关文章

  • 优雅的使用slf4j

    前言 Java各种日志框架中,大的趋势是在从经典的common-logging & log4j逐渐过渡到现代的sl...

  • 日志框架slf4j

    一、使用slf4j 使用slf4j时需要写private final Logger logger = Logge...

  • SpringBoot日志

    springboot使用SLF4J+logback的组合 使用SLF4J各种组合需要导入的jar SLF4J是一个...

  • SLF4J: Failed to load class "org

    使用slf4j报错 slf4j即简单日志门面(Simple Logging Facade for Java),不是...

  • Log4j2设置与使用(基于Spring4的Maven项目)

    Maven配置 属性设置: 依赖设置: 如果同时使用了slf4j包(例如使用quartz包就会自带一个slf4j包...

  • 探究SLF4J日志框架的原理和使用

    介绍 最近发现项目中使用log4j和SLF4J,打算研究下SLF4J。SLF4J用作各种日志框架的简单外观或抽象,...

  • slf4j加载过程(基于logback实现)

    主流的Web框架spring boot默认使用slf4j + logback,slf4j 为日志处理提供了统一的接...

  • SLF4J

    为什么要使用SLF4J而不是Log4J SLF4J不同于其他日志类库,与其它有很大的不同。SLF4J(Simple...

  • feign日志处理

    使用了slf4j来输出日志。FeignConfig.java application.properties 使用:...

  • logback配置详解

    基于springboot已经自动加载了slf4j组件,只需要导入lombok依赖,就可以直接使用@Slf4j了。 ...

网友评论

    本文标题:优雅的使用slf4j

    本文链接:https://www.haomeiwen.com/subject/mrbuiftx.html