您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

Java 日志框架冲突问题排查与总结

时间:2020-01-17 10:17:31  来源:  作者:

作者:Sharehub

来源:blog.xiaohansong.com/JAVA-log-confict-solve.html

前言

Java 有很多的日志框架可以选择,当同一个项目中出现多种日志框架时就很容易出现日志框架冲突的问题,导致日志打印不出来。本文将以一次典型的日志冲突排查问题为例,提供排查步骤和思路,最后分析日志框架冲突的原因。

一般行文思路都是先讲 Why,再讲 How,这里我颠倒了,因为一般遇到问题的时候我们对问题背后的根本原因是一无所知的,如果我们已经知道问题的原因,那么问题也就迎刃而解了。

因此我希望先复现我当时在对日志框架了解不多的情况下排查问题的思路和步骤,如何在面对未知问题找到破题思路是非常重要的技能。

一次典型的日志冲突排查

问题背景

在 A 工程中,日志框架配置选用了 Log4j2,master 分支上日志打印正常,但开发分支增加了代码之后日志打印不出来。项目的依赖中包含了 Log4j2、Logback 等日志框架。

排查思路与过程

排查问题的时候首先必须要有明确的思路,即大胆假设,小心求证,不能像无头苍蝇一样乱试。从问题的现象看,直觉上可以得出几个假设:

  • 服务器环境有问题
  • 开发分支的 Log4j2 配置有问题

接下来就是验证假设,首先多申请几台机器部署项目分支,发现问题仍然存在,可以排除第一个假设。其次找到另一个工程 B 跟 A 工程对比 Log4j2 的配置,也没有发现明细的差异,可以排除第二个假设。

在已有假设都验证失败的情况下,需要收集更多的信息作出判断,接下来就是要用对照实验收集信息。于是我分别断点了 A 和 B 两个工程,观察它们日志实体的类型是否一致。结果发现两者的日志实体类型不一样,A 的日志实现是 Logback,B 的日志实现是 Log4j2,很明显 A 打印不出日志是因为日志实体不对,但是两者都是用的同一个 LoggerFactory 创建 Logger 的。

从对照实验的结果来看,可以得出一个假设:依赖冲突导致了 A 运行时使用日志实体不是 Log4j2。

至此我们已经找到了问题的大致方向,接下来就是要排包。排包一般有两种思路:

  • 暴力求解:把所有可能冲突的日志包排掉,一个个试。
  • 精准爆破:利用类加载的信息判断运行时加载的具体是哪个 jar

暴力求解的方式太花费时间了,所以我用的第二种方式。

获取日志实体的方式如下:

privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(xxx.class);

LoggerFactory 的代码如下:

 

Java 日志框架冲突问题排查与总结

 

从代码上可以发现,getLog方法是来自父类LogFactory,当我去尝试获取LogFactory的实现时候,发现竟然有 3 个 jar 中都有同样包名的LogFactory实现。于是我断点了 A 和 B 工程的代码,用 IDEA 的运行代码功能执行以下命令获取LogFactory的加载信息。

Java 日志框架冲突问题排查与总结

 

结果发现 B 工程使用是spring-jcl,A 使用的是jcl-over-slf4j,然后排除掉 A 中jcl-over-slf4j,问题解决。

上面的排查过程中,关键的地方有两点:

  • 定位到问题的根源是类加载冲突,确定排查方向。
  • 通过断点获取冲突类的加载信息,快速定位到冲突的 jar。

为什么日志框架会冲突

问题至此就解决了,但是还有一个更深入的问题没有解决:为什么同时存在多个日志框架的时候就会出现冲突呢?在解决完问题之后,我深入研究了日志框架的历史和设计,发现原来这跟日志框架的实现机制有关系。

日志框架的历史

首先要从日志框架的发展历史开始说起。

  • 首先登场是Java Util Log,简称JUL,是JDK 中自带的 log 功能。虽然是官方自带的,JUL 的使用却不广泛,主要是因为功能比较简单,不好用。
  • 然后Log4j 1.x就登场了:它是 Gülcü 设计实现的日志框架,设计非常优秀,是非常广泛使用的框架。
  • Commons Logging:简称 JCL,是 Apache 的项目。JCL 是一个 Log Facade,只提供 Log API,不提供实现,用 Adapter 来使用 Log4j 或者 JUL 作为 Log Implementation。目的是统一日志接口规范,适配多种日志实现。
  • SLF4J/Logback:SLF4J(The Simple Logging Facade for Java) 和 Logback 也是 Gülcü 创立的项目,其创立主要是为了提供更高性能的实现。其中,SLF4j 是类似于JCL 的Log Facade,Logback 是类似于Log4j 的 Log Implementation。这老哥觉得 JCL 的接口设计不好,所以重新设计了一套。
  • Log4j2:维护 Log4j 的人为了不让 Log4j 的用户被 SLF4J/Logback 抢走,所以搞出了新的日志框架。Log4j2 和 Log4j1.x 并不兼容,设计上很大程度上模仿了 SLF4J/Logback,性能上也获得了很大的提升。Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。

至此我们已经有了三个的 Log 接口和四个 Log 实现,果然程序员真的是爱造轮子。出现这么多框架之后,有人开始搞各个框架之间的桥接,你兼容我,我兼容你,如下图所示。

Java 日志框架冲突问题排查与总结

 

因为很多 jar 使用的日志框架不同,所以经常会出现引入 jar 包之后导致日志类冲突,前面我们排查的那个问题就是因为引入了 jcl-over-slf4j 的桥接包。

动态加载日志实现

前面我们提到日志框架分为日志接口和日志实现,只要我们代码中使用的是日志接口(JCL、SLF4J),我们可以随时替换日志的实现。

SLF4J 加载日志实现的方式

SLF4J 加载日志实现分为两个步骤:

  • 获取 ILoggerFactory 日志工厂
  • 根据 ILoggerFactory 获取 Logger

SLF4J 要求日志实现 jar 包都要实现 StaticLoggerBinder 这个类,而且要放在指定目录:org/slf4j/impl/StaticLoggerBinder.class,SLF4J 的LoggerFactory会去扫描所有 jar 包中的这个地址,参考下面的代码。

 

Java 日志框架冲突问题排查与总结

 

虽然它扫描了多个日志实现,但实际上同名类 JVM 只能存在一个,它这里扫描的目的是为了打印日志告诉用户有多少个日志实现在依赖包中。下面的代码返回的是最终使用的日志实现。

Java 日志框架冲突问题排查与总结

 

你可能要问了,同时存在多个日志实现类的时候,到底是用的是哪个?答案很简单,因为 SLF4J 利用了静态类来加载日志工程,实际上就是让 JVM 决定使用哪个类:哪个被先加载到 JVM 中就用哪个。为了搞清楚这个问题的答案,我特地去看了URLClassPath加载类的实现,它就是按照 jar 加入到 URLClassPath的顺序遍历扫描,找到第一个符合条件的就返回。

JCL 加载日志实现的方式

相比 SLF4J 比较任性的加载方式(依赖 JVM 加载类的顺序),JCL 提供了更多的配置能力,可以指定使用哪一个日志工程类。

类似的,JCL 也分为两个步骤加载日志实现:

  • 获取 LogFactory 日志工厂类
  • 根据 LogFactory 获取 Logger

首先是获取 LogFactory:

  • 先从系统属性中读取系统属性System.getProperty("org.apache.commons.logging.LogFactory")
  • 使用 Java 的 SPI 机制,来搜寻对应的实现:META-INF/services/org.apache.commons.logging.LogFactory,这里就不对 SPI 进行过多介绍了,简单来说就是搜寻哪些 jar 包中含有搜寻含有上述文件,该文件中指明了对应的 LogFactory 实现
  • 从 commons-logging 的配置文件中 commons-logging.properties 寻找org.apache.commons.logging.LogFactory的值
  • 最后还没找到的话,使用默认的org.apache.commons.logging.impl.LogFactoryImpl

找到 LogFactory 之后就根据 LogFactory 获取 Logger,这个根据不同的 LogFactory 实现有不同的方式。前面我遇到那个问题就是因为类冲突导致使用了 SLJ4J 的 LogFactory ,加载了错误的 Logger。

总结

开发过程中总会遇到奇奇怪怪的问题,有无处下手的感觉时先稳住心态,按照大胆假设,小心求证的方式进行排查,实在没有思路往往是因为基础还不扎实。

像这次日志打印不出来的问题,如果了解日志框架的加载实现,很容易就能定位到问题;差一点的像我不了解日志框架的实现,但是我可以根据之前对类加载机制的了解也能解决问题;如果对类加载机制不了解,那基本上是无解了。

因此,要把问题当做学习机会,不光要解决问题,还要深挖背后的原理,做好总结,这样才能为解决更多的问题打下扎实基础。



Tags:Java   点击:()  评论:()
声明:本站部分内容来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除,谢谢。
▌相关评论
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表
▌相关推荐
本文最初发布于 hackernnon 网站,经原作者授权由 InfoQ 中文站翻译并分享。SOLID 原则是开发人员创建灵活、可理解和可维护代码的基础。但你要正确遵循这些原则就可能明显减...【详细内容】
2020-11-11   Java  点击:(1)  评论:(0)  加入收藏
从 Java 8 引入的一个很有趣的特性是 Optional 类。Optional 类主要解决的问题是臭名昭著的空指针异常(NullPointerException) —— 每个 Java 程序员都非常了解的...【详细内容】
2020-11-10   Java  点击:(3)  评论:(0)  加入收藏
背景“生产者和消费者模型” 是多线程通信的典型案例,本章节将利用前一节的锁和条件队列的知识,来实现一个完整的有界缓冲区,并创建多个线程访问该有界缓冲区,模拟生产者提供数...【详细内容】
2020-11-10   Java  点击:(2)  评论:(0)  加入收藏
前言在 java 中你不了解异步编程,crud 完全没有问题,但是有的需求你无法优雅的实现。js 也存在异步编程,当你理解了用同步的思维编写异步的代码时,相信你在编程上的造诣又更进一...【详细内容】
2020-11-09   Java  点击:(4)  评论:(0)  加入收藏
之前我们介绍了线程池的四种拒绝策略,了解了线程池参数的含义,那么今天我们来聊聊Java 中常见的几种线程池,以及在jdk7 加入的 ForkJoin 新型线程池 首先我们列出Java 中的...【详细内容】
2020-11-05   Java  点击:(5)  评论:(0)  加入收藏
HelloGitHub 推出的《讲解开源项目》 系列。这一期是由亚马逊工程师:Keerthan Vasist,为我们讲解 DJL(完全由 Java 构建的深度学习平台)系列的第 4 篇。...【详细内容】
2020-11-03   Java  点击:(4)  评论:(0)  加入收藏
有时候我们需要知道一个文件的大小。我们可以使用一些方法,比如说将文件读取成 InputStream,然后再使用 available() 获得长度就可以了。我们也可以使用 FileUtils 来获得。使...【详细内容】
2020-10-29   Java  点击:(6)  评论:(0)  加入收藏
作为一名程序员,我们要避免重复发明轮子,尽可能使用一些成熟、优秀、稳定的的第三方库,站在巨人的肩膀上搭建可靠、稳定的系统。本篇我整理了Java开发人员经常会使用到的第三...【详细内容】
2020-10-28   Java  点击:(2)  评论:(0)  加入收藏
什么是反射?在Java中,对于任意一个运行中的对象,可以调用该对象的任意属性和方法;已知一个类,可以获取这个类的所有属性和方法,这就是反射机制。一般情况下,根据面向对象封装原则,J...【详细内容】
2020-10-28   Java  点击:(7)  评论:(0)  加入收藏
正如本文标题所言,今天我们来聊一聊在Java应用系统中如何防止接口重复提交;简单地讲,这其实就是“重复提交”的话题,本文将从以下几个部分展开介绍:1.“重复提交”简介与造成的后...【详细内容】
2020-10-28   Java  点击:(7)  评论:(0)  加入收藏
1、 Java的堆内存和栈内存Java把内存划分为两种:一种是堆内存,一种是栈内存堆:主要用于储存实例化的对象、数组。由JVM动态分配内存空间。一个jvm只有一个堆内存,线程是可以共享...【详细内容】
2020-10-23   Java  点击:(8)  评论:(0)  加入收藏
简介JavaScript 是一种动态类型语言,这意味着解释器在运行时确定变量的类型。实际上,这也允许我们在相同的代码中使用相同的变量来存储不同类型的数据。如果没有文档和一致性,...【详细内容】
2020-10-22   Java  点击:(3)  评论:(0)  加入收藏
简介在本文中,我们将介绍如何用Node.js和Express来使用 Handlebars 模板引擎。还会介绍什么是模板引擎,以及如何使用把 Handlebars 建服务器端渲染(SSR) web应用程序。我们还...【详细内容】
2020-10-19   Java  点击:(5)  评论:(0)  加入收藏
简介java中多线程的开发中少不了使用Thread,我们在使用Thread中提供的API过程中,应该注意些什么规则呢?一起来看一看吧。start一个ThreadThread中有两个方法,一个是start方法,一...【详细内容】
2020-10-19   Java  点击:(5)  评论:(0)  加入收藏
▶️四个参数 Accumulator (acc) (累计器) Current Value (cur) (当前值) Current Index (idx) (当前索引) Source Array (src) (源数组)reducer 函数的返回值分配给累计器,并...【详细内容】
2020-10-16   Java  点击:(6)  评论:(0)  加入收藏
逻辑赋值是对现有数学和二进制逻辑运算符的扩展。我们先复习一下,然后看看把它们结合在一起能得到什么。首先,我们来看下 JS 中条件运算符与无条件运算符之间的区别 。无条件...【详细内容】
2020-10-15   Java  点击:(3)  评论:(0)  加入收藏
jQWidgets是一个基于jQuery JavaScript的综合性和创新性的HTML5 UI组件库,旨在帮助开发者创建专业、跨平台的Web应用程序,并最大限度的节省开发时间。jQWidgets包含30多种UI组...【详细内容】
2020-10-15   Java  点击:(6)  评论:(0)  加入收藏
一、Java日志框架发展历程 1996年,欧洲安全电子市场项目组编写了自己的程序跟踪API,后发展成为Log4j,并成为Apache基金会项目成员; 2002年Java1.4,Sun推出Java Util Loggin(JUL)...【详细内容】
2020-10-14   Java  点击:(4)  评论:(0)  加入收藏
1. 简述 Elasticsearch 是基于 Lucene 开发的一个分布式全文检索框架,向 Elasticsearch 中存储和从 Elasticsearch 中查询,格式是json。 向 Elasticsearch 中存储数据,其实...【详细内容】
2020-10-12   Java  点击:(5)  评论:(0)  加入收藏
写这篇的时候,相信有很多朋友还在用Jedis作为Redis的客户端,我不禁有很多问号,Jedis还香吗?如果你早些年说它香我信,但是都2020年了,它真的不那么香了。那为什么还继续使用它呢?大...【详细内容】
2020-10-12   Java  点击:(6)  评论:(0)  加入收藏