转载

深入理解jar包冲突的本质

前言

上篇文章 记一次log4j不打印日志的踩坑记 介绍了遇到的log4j踩坑经历和解决方法,这篇文章我们重点来学习和了解下有关Java中日志组件的内容,在这之前,其实在我的头脑里,并没有形成系统的日志框架知识,原因其实是一直没有重视过这块,之前都是各种拷贝改改能跑就行,并不理解相关的架构和原理,这次趁着这个机会正好来系统了解一下,除了要系统的理解日志框架大多数知识外,我们还要学习一个非常关键的知识,就是关于Java默认的类加载器加载jar包的顺序问题,不夸张的说,只有理解了这个,才能搞明白jar冲突问题发生的本质。

Java日志框架一览

  1. java.util.logging (JUL)

JDK自带日志组件,使用方式简单,不需要依赖第三方日志组件。支持将日志打印到控制台,文件,甚至可以将日志通过网络打印到指定主机。相对于第三方独立日志框架来说,支持的日志级别比较少,功能也比较单一。

2,apache commons logging

Apache Commons Logging也称(JCL),为啥不叫ACL而是JCL呢,其实这里面有个小典故,曾经Apache 基金会用于管理各个 Java 子项目,诸如 Ant、Commons、JAMES 等都使用Jakarta,2011 年 12 月,在所有子项目都被迁移为独立项目后, Jakarta 名称就不再使用了。其实2011年12月之前,apache commons logging的别名是Jakarta commons logging 故称为JCL,因为历史问题,所以一直没有更正。

JCL提供了一个轻量级的日志抽象,为应用程序提供统一的日志API。允许用户使用具体的日志实现,如:log4j,Avalon LogKit,java.util.logging。当然,JCL同时也提供了一个简单的日志实现org.apache.commons.logging.impl.SimpleLog,将日志输出到System.err。目前JCL已经停止更新了,最新发布版本为1.2 Release - July 2014。

3,log4j

Apache的一个开放源代码项目,log4j可以当之无愧地说是Java日志框架的元老,1999年发布首个版本,2012年发布最后一个版本,2015年正式宣布终止,至今还有无数的系统在使用log4j,甚至很多新系统的日志框架选型仍在选择log4j。log4j是通过一个.properties的文件作为主配置文件的log系统,由于自身存在太多弊端,比如高并发情况下死锁问题,不支持占位符等

所以已经在2015年8月份停止更新,最后一个版本为log4j 1.2.17。

4,logback

Logback是由log4j创始人设计的另一个开源日志组件,可以认为是log4j的改进版,非常简单灵活,官方网站:http://logback.qos.ch。它当前分为下面几个模块:

logback-core:核心代码模块 logback-classic:log4j的一个改良版本,同时实现了slf4j的接口,这样你如果之后要切换其他日志组件也是一件很容易的事 logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能

5,log4j2

logback和log4j2其实都是log4j的后代,最初都是出自同一个作者,但其后来放弃了log4j,又开发了新的logback,而log4j则由社区接管,在2014年底才推出log4j2,比logback晚了好几年,这期间log4j2大量吸收了slf4j和logback的一些优点(比如日志模板),同时应用了不少的新技术,由于采用了更先进的锁机制和LMAX Disruptor库,log4j2的性能优于logback,特别是在多线程环境下和使用异步日志的环境下,此外也支持占位符,插件化,gc优化,支持java8等功能。

6,slf4j

slf4j也称The Simple Logging Facade ,即简单日志门面(Simple Logging Facade for Java),有点类似于我们的USB接口,制定了统一的接口访问标准,并不是具体的日志实现,也就是说slf4j并不是为了替代前面的几个日志框架,而是合作互惠关系,当然你也可以不合作,拒绝使用USB接口,那就会导致你这个产品兼容性太差,绑定太紧,肯定不是用户所期待的。这也是slf4j与前面介绍过的5个日志框架是最大的区别之处。简单点说sfl4j是一层api接口,其他的5个日志框架都是实现类。

slf4j的出现是为了解决,一个项目中出现了多个日志依赖,从而导致项目难以管理和维护。举个例子假如你的项目依赖log4j,某天你引入了redis框架,而redis又依赖logback,这个时候你得同时管理两套日志框架,最为致命的是多年后log4j已经废弃不维护了,这个时候如果你想换新的log4j2框架,那就意味着你需要改原来到处散落的log4j代码,这导致了耦合太近,维护复杂。为了解决这个问题,slf4j就诞生了,其制定了统一的api接口,从设计模式的角度考虑,它是用来在log和代码层之间起到门面的作用。对用户来说只要使用slf4j提供的接口,即可隐藏日志的具体实现。这与jdbc相似。使用jdbc也就避免了不同的具体数据库。使用了slf4j可以对客户端应用解耦。因为当我们在代码实现中引入log日志的时候,用的是接口,所以可以实时的根据情况来调换具体的日志实现类。这就是slf4j的作用。当然前面提到的JCL也有门面的功能,但后来被slf4j全面超越,故已经衰退,所以slf4j已经成了事实上日志门面接口标准。

官网给出的一张图,非常形象的表达了这种关系:

深入理解jar包冲突的本质

日志组件的构造

大体上来说共有三个部分:

(1)日志门面接口

(2)桥接器

(3)日志框架具体实现

如下面的一张图所示:

深入理解jar包冲突的本质

从上面的图中我们可以看到日志门面接口会通过桥接绑定的方式与下游的日志框架类进行绑定,需要注意的是slf4j在运行的时候,只会与下游的实现类绑定一次,也就是说slf4j,有且只能在运行时绑定一款日志实现框架,那么该有同学该提问了,如果下游同时存在多个日志实现框架,会发生什么情况?

这个问题很有意思,首先slf4j在运行时会打印所有在classpath里面发现的所有日志实现类,然后会选择第一个被类加载器加载的实现类作为底层的真正的日志组件,之后其他的实现类会被忽略,因为Java类加载器在加载多个同包名同类名的class的时候,只有第一个会成功,后面的不会被加载,这也是双亲委派模型的经典之处。

jar包冲突之谜

ok,我们回顾下上篇文章末尾提到的问题:

(1)同样的部署包,为什么有的机器会正常输出log,而有的却失败了呢?

(2)同样的slf4j 绑定的实现类,为什么也会发生有的机器可以输出,有的会失败呢?

回答:

第一个原因:

Java类加载器加载同一个目录下的jar包的顺序是随机的,会受操作系统的文件系统影响。

第二个原因:

加载的jar包中出现了冲突,包括同jar不同版本和不同jar但存在同包名同类名的class,其实在包冲突的情况下,如果类加载器按照正常的顺序加载,是没有问题的,但如果恰好冲突的jar包,加载的顺序发生了颠倒,那么就极有可能引发莫名其妙的问题,这也是为什么篇文章中提到的在200多台机器中,仅仅只有50多台发生了问题,其他的缺没有出现任何问题,这也从侧面证实了jar顺序的问题。

关于Java类加载器加载jar包的顺序是随机的,我特意找了相关的理论资料,因为仅仅从现象上推断还不够严谨,必须有权威的资料来说明才行:

(一)来自Oracle JDK官网的一段说明:

https://docs.oracle.com/javase/7/docs/technotes/tools/solaris/classpath.html

The order in which the JAR files in a directory are enumerated in the expanded class path is not specified and may vary from platform to platform and even from moment to moment on the same machine. A well-constructed application should not depend upon any particular order. If a specific order is required then the JAR files can be enumerated explicitly in the class path.

含义:同一个目录下,jvm加载jar包顺序是无法保证的,每个系统的都不一样,甚至同一个系统不同的时刻加载都不一样。良好设计的系统不应该依赖任何特定的加载顺序。(画外音:最好不要出现多个冲突的jar包)

(二)其他搜集资料

https://stackoverflow.com/questions/5474765/order-of-loading-jar-files-from-lib-directory/26642798

https://www.maheshsubramaniya.com/article/understanding-how-jars-are-loaded-into-jvm-from-a-directory.html

jar冲突的解决方法

(1)上线前 + 提前检测

这里分两种情况:jar冲突可避免:在部署前检测是否有冲突的可能,如果有就提前移除冲突的依赖,减少这种问题发生的可能。推荐使用maven的maven-enforcer-plugin插件,可以帮助检测同包名+同类名的依赖冲突 jar冲突不可避免 如果jar冲突不可避免,这个时候是不能直接移除依赖的,否则会引起另外一个组件报异常,这个时候 可以使用maven-shade-plugin插件,来对同名同包的其中一个版本进行 ”rename“,不影响正常功能,相当于是 绕过了冲突。

(2)上线后 + 临时解决

如果上线前没有注意到这些,导致在上线后才发现问题,那么我们可以采用临时处理方式,只需要移除与冲突的相关的jar包即可。比如在我们的项目里使用的是log4j 2.x 版本,所以只需要临时移除如下冲突的log4j 1.x的jar包,然后重启即可,另外在记得下一个版本中去掉无用的依赖。

总结

想必现在,大家应该对jar冲突的问题,应该有了一个深刻的认知了,而不是仅仅停留在问题的表面,这里面关键点在于,要认知到JVM加载jar顺序是不确定的,其会受不同的操作系统平台带来的影响,具体细节可以看我发的资料链接。正如Oracle JDK官网文档所言,良好的系统设计不应该依赖jar包的加载顺序,其实也在提示我们最好不要有冲突存在的情况,如果冲突真的不可避免,那么可以通过maven插件来间接的绕过冲突。

最后在多说一句,遇到问题时,不要有意排斥,而应该抓住机会,迎面而上研究其根本原因,排查的过程也是提升问题解决能力的一个重要方式,每一个优秀的工程师和技术专家都必定经历过无数个复杂问题的洗礼才得以成长起来。

原文  http://mp.weixin.qq.com/s?__biz=MzAxMzE4MDI0NQ==&mid=2650336747&idx=1&sn=d2c0f42f81efd5acbac49626e0abd090
正文到此结束
Loading...