Java搬砖这么久,日志还是出问题?

开发 后端
用了那么久的日志框架,你是否仍会遇到即使配置了日志,日志依然不知去向?来接受下面的 8 连发灵魂拷问。

 前言

用了那么久的日志框架,你是否仍会遇到即使配置了日志,日志依然不知去向?

来接受下面的 8 连发灵魂拷问!

Q1:你是否遇到过配置了logback,启动时却提示log4j错误的情况?像下面这样: 

  1. log4j:WARN No appenders could be found for logger (org.example.App).  
  2. log4j:WARN Please initialize the log4j system properly.  
  3. log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info. 

Q2:你是否遇到过 SLF4J 的这种报错? 

  1. SLF4J: Class path contains multiple SLF4J bindings.  
  2. SLF4J: Found binding in [jar:file:/C:/Users/jiang/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class] 
  3. SLF4J: Found binding in [jar:file:/C:/Users/jiang/.m2/repository/org/slf4j/slf4j-log4j12/1.7.30/slf4j-log4j12-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class] 
  4. SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation. 
  5. SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder] 

 Q3:你是否遇到过DUBBO日志打印不正常的情况?

Q4:你是否遇到过Mybatis SQL日志打印不出来的情况?

Q5:你是否遇到过JPA/Hibernate SQL日志无法打印的情况?

Q6:你是否遇到过复杂项目中,很多框架内部日志无法打印的情况?

Q7:你是否遇到过Tomcat工程,日志文件打印了多份,catalina.out和其他文件?

Q8:你是否遇到过SpringBoot项目,日志文件打印了多份的问题?

What!你还遇到过其他各种日志配置问题……

## 日志框架的冲突 这些问题,基本都是由于多套日志框架共存或配置错误导致的。> 为什么会出现共存或者冲突呢?

一般是以下几种原因:

  •  项目手动引用了各种日志框架的包,比如同时引用了log4j/log4j2/logback/jboss-logging/jcl等
  •  包管理工具的传递依赖(Transitive Dependencies)导致,比如依赖了dubbo,但是dubbo依赖了zkclient,可zkclient又依赖了log4j,此时如果你的项目中还有其他日志框架存在并有使用,那么就会导致多套共存
  •  同一个日志框架多版本共存

JAVA里的各种日志框架

在正式介绍冲突和解决之前,需要先简单的说一下Java中的各种日志框架:

Java 中的日志框架分为两种,分别为日志抽象/门面,日志实现

日志抽象/门面

不负责具体的日志打印,如输出到文件、配置日志内容格式等。这只是一套日志抽象,定义了一套统一的日志打印标准,如Logger对象,Level对象。

slf4j(Simple Logging Facade for Java)和 jcl(Apache Commons Logging)这两个日志框架就是JAVA中最主流的日志抽象了。

还有一个 jboss-logging,主要用于jboss系列软件,比如hibernate之类。像 jcl 已经多年不更新了(上一次更新时间还是14年),目前最推荐的是使用 slf4j。

日志实现

Java 中的日志实现框架,主流的有以下几种:

  •  log4j,Apache(老牌日志框架,不过多年不更新了,新版本为log4j2)
  •  log4j2,Apache(log4j 的新版本,目前异步IO性能最强,配置也较简单)
  •  logback,QOS(slf4j就是这家公司的产品)
  •  jul(java.util.logging),这是jdk内置

在程序中,可以直接使用日志框架,也可以使用日志抽象+日志实现搭配的方案。不过一般都是用日志抽象+日志实现,这样更灵活,适配起来更简单。

目前最主流的方案是 slf4j+logback/log4j2,不过如果是jboss系列的产品,可能用的更多的还是 jboss-logging,毕竟自家产的,总得用上去,是吧!像 JPA/Hibernate 这种框架里,内置的就是 jboss-logging。

SpringBoot + Dubbo 日志框架冲突的例子

举个例子来说个最常见的传递依赖导致的共存冲突:比如我有一个“干净的”spring-boot项目,干净到只有一个spring-boot-starter依赖,此时我想集成dubbo,使用zookeeper作为注册中心,此时我的依赖配置是这样: 

  1. <dependencies>  
  2.   <dependency>  
  3.     <groupId>org.springframework.boot</groupId>  
  4.     <artifactId>spring-boot-starter</artifactId>  
  5.   </dependency>  
  6.   <dependency>  
  7.     <groupId>org.apache.dubbo</groupId>  
  8.     <artifactId>dubbo-spring-boot-starter</artifactId>  
  9.     <version>2.7.9</version> 
  10.    </dependency>  
  11.   <dependency>  
  12.     <groupId>org.apache.dubbo</groupId>  
  13.     <artifactId>dubbo-registry-zookeeper</artifactId>  
  14.     <version>2.7.9</version>  
  15.   </dependency> 
  16.  </dependencies> 

现在启动这个spring-boot项目,会发现一堆红色错误: 

  1. SLF4J: Class path contains multiple SLF4J bindings.  
  2. SLF4J: Found binding in [jar:file:/C:/Users/jiang/.m2/repository/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar!/org/slf4j/impl/StaticLoggerBinder.class] 
  3. SLF4J: Found binding in [jar:file:/C:/Users/jiang/.m2/repository/org/slf4j/slf4j-log4j12/1.7.30/slf4j-log4j12-1.7.30.jar!/org/slf4j/impl/StaticLoggerBinder.class] 
  4. SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation. 
  5. SLF4J: Actual binding is of type [ch.qos.logback.classic.util.ContextSelectorStaticBinder] 
  6. ----------------------------------我是分割线----------------------------------------  
  7. log4j:WARN No appenders could be found for logger (org.apache.dubbo.common.logger.LoggerFactory).  
  8. log4j:WARN Please initialize the log4j system properly.  
  9. log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info. 

从错误提示上看,错误内容分为两个部分:

  •  slf4j报错,提示找到多个slf4j的日志绑定
  •  log4j报错,提示log4j没有appender配置

出现这个错误,就是因为 dubbo 的传递依赖中含有 log4j,但是 spring-boot 的默认配置是 slf4j+logback。在依赖了 dubbo 相关包之后,现在项目中同时存在 logback/jcl(apache commons-logging)/log4j/jul-to-slf4j/slf4j-log4j/log4j-to-slf4j。

来看一下依赖图:

这个时候就乱套了,slf4j-log4j 是 log4j 的 slf4j 实现,作用是调用 slf4j api 的时候使用 log4j 输出;而 log4j-to-slf4j 的作用是将 log4j 的实现替换为 log4j,这样一来造成了死循环。

而且还有 logback 的存在,logback 默认实现了 slf4j 的抽象,而 slf4j-log4j 也是一样实现了 slf4j 的抽象。

logback 项目里共存了两套 slf4j的实现,那么在使用slf4j接口打印的时候会使用哪个实现呢?

答案是“第一个”,也就是第一个被加载的Slf4j的实现类,但这种依靠ClassLoader加载顺序来保证的日志配置顺序是非常不靠谱的。

如果想正常使用日志,让这个项目里所有的框架都正常打印日志,必须将日志框架统一。不过这里的统一并不是至强行修改,而是用“适配/中转”的方式。

现在项目里虽然有 slf4j-log4j 的配置,但这个配置是适配 log4j2 用的,而我们的依赖了只有 log4j1,实际上这个中转是无效的。但logback是有效的,而且是 spring-boot 项目的默认配置,这次就选择 logback 作为项目的统一日志框架吧!

现在项目里存在 log4j(1) 的包,而且启动时又报 log4j 的错误,说明某些代码调用了 log4j 的 api。但我们又不想用 log4j,所以需要先解决 log4j 的问题。

由于有 log4j 代码的引用,所以直接删除 log4j 一定是不可行的。slf4j 提供了一个 log4j-over-slf4j 的包,这个包复制了一份 log4j1 的接口类(Logger等),同时将实现类修改为 slf4j 了。

所以将 log4j 的(传递)依赖排除,同时引用 log4j-over-slf4j,就解决了这个 log4j 的问题。现在来修改下 pom 中的依赖(查看依赖图可以使用 maven 的命令,或者是 IDEA 自带的 Maven Dependencies Diagram,再或者 Maven Helper 之类的插件)。 

  1. <dependency>  
  2.   <groupId>org.apache.dubbo</groupId>  
  3.   <artifactId>dubbo-registry-zookeeper</artifactId>  
  4.   <version>2.7.9</version>  
  5.   <scope>compile</scope>  
  6.   <!--排除log4j-->  
  7.   <exclusions>  
  8.     <exclusion>  
  9.       <artifactId>log4j</artifactId>  
  10.       <groupId>log4j</groupId>  
  11.     </exclusion>  
  12.   </exclusions>  
  13. </dependency>  
  14. <!--增加log4j-slf4j -->  
  15. <dependency>  
  16.     <groupId>org.slf4j</groupId>  
  17.     <artifactId>log4j-over-slf4j</artifactId>  
  18.     <version>1.7.30</version>  
  19. </dependency> 

解决了log4j的问题之后,现在还有 slf4j 有两个实现的问题,这个问题处理就更简单了。由于我们计划使用 logback,那么只需要排除/删除 slf4j-log4j 这个实现的依赖即可。 

  1. <dependency>  
  2.   <groupId>org.apache.dubbo</groupId>  
  3.   <artifactId>dubbo-registry-zookeeper</artifactId>  
  4.   <version>2.7.9</version>  
  5.   <scope>compile</scope>  
  6.   <exclusions>  
  7.     <exclusion>  
  8.       <artifactId>log4j</artifactId>  
  9.       <groupId>log4j</groupId>  
  10.     </exclusion>  
  11.     <exclusion>  
  12.       <artifactId>slf4j-log4j12</artifactId>  
  13.       <groupId>org.slf4j</groupId>  
  14.     </exclusion>  
  15.   </exclusions>  
  16. </dependency> 

修改完成,再次启动就没有错误了,轻松解决问题

日志适配大全

上面只是介绍了一种转换的方式,但这么多日志框架,它们之间是可以互相转换的。

最终目的都是统一一套日志框架,让最终的日志实现只有一套

这么多的日志适配/转换方式,全记住肯定是有点难。请看下图来搭配记忆,如果再遇到冲突,需要将一个日志框架转换到另一款的时候,只需要按照图上的路径,引入相关的依赖包即可。

比如想把slf4j,适配/转换到log4j2。按照图上的路径,只需要引用log4j-slf4j-impl即可。

如果想把jcl,适配/转换到slf4j,只需要删除jcl包,然后引用jcl-over-slf4j即可。

图上的箭头,有些标了文字的,是需要额外包进行转换的,有些没有标文字的,是内置了适配的实现。其实内置实现的这种会更麻烦,因为如果遇到共存基本都需要通过配置环境变量/配置额外属性的方式来指定一款日志实现。

目前slf4j是适配方案中,最核心的那个框架,算是这个图的中心枢纽。只要围绕slf4j做适配/转化,就没有处理不了的冲突了。

总结

解决日志框架共存/冲突问题其实很简单,只要遵循几个原则:

  •  统一使用一套日志实现
  •  删除多余的无用日志依赖
  •  如果有引用必须共存的话,那么就移除原始包,使用“over”类型的包(over类型的包复制了一份原始接口,重新实现)
  •  不能over的,使用日志抽象提供的指定方式,例如jboss-logging中,可以通过org.jboss.logging.provider环境变量指定一个具体的日志框架实现

项目里统一了日志框架之后,无论用那种日志框架打印,最终还是走向我们中转/适配后的唯一一个日志框架。解决了共存/冲突之后,项目里就只剩一款日志框架。 

 

责任编辑:庞桂玉 来源: 程序员虎哥
相关推荐

2022-02-08 13:39:35

LinuxUNIX系统

2019-08-26 10:36:38

Python操作系统高考

2021-05-27 21:18:56

谷歌Fuchsia OS操作系统

2021-05-28 06:16:28

蓝牙Wi-FiNFC

2022-08-21 14:00:11

消息中间件MQ

2020-02-15 15:33:55

Python如何运作

2020-12-01 10:18:16

RabbitMQ

2020-03-30 09:22:03

AI语音技术机器视觉

2018-05-09 16:25:20

关系型数据库SQL Server

2021-04-23 14:49:31

云计算混合云

2022-01-25 12:41:31

ChromeResponse接口

2021-07-21 10:10:14

require前端代码

2021-11-08 10:00:19

require前端模块

2019-12-04 12:33:48

程序员技术设计

2018-01-31 10:24:45

热插拔原理服务器

2021-05-28 07:12:58

Mybatis面试官Java

2021-09-08 07:49:34

数据 Python技术

2021-03-22 09:27:44

PythonEXCEL热点推荐

2020-12-07 11:05:21

HttpClient代码Java

2018-06-08 10:12:10

Web缓存体系服务器
点赞
收藏

51CTO技术栈公众号