深度探究依赖冲突 NoSuchMethodError 问题解决之道
一、背景
- 由于公司parent-pom.xml父模块做了一次整体升级,下游相关服务部署测试时报错,报错内容如下:
2023-10-26 14:09:24.555 ERROR [main] [tid:TID: N/A|req:|cip:|channel:] [o.s.b.SpringApplication:858] - Application run failed
java. lang. NoSuchMethodError: com.google.common.base.Splitter.splitToList(Ljava/lang/Charsequence:)Ljava/util/List;
at com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer.initialize(ApolloApplicationContextInitializer.java:101) ~[apollo-client-1.7.1
20210812.jar!/:1.7.1-20210812]
at com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer.postProcessEnvironment (ApolloApplicationContextInitializer,java:166) ~[apollo-
client-1.7.1-20210812.jarl/:1.7.1-20210812]
at org.springframework.boot.context.config.ConfigFileApplicationListener.onApplicationEnvironmentPreparedEvent(ConfigFileApplicationListener.java:179) ~[spr
ing-boot-2.1.4. RELEASE. jarl/:2.1.4.RELEASE]
at org.springframework.boot.context. config.ConfigFileApplicationListener.onApplicationEvent(ConfigFileApplicationListener.java:165) ~[spring-boot-2.1.4.RELE
ASE.jarl/:2.1.4.RELEASE]
at org.springframework.context.event. SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172) -[spring-context-5.1.6.R
ELEASE.jarl/:5.1.6. RELEASE]
at org.springframework.context.event. SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster,java:165) -[spring-context-5.1.6.REL
EASE. jarl/:5.1.6. RELEASE]
at org.springframework.context.event. SiDpleApplicationEventMulticaster.multicastEvent (SimpleApplicationEventMulticaster.java:139) ~[spring-context-5.1.6.REL
EASE.jar!/:5.1.6.RELEASE]
at org.springframework.context.event. SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:127) -[spring context-5.1.6.REL
EASE, jarl/:5.1.6. RELEASE]
at org.springframework.boot.context.event.EventPublishingRuntistener.environmentPrepared(EventPublishingRuntistener. java:/5) ~[spring-boot-2.1.4.RELEASE. jar
!/:2.1.4.RELEASE]
at org.springframework.boot.SpringapplicationRunListeners.environmentPrepared(SpringApplicationkuntisteners.java:54) -[spring-boot-2.1.4.RELEASE.jar!/:2.1.4
RELEASE]
at org.springframework. boot. SpringApplication.prepareEnvironment (SpringApplication.java:347) -[spring-boot-2.1.4.RELEASE.jar!/:2.1.4.RELEASE]
at org. springframework. boot. SpringApplication.run(SpringApplication.java:306) -[spring-boot-2.1.4. RELEASE.jarl/:2.1.4.RELEASE]
at org.springframework. boot. SpringApplication. run(SpringApplication, java:1260) |[spring boot-2.1.4.RELEASE. jar!/:2.1.4. RELEASE]
at org.springframework.boot. SpringApplication.run(SpringApplication.java:1248) -[spring-boot-2.1.4.RELEASE.jar!/:2.1.4.RELEASE]
at tech.qifu. jinke.yushu.dds.app. YushuDdsAppApplication.main(YushuodsAppApplication.java:24) -[classes]/:7]
at sun. reflect. NativeMethodAccessorImpl. invoke0(Native Method) [ ?: 1.8.0 352)
at sun. reflect. NativeMethodAccessorImpl. invoke(NativeMethodAccessorImpl.java:62) [7:18.0 _352]
at sun. reflect. DelegatingMethodAccessorImpl. invoke(DelegatingMethodAccessorImol.java:43) -[ ?: 1.8.0 352]
at java. lang. reflect.Method. invoke(Method. java:498) ~[7:1.8.0 352]
at org.springframework.boot. loader.MainMethodRunner.run(MainMethodRunner. java:47) -[jsbank-yushu-dds_231026115655932-1b4030f.jar :? ]
at org. springframework.boot. loader.Launcher. launch(Launcher, java: 86) ~[isbank-yushu-dds 231026115655932-1b4030f. jar:7
at org.springframework. boot. loader.Launcher. launch(Launcher. java:50) ~[jsbank-yushu-dds 231026115655932-1b4030f.jar :? ]
at org.springframework.boot. loader. JarLauncher.main(JarLauncher. java:51) -[jsbank-yushu-dds_231026115655932-1b4030f.jar :? ]
二、问题定位
2.1. 第一次定位
日志最关键的一句如下:由于程序在运行时调用com.google.common.base.Splitter类的splitToList函数,但未找到该方法导致抛异常,于是着手在服务上寻找该类的依赖包
java.lang.NoSuchMethodError: com.google.common.base.Splitter.splitToList(Ljava/lang/Charsequence:)Ljava/util/List;
- 在IDEA中通过全限定名寻找该类,结果如下:该类属于guava包
- 看到该类属于guava包,下意识认为是jar包冲突问题,毕竟guava包冲突是常见问题,于是着手分析应用的pom.xml依赖,如下:一片爆红
- 此时到服务器上找到应用,解压后查看依赖:BOOT-INF/lib目录下存在的是guava:32.0.1-jre.jar包
- 此时感觉已经定位到问题了,认为只要将所有依赖guava:32.0.1-fre的包全都<exclusion>剥离,只保留应用自身的guava:20.0依赖应该可以解决,如下:此时maven依赖清爽了很多
- 随后打包部署验证,遗憾的是和之前的报错一样:java.lang.NoSuchMethodError: com.google.common.base.Splitter.splitToList(Ljava/lang/Charsequence:)Ljava/util/List;
2.2. 第二次定位
- 经过了第一次失败后开始着手看栈日志,定位报错位置代码,如下:ApolloApplicationContextInitializer.java:101
2023-10-26 14:09:24.555 ERROR \[o.s.b.SpringApplication:858\] - Application run failed
java.lang.NoSuchMethodError: com.google.common.base.Splitter.splitToList(Ljava/lang/Charsequence:)Ljava/util/List; at
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer.initialize(ApolloApplicationContextInitializer.java:101) ~\[apollo-client-1.7.1]
- 报错位置显示为:ApolloApplicationContextInitializer.java 类 101 行引发报错,于是翻看源码,如下:正是此处调用com.google.common.base.Splitter.splitToList函数导致报错
- 于是推断该apollo包的pom.xml中应该依赖了guava,只要我的应用在外部指定的guava版本和apollo依赖的guava版本保持一致应该就可以解决此问题,于是查看apollo:pom.xml依赖,如下:
- 此时又认为定位到了问题,认为只要将应用的guava版本改为19.0即可,如下:
- 于是打包部署验证,遗憾的是和之前的异常一样 = =!java.lang.NoSuchMethodError: com.google.common.base.Splitter.splitToList(Ljava/lang/Charsequence:)Ljava/util/List;
2.3. 第三次定位
- 经过了前两次的失败,已经不在单纯的认为是guava版本冲突了,思考:NoSuchMethodError异常其实很好理解,即Splitter类中没有splitToList函数;于是翻看guava:32.0.1-jre/20.0/19.0 这三个不同版本包中的Splitter类,如下:三个版本的包皆有splitToList函数
- 于是推测应该是其他依赖包中依赖了guava包,而在类加载过程中指向了错误的依赖包而没有依赖我们指定为的guava包从而导致这个问题,于是开始逐一搜索含有com.google.common.base.Splitter类的jar包,一番寻找后定位到了hive-exec:2.3.2.jar,如下:
- 进到hive-exec:2.3.2.jar包目录下发现Splitter类没有splitToList函数,如下图:
- 此时认为已经定位到了问题,解决方案是在应用中将hive-exex.jar依赖通过<exclusion>剥离guava包即可
- 但是发现应用中的pom.xml中本就排除了guava依赖,如下:这就有点奇怪了
2.4. 第四次定位
- 经过前几次的尝试,仍认为是hive-exec.jar包问题,于是又仔细查看了hive-exec的结构目录,果然有了新的发现,如下:hive-exec.jar代码包中就嵌入了com.google.common.base.Splitter类,从pom.xml层面根本无法通过<exclusion>剥离。
- 本质:原因是hive-exec通过Maven Shade Plugin的方式将所有依赖打成了shadow JAR,这里解释一下Maven Shade Plugin:
Maven Shade Plugin通常用于创建"fat JAR"(也称为"uber JAR")或"shadow JAR",这种 JAR 文件包含了所有应用程序的依赖项,以便在运行时可以独立运行。这种 JAR 文件通常不再包含BOOT-INF目录,而是将所有依赖项合并到了JAR文件的根目录下。
当使用Maven Shade Plugin创建"fat JAR"时,它会合并所有的依赖项,包括JAR文件和类文件,将它们放在一个JAR文件中。这个JAR文件没有BOOT-INF目录,而是将所有的类和资源放在一个扁平的结构中。这意味着应用程序的类和依赖项的类都在同一个类路径下,没有明确的区分。
- 正是因为Maven Shade Plugin 将所有依赖放在同一个结构中,这才导致传统的 <exclusion>无法剥离guava依赖,因为该依赖已经嵌入到代码中!
解决方案
- 经过了上面四次的尝试已经定位到了问题,此时有如下三种解决方案:
- 直接剔除hive-exec依赖包
- 升级hive-exec到3.x版本,3.x版本中Splitter类包含splitToList函数
- 通过-Djava.class.path=./lib/guava-20.0.jar 启动参数来调整类加载顺序,优先加载指定的guava包
- 通<classifier>core</classifier>配置指定特定版本的依赖项
第一种风险较大不推荐,第二种升级可能会存在潜在问题,第三种改动最小也最优雅,前三种实现方式都不复杂,但是这里想介绍一下第四种方式:下面将介绍<classifier>标签作用
classifier
标签通常用于 Maven 依赖项的配置,以指定要使用的依赖项的分类(classifier)。分类是一种方式,它允许您为同一依赖项的不同版本或变种(variant)创建不同的标识。
一般情况下,Maven 依赖项的坐标包括以下元素:
- groupId:依赖项的组标识。
- artifactId:依赖项的工件标识。
- version:依赖项的版本号。
然而,有时候同一个 groupId、artifactId 和 version 的依赖项可能有多个变种或版本。这时, 元素可以用来指定使用哪个特定版本或变种的依赖项。
例如,假设有一个库 my-library 有两个不同的版本:1.0 和 1.0-core,您可以使用 元素来指定使用1.0-core版本,如下所示:
<dependency>
<groupId>com.example</groupId>
<artifactId>my-library</artifactId>
<version>1.0</version>
<classifier>core</classifier>
</dependency>
在这个示例中, 元素指定了使用 1.0-core 版本的库,而不是默认的 1.0 版本。
分类通常用于区分不同构建或变种,以满足不同的需求。它允许您在同一个项目中同时使用不同版本的依赖项,而不必修改它们的工件标识或版本号。
实施
- 此时我们查看maven仓库中的hive-exec:2.3.2所对应的不同依赖项,maven地址
- 所有变种依赖如下:而应用只需用到hive-exex-2.3.2-core.jar依赖即可满足使用;
- 注意:如果你的应用还需要source等依赖则要按需依赖!
- 更改maven配置后,查看dependence依赖发现google目录已消失,如下:
- 此时打包部署测试后问题解决
总结
此次问题的排查总结如下:
- 详细的日志和异常信息非常关键:仔细分析应用程序的异常信息和日志可以提供关键线索,帮助快速定位问题。
- 深入了解依赖关系:了解应用程序的依赖关系,特别是外部库和其版本,有助于更好地理解问题的根本原因。
- 查看应用程序的依赖项:检查应用程序的依赖项,尤其是可能与异常相关的依赖项,可以帮助确定问题来源。
- 逐步分析和排查:问题的定位通常需要逐步分析和排查。在解决问题时,不要害怕多次尝试,每次都获取更多信息。
转载自:https://juejin.cn/post/7350977538275852297