jar包冲突与NoClassDefFoundError

目录

前言

现象

分析

原因分析

Jar包冲突的原理

Maven依赖传递原则

Maven依赖实例分析

编译期不报错而运行期报错的原因


前言

相信jar包冲突问题是Java工程师经常遇到的问题之一。

说来惭愧,作为一名多年coding经验的老工程师,之前一直得过且过,没有仔细分析这个问题。

现象

经过一轮新的迭代,各功能在开发和测试环境验证均没问题,但上线后突然报错,关键日志摘录如下:

Caused by: java.lang.NoSuchMethodError: reactor.core.publisher.Mono.contextWrite(Lreactor/util/context/ContextView;)Lreactor/core/publisher/Mono;
	at com.shaded.azure.core.http.rest.RestProxy.handleRestReturnType(RestProxy.java:548)
	at com.shaded.azure.core.http.rest.RestProxy.invoke(RestProxy.java:148)
	at com.sun.proxy.$Proxy182.upload(Unknown Source)
	at com.shaded.azure.storage.blob.implementation.BlockBlobsImpl.uploadWithResponseAsync(BlockBlobsImpl.java:395)
	... 77 more

排查发现是该轮迭代新增的存储功能抛错,项目依赖了某个SDK,该SDK对用户屏蔽了底层存储(Ceph/Azure Blob/AWS S3等)的差异,但是测试环境和生产环境的底层存储介质不同,测试阶段没有覆盖到这些不同的介质。

这就是jar包冲突的经典在线场景:1、Java项目编译不报错;2、在A环境下运行正常;3、切换到B环境后运行报错(NoSuchMethodError、ClassNotFoundException等最常见)。

分析

首先梳理了下我司现有Java项目构建发布的流程(我司依赖某款自研工具进行自动进行),该流程分位两个阶段,构建和部署,截取构建阶段关键信息如下:

 ------------------------- [ Check Out Code  ] ------------------------- 
check out code 1660554686391
/xxxxxx/281534
Cloning into 'xxxxxxx'...
Note: checking out 'f649b4102ff5266f5d213c9e6562831fb237adcd'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at f649b410 refine es code
 ------------------------- [ Finish Check Out Code  ] ------------------------- 


 ------------------------- [ Build Package  ] ------------------------- 
build package 1660554688525

 ------------------------- [ Excute mvn clean package ..... ] ------------------------- 
mvn clean package -U -B -DskipTests
[INFO] Scanning for projects...
[INFO] Downloading from nexus: http://nexus.xxx.com/content/groups/public/com/envisioniot/parent-pom/1.0.0-SNAPSHOT/maven-metadata.xml
[INFO] Downloaded from nexus: http://nexus.xxx.com/content/groups/public/com/envisioniot/parent-pom/1.0.0-SNAPSHOT/maven-metadata.xml (604 B at 3.2 kB/s)

.......

 ------------------------- [ Finish Build Package  ] ------------------------- 
finish build package 1660554791437


 ------------------------- [ Build Docker Image  ] ------------------------- 
build docker image 1660554791441
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

 ------------------------- [ Building Docker Image harbor-alpha1.xxx.io... ] ------------------------- 
docker build -f Dockerfile -t harbor-alpha1.xxx.io/xxx/xxx:feature_branch_2208_20220815091119 . --pull
Sending build context to Docker daemon  435.9MB

 ------------------------- [ Finish Build Docker Image  ] ------------------------- 
finish build docker image 1660554831194

 ------------------------- [ Push Docker Image  ] ------------------------- 
push docker image 1660554831200
The push refers to repository [harbor-alpha1.xxx.io/xxx/xxx]
74c17a129617: Preparing

........

258ddc74925c: Pushed
feature_branch_2208_20220815091119: digest: sha256:68ecae221c0881ca47180ce73c25671c501fd8f4fa1c32dc9843de875939ca58 size: 2422

 ------------------------- [ Finish push Docker Image  ] ------------------------- 

简单分析可见构建阶段大致进行了如下操作:

  1. 拉取代码(git checkout)
  2. Maven构建(mvn clean package -U -B -DskipTests)
  3. 打包docker镜像
  4. 上传镜像到harbor仓库

部署阶段则是将harbor仓库的镜像部署到K8s的过程,这里就不做分析了。

下面开始思考这个问题:

为什么jar包冲突在编译期发现不了,到了运行期才能被被发现呢?

原因分析

Jar包冲突的原理

假设我们项目中依赖了A和B两个Jar包。而A和B各自又有以下传递依赖

A -> X -> Z(2.0)

B -> X -> Y -> Z(2.5)

那最终系统中Z包就产生了冲突,2.0和2.5两个版本冲突。但是classpath中只会依赖一个版本的Z包。根据传递依赖的最短路径优先原则,最终依赖的应该是2.0版本。

Maven依赖传递原则

先从Maven工具开始,公开资料显示,几乎所有的Jar包冲突都和依赖传递原则有关:

最短路径优先原则

假如引入了2个Jar包A和B,都传递依赖了Z这个Jar包:

A -> X -> Y -> Z(2.5)

B -> X -> Z(2.0)

那其实最终生效的是Z(2.0)这个版本。因为他的路径更加短。如果我本地引用了Z(3.0)的包,那生效的就是3.0的版本。一样的道理。

最先声明优先原则

如果路径长短一样,优先选最先声明的那个。

A -> Z(3.0)

B -> Z(2.5)

这里A最先声明,所以传递过来的Z选择用3.0版本的。

Maven依赖实例分析

  1. 先借助mavn命令对某个工程进行构建,运行命令mvn clean package -Dmaven.test.skip=true
  2. 找到其构建好的工程jar包,并进行解压,解压后的项目如图所示
  3. 借助maven helper插件对某个maven工程进行分析
  4. 上图显示HdrHistogram这个包存在jar包冲突的现象,分别有4个不同的jar包带入了2.1.9和2.1.12的2种不同的jar包版本,而根据最短路径优先原则,Maven最终选取了第4个包中的依赖,也就是2.1.9这个版本
  5. 继续分析第二个发生了冲突的包
  6. 上图显示cat-core这个包存在jar包冲突的现象,分别有2个不同的jar包带入了1.3.9和1.3.10的2种不同的jar包版本,而他们的依赖深度一致,此时最短路径优先原则失效,而根据最先声明原则,上面的一个jar包早于下面的jar包声明,所以Maven最终选取了上面jar包中的依赖,也就是1.3.9这个版本

编译期不报错而运行期报错的原因

最后回到最初的疑问,为什么jar包冲突的问题不会在编译期报错呢,我的判断是:发生冲突的jar包都是些已经编译好的.class压缩包,java工程的编译是个.java -> .class的过程,它在编译期只检查.java文件,假如.java引用了jar包里的A Class的a方法,如果a方法存在,编译就能过。但如果a方法又引用了jar包里其他的b方法,而这个b方法实际存不存在,这就不在编译期的检查范围了。

总结

最后总结一下可能导致NoClassDefFoundError产生的几种原因:

  1. Maven中对依赖包的声明为provided,但运行期容器并未提供对应的jar
  2. Maven中嵌套依赖了同一个包的不同版本,但在Maven编译打包的时候排掉了某个版本
  3. 不同jar包中提供了class名称相同的类

参考资料:

用好这几个技巧,解决Maven Jar包冲突易如反掌 - SegmentFault 思否

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值