目录

前言

现象

分析

原因分析

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-storeLogin 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 思否

jar包冲突与NoClassDefFoundError相关推荐

  1. maven jar包冲突常见报错及解决方法

    见到如下错误,可以想到是不是jar包冲突 1.java.lang.NoSuchMethodError 2.java.lang.ClassNotFoundException 3.java.lang.No ...

  2. Jar包冲突详解(Java)

    Jar包冲突是老生常谈的问题,几乎每一个Java程序猿都不可避免地遇到过,并且也都能想到通常的原因一般是同一个Jar包由于maven传递依赖等原因被引进了多个不同的版本而导致,可采用依赖排除.依赖管理 ...

  3. idea解决jar包冲突的实用技巧

    背景:在项目开发过程中,我们经常会使用到maven来管理jar包并作为项目打包构建工具,但是经常会遇到jar包冲突的问题 下面就分享一下解决jar包冲突的几个实用技巧 1.比如说:经常遇到NoClas ...

  4. 系统重构:从Jar包冲突搞到类加载机制,就是这么霸气

    接手了一套比较有年代感的系统,计划把重构及遇到的问题写成系列文章,老树发新枝,重温一些实战技术,分享给大家.[重构01篇],给大家讲讲Jar包冲突及原理. 背景 目前市面上项目管理要么是基于Maven ...

  5. 霸气的来了:从Jar包冲突搞到类加载机制!

    接手了一套比较有年代感的系统,计划把重构及遇到的问题写成系列文章,老树发新枝,重温一些实战技术,分享给大家.[重构01篇],给大家讲讲Jar包冲突及原理. 背景 目前市面上项目管理要么是基于Maven ...

  6. jar包冲突解决思路

    工作中,难免会因为各种原因需要对项目jar包进行升级,一升级,就会碰到各种jar包冲突问题,这时候就需要我们能很快定位冲突的位置,以及如何快速调整依赖. 首先,我们通常说的jar包冲突到底是指什么? ...

  7. 就是这么zhuai,从Jar包冲突搞到类加载机制

    接手了一套比较有年代感的系统,计划把重构及遇到的问题写成系列文章,老树发新枝,重温一些实战技术,分享给大家.[重构01篇],给大家讲讲Jar包冲突及原理. 背景 目前市面上项目管理要么是基于Maven ...

  8. Maven Jar包冲突?看看高手是怎么解决的

    接手了一套比较有年代感的系统,计划把重构及遇到的问题写成系列文章,老树发新枝,重温一些实战技术,分享给大家.[重构02篇]:Maven项目Jar包管理机制.冲突解决. 知识背景 Jar包冲突在软件开发 ...

  9. jar包又冲突了?如何快速确定与哪个jar包冲突?

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 前段时间写代码的时候想借助一下fastjson的Featu ...

最新文章

  1. MacBook如何用Parallels Desktop安装windows7/8
  2. PHPCMS 错误日志 Only variables should be passed by ...
  3. STM32的CAN总线的接收双FIFO使用方法
  4. go语言离线查看说明文档
  5. 别总写代码,这100多个相见恨晚的网站比涨工资都重要
  6. 在项目中寻找代码的坏味道(命名)
  7. 【白话科普】网站图片不显示,背后的原因你都清楚吗
  8. StyleAI:白度-物理上,怎样才算白?
  9. SAP ABAP实用技巧介绍系列之ABAP取中文字符串的字节长度
  10. php vprintf,vprintf - [ C语言中文开发手册 ] - 在线原生手册 - php中文网
  11. C语言格式控制符/占位符 - C语言零基础入门教程
  12. KafkaConsumer源码解析
  13. c语言酒店管理系统,C语言酒店管理系统.pdf
  14. MySQL 非空约束(NOT NULL)入门
  15. NYOJ_23_取石子(一)
  16. AMD Catalyst 14.4 Linux带来完整的 OpenGL 4.4 支持
  17. [文档]CSS中文字体对照表
  18. ROS dst-nat端口映射限制访问映射IP
  19. 视觉三维重建核心算法讲解和代码实现(sfm构建稀疏地图和mvs构建稠密地图)...
  20. C语言数字图像处理---3.3图像锐化

热门文章

  1. LeetCode-878. 第 N 个神奇数字【数学,二分查找,找规律】
  2. 【实例】使用PHP类库PHPqrCode生成二维码
  3. Severlet学习:Cookie 处理
  4. java获取文件名乱码
  5. 虚拟机使用的是此版本 VMware Workstation 不支持的硬件版本。 模块“Upgrade”启动失败。 未能启动虚拟机。
  6. 【点云处理】PointNet网络
  7. 体验deepin作为办公系统
  8. 图片和图形之减少透支(17)
  9. SSH 连接工具 xshell - 业界最强大的 SSH 客户端、Linux 远程连接工具
  10. 微信小程序面试题(个人学习)