上篇文章中,小黑哥分析 Maven 依赖冲突分为两类:java
第二种状况,每每是这个场景,本地/测试环境运行的都是好好的,上线以后测试就是不行。shell
这其实与 JVM
类加载有关,本地/测试环境加载正确类,而生产环节加载错的类,为何会这样?segmentfault
主要有两个缘由:缓存
JVM
类加载具备缓存机制,每一个类加载的时候首先检查一遍,类是否被当前类加载器加载。若未被加载,先交给其父类加载器加载,父类加载器不能加载,才会交给当前类加载器。网络
当前类加载器加载完成以后,将会将其缓存起来。jvm
类加载的核心源码位于 ClassLoader#loadClass
:ide
① 处将会检查ClassLoader#findLoadedClass
最终将会调用 ClassLoader#findLoadedClass0
,这是一个 native
方法,最终将会根据类名加类加载器为键值查找缓存。测试
每一个类加载器负责的加载范围都不同:idea
BootstrapClassLoader
引导类加载加载最核心的类库,如 $JAVA_HOME/jre/lib/
ExtClassLoader
扩展类加载器负责加载$JAVA_HOME/jre/lib/ext
下的一些扩展类AppClassLoader
应用类加载器将加载 classpath
指定的类。咱们运行的应用依赖的各类类,通常将会由 AppClassLoader
记载,同名类被加载后,下次碰到就不会再被加载。spa
画外音:利用缓存加快查询速度
Java 可使用 -classpath
参数指定依赖类所在位置。
类的加载顺序能够经过如下方式指定:
java -classpath a.jar:b.jar:c.jar xx.xx.Main
上面这种方式,类加载首先会从 a.jar 中查找相关类,找不到才会继续日后查找。因此能够经过这种方式能够指定使用哪一个 jar
包内同名类。
可是这种方式有点繁琐,若是依赖 100 个 jar
包,须要所有写上去。
因此生产环境可使用使用 shell
命令将 jar 拼接起来:
LIB_DIR=lib LIB_JARS=`ls $LIB_DIR|grep .jar|awk '{print "'$LIB_DIR'/"$0}'|tr "\n" ":"`
另外 java
支持通配符的写法:
java -classpath './*' xx.xx.Main
这种方式的加载顺序将会受到底层系统文件加载顺序影响。
假设咱们如今应用依赖以下:
A 应用依赖 B、C,且 B,C 中存在同包同名类 org.example.App
,代码以下:
若是指定 jar 包顺序启动应用:
# A,B,C 放置同一文件夹下 java -classpath A-1.0-SNAPSHOT.jar:B-1.0-SNAPSHOT.jar:C-1.0-SNAPSHOT.jar org.example.ClassA
日志输出以下:
改变 B ,C 顺序:
类加载器的类的查找顺序将会经过 classpath
指定顺序从前日后查找。
若是使用通配符启动:
java -classpath './*' org.example.ClassA
这种状况 jvm
到底加载那个类就成了薛定谔的类了,运行以前没法肯定加载类来自哪一个 jar
包。
咱们能够在 jvm
启动脚本加入以下参数 -verbose:class
,而后重启,日志里会打印出每一个类的加载信息。
java -verbose:class -classpath './*' xx.xx.Main
日志输出以下:
经过这种方式能够看到加载类来源于哪一个Jar
包。
不过这种方式须要重启应用,对生产系统来讲,影响仍是比较大,不太优雅。
阿里开源项目 Arthas sc 命令能够用来查找加载类的信息。。
sc 命令是 Search-Class 简写,这个命令能搜索出已经加载到 JVM 中的 Class 信息,支持参数以下表格所示。
程序启动以后,启动 arthas,进入 A 应用。
运行以下命令:
sc -d org.example.App
输出结果以下 :
code-source 显示当前查找类 org.example.App
来自的 C。
另外咱们能够 jad 命令反编译类,在线查看源码。
这篇文章主要解释应用中存在多个同名类,环境不一样,类加载不一样的缘由。接着介绍了两种快速查找运行应用依赖类来源的方法。
当定位到了冲突类的来源,咱们能够显示指定 classpath
jar 包的顺序,指定类加载的顺序。但这只是暂时解决问题。本质上依赖冲突的问题,仍是须要深层次排除的。
欢迎关注个人公众号:程序通事,得到平常干货推送。若是您对个人专题内容感兴趣,也能够关注个人博客: studyidea.cn