首页 / 资讯中心 / 文章详情

Windows+IDEA下Spark项目Hadoop/Hive/Jackson/Guava依赖冲突排查与解决

Windows+IDEA下Spark项目Hadoop/Hive/Jackson/Guava依赖冲突排查与解决 ★ FEATURED ARTICLE
1. 从一次本地调试崩溃说起依赖冲突到底长什么样Windows 上用 IDEA 跑 Spark 项目很多人第一次遇到依赖冲突时的反应都是懵的——代码明明在 Linux 集群上跑得好好的搬到本地就报NoSuchMethodError、ClassNotFoundException或者更诡异的java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkArgument。这类报错有个共同特征编译期一切正常运行期才炸。原因就在于 Maven 的依赖仲裁机制在编译时只保证类存在不保证运行时加载的是哪个版本的类。我这次遇到的具体场景是这样的一个 Spark 项目需要同时读写 Hive 表底层依赖 Hadoop 的Configuration和FileSystem同时 Spark SQL 内部又依赖 Hive 的SessionState。项目本身还引入了 Guava 做本地缓存、Jackson 做 JSON 序列化。结果一跑spark-submit或者直接在 IDEA 里点 Run就抛出java.lang.NoSuchMethodError: com.google.common.base.Preconditions.checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V这个报错的本质是编译时用的 Guava 版本有这个方法运行时 classpath 里加载的却是另一个没有该方法的旧版本。Guava 在 20.0 之后把很多方法签名改了而 Hadoop 2.x 系列默认绑定的 Guava 版本普遍偏低常见是 11.0.2 或 16.0Spark 又可能带自己的 Guava。三个框架各带一份谁先被 JVM 加载谁就赢这就是典型的类路径污染。Jackson 的问题更隐蔽。Hadoop、Hive、Spark 三家对 Jackson 的依赖版本差异很大jackson-databind从 2.9 到 2.12 之间有好几个不兼容的 API 变更。典型症状是com.fasterxml.jackson.databind.JsonMappingException或者NoClassDefFoundError: com/fasterxml/jackson/core/JsonProcessingException。有时候甚至不报错只是序列化结果悄悄变了这种最难查。所以这篇文章要解决的核心问题就一个在 Windows IDEA 环境下如何系统性地定位并解决 Hadoop、Hive、Spark 三者与 Jackson、Guava 之间的依赖冲突。适合正在本地调试 Spark 项目、被NoSuchMethodError折磨过的开发者。下面我会从依赖树分析、冲突定位、排除策略、版本对齐、验证方法几个层面完整走一遍每一步都给出可复现的操作。2. 先搞清楚谁在打架依赖树分析与冲突定位2.1 用 dependency:tree 把整棵依赖树摊开解决依赖冲突的第一步永远是看清楚现状。IDEA 右侧的 Maven 面板虽然能看依赖但层级折叠得厉害不如命令行直接。在项目根目录执行mvn dependency:tree -Dverbose -Dincludescom.google.guava:guava-Dverbose会显示被省略的冲突依赖标记为omitted for conflict-Dincludes过滤只看 Guava。输出大概长这样[INFO] - org.apache.spark:spark-core_2.12:jar:3.3.0:compile [INFO] | - com.google.guava:guava:jar:14.0.1:compile [INFO] | \- (com.google.guava:guava:jar:16.0.1:compile - omitted for conflict with 14.0.1) [INFO] - org.apache.hadoop:hadoop-common:jar:3.3.1:compile [INFO] | \- (com.google.guava:guava:jar:27.0-jre:compile - omitted for conflict with 14.0.1)这里的关键信息是Maven 默认采用最近优先策略谁在依赖树里离根节点近谁就胜出。上面 Spark 直接依赖的 14.0.1 赢了Hadoop 想要的 27.0-jre 被 omitted 掉了。但问题是 Spark 运行时真正需要的是更高版本的 Guava14.0.1 太老于是Preconditions.checkArgument的新签名找不到直接崩。对 Jackson 同理mvn dependency:tree -Dverbose -Dincludescom.fasterxml.jackson.core:jackson-databind你会看到 Hadoop 带 2.10.x、Hive 带 2.9.x、Spark 带 2.12.x 之类的混战。记住一个原则不要只看直接依赖一定要看传递依赖冲突 90% 来自传递依赖。2.2 在 IDEA 里用 Maven Helper 插件可视化冲突命令行适合快速定位但要看全貌还是得靠插件。IDEA 装一个Maven Helper打开pom.xml底部会多一个 Dependency Analyzer 标签。切到 Conflicts 视图所有冲突依赖会用红色标出右键可以直接 Exclude。这个视图的好处是能一眼看到同一个groupId:artifactId下有几个版本、分别从哪条路径引入。我一般会配合 All Dependencies as Tree 视图把冲突的 artifact 展开看清楚每条引入路径。比如 Guava 冲突展开后能看到spark-core→guava:14.0.1hadoop-common→guava:27.0-jrehive-exec→guava:19.0三条路径三个版本。这时候就要决定统一到哪个版本。2.3 运行时到底加载了哪个类用代码验证光看依赖树还不够因为 classpath 顺序、shade 打包、IDEA 的模块依赖都可能改变实际加载结果。最靠谱的办法是运行时打印public class ClasspathChecker { public static void main(String[] args) { Class? clazz com.google.common.base.Preconditions.class; System.out.println(Guava loaded from: clazz.getProtectionDomain() .getCodeSource().getLocation()); System.out.println(Guava version: com.google.common.base.Objects.class .getPackage().getImplementationVersion()); } }在 IDEA 里直接 Run 这个类输出会告诉你 JVM 实际从哪个 jar 加载了 Guava。如果输出的是guava-14.0.1.jar而你的代码需要 27.0 的 API那冲突就实锤了。Jackson 同理打印com.fasterxml.jackson.databind.ObjectMapper.class的来源即可。提示这个方法在排查编译通过但运行报错时特别有效因为编译期和运行期的 classpath 可能不一致尤其是 IDEA 的 Provided scope 依赖。3. 版本对齐策略为什么不能简单粗暴地全排除3.1 排除法的陷阱排错了会引发连锁反应很多人遇到冲突的第一反应是把低版本 exclude 掉。比如看到 Guava 14.0.1 太老就在spark-core里加dependency groupIdorg.apache.spark/groupId artifactIdspark-core_2.12/artifactId version3.3.0/version exclusions exclusion groupIdcom.google.guava/groupId artifactIdguava/artifactId /exclusion /exclusions /dependency然后手动引入 27.0-jre。看起来解决了但Spark 内部有些代码是针对特定 Guava 版本编译的你换成 27.0 后如果 Spark 用了 27.0 里被移除的 API又会报新的NoSuchMethodError。这就是按下葫芦浮起瓢。我的经验是排除之前先确认目标版本是否向下兼容。Guava 从 14 到 27 大部分 API 是兼容的但Files、CharMatcher等类有过 breaking change。Jackson 的databind在 2.9 → 2.10 之间相对平滑2.10 → 2.12 有JsonNode相关的方法调整。所以对齐版本时优先选择三者都兼容的最大公约数版本而不是盲目追新。3.2 用 dependencyManagement 统一版本号比逐个 exclude 更优雅的做法是在pom.xml顶层用dependencyManagement锁定版本dependencyManagement dependencies dependency groupIdcom.google.guava/groupId artifactIdguava/artifactId version27.0-jre/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.12.7/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-core/artifactId version2.12.7/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-annotations/artifactId version2.12.7/version /dependency /dependencies /dependencyManagementdependencyManagement只声明版本不实际引入依赖。它的作用是当传递依赖引入这些 artifact 时强制使用这里指定的版本。这样你不需要在每个依赖里写 exclusionMaven 会自动仲裁到统一版本。但要注意dependencyManagement对直接依赖无效如果你自己又显式引入了 Guava 的其他版本还是以直接依赖为准。所以要么全部走 dependencyManagement要么全部显式声明别混着来。3.3 版本选择的实操判断依据怎么确定最大公约数版本我的做法是先看 Spark 官方文档的 Dependency Management 章节Spark 3.3.x 明确推荐 Guava 27.0-jre、Jackson 2.12.x。再看 Hadoop 3.3.x 的pom.xml它依赖 Guava 27.0-jre、Jackson 2.12.x和 Spark 基本一致。Hive 3.1.x 稍老依赖 Guava 19.0、Jackson 2.9.x但 Hive 的 Guava 使用面窄升到 27.0 一般没问题。所以最终锁定Guava 27.0-jre、Jackson 2.12.7。这个组合在 Spark 3.3 Hadoop 3.3 Hive 3.1 下实测稳定。组件默认 Guava默认 Jackson对齐后版本Spark 3.3.014.0.12.12.7Guava 27.0-jre / Jackson 2.12.7Hadoop 3.3.127.0-jre2.12.7同上Hive 3.1.319.02.9.2同上注意Hive 的 Jackson 从 2.9 升到 2.12 时ObjectMapper的configure方法行为有细微变化如果项目里有自定义序列化器升级后要回归测试。4. Windows IDEA 特有的坑classpath 顺序与 scope 问题4.1 IDEA 的模块依赖顺序会覆盖 Maven 仲裁这是 Windows 上最容易踩的坑。Maven 仲裁好了版本但 IDEA 在构建 classpath 时模块依赖Module Dependencies的顺序可能和 Maven 不一致。尤其是多模块项目IDEA 会把当前模块的直接依赖排在前面传递依赖排在后面导致实际加载的 jar 和dependency:tree显示的不一样。解决办法在 IDEA 里打开File → Project Structure → Modules → Dependencies把冲突的依赖手动调整顺序或者干脆用 Maven 重新导入。更彻底的做法是在pom.xml里显式声明所有关键依赖让 IDEA 没有自由发挥的空间。我遇到过一次dependency:tree显示 Guava 是 27.0但运行时加载的是 14.0.1。查了半天发现是 IDEA 的 Provided scope 依赖被排在了 compile 依赖前面。在 Windows 上IDEA 对 scope 的处理和 Maven 命令行有差异这点要特别注意。4.2 provided scope 的依赖在本地运行时的处理Spark 项目通常把spark-core、spark-sql声明为provided因为集群上已经有这些 jar。但在 IDEA 本地运行时provided依赖默认不加入运行 classpath除非你勾选 Include dependencies with Provided scope。在 IDEA 的 Run Configuration 里找到 Use classpath of module然后确保勾选了 Include dependencies with Provided scope。否则你会遇到ClassNotFoundException: org.apache.spark.SparkConf这种明明依赖了却找不到的问题。但勾选之后provided依赖里的传递依赖比如 Spark 自带的 Guava 14.0.1也会进来又可能和你的 Guava 27.0 冲突。所以本地运行时最好把 Spark 相关依赖的 scope 临时改成 compile或者用 profile 区分本地和集群。profiles profile idlocal/id dependencies dependency groupIdorg.apache.spark/groupId artifactIdspark-core_2.12/artifactId version3.3.0/version scopecompile/scope /dependency /dependencies /profile /profiles本地跑的时候mvn -Plocal集群打包时用默认 profile干净利落。4.3 Windows 路径分隔符对 classpath 的影响Windows 用分号;分隔 classpathLinux 用冒号:。IDEA 在 Windows 上生成 classpath 时会自动处理但如果你手动拼 classpath比如写脚本启动一定要用File.pathSeparator而不是硬编码。这个坑在跨平台调试时很常见虽然和依赖冲突不直接相关但会导致类找不到的假象浪费排查时间。5. 完整解决流程从报错到验证的每一步5.1 第一步复现并记录完整堆栈不要急着改 pom先把报错完整记录下来。重点看三样东西异常类型NoSuchMethodError通常是版本不兼容ClassNotFoundException通常是缺依赖或 scope 问题NoClassDefFoundError可能是初始化失败。类名和包名com.google.common.base.Preconditions指向 Guavacom.fasterxml.jackson.databind指向 Jackson。方法签名checkArgument(ZLjava/lang/String;Ljava/lang/Object;)V这种签名能精确告诉你需要哪个版本。把堆栈贴到一个文本文件里改一次 pom 就对比一次避免改出新问题还不知道。5.2 第二步用 dependency:tree 定位冲突源mvn dependency:tree -Dverbose deps.txt把输出重定向到文件然后用编辑器搜索guava、jackson。找到所有版本和引入路径后画一张简单的依赖关系图纸上画就行标出每个版本的来源。这一步的目的是确认冲突是多版本共存还是版本过低。5.3 第三步在 dependencyManagement 中锁定版本按照第 3.2 节的写法在顶层 pom 里锁定 Guava 和 Jackson 的版本。如果项目是多模块的把dependencyManagement放在父 pom 里所有子模块继承。5.4 第四步针对性排除无法对齐的传递依赖有些依赖会强制绑定特定版本比如某些老版本的 Hive 会硬编码 Guava 19.0。这时候dependencyManagement可能压不住需要显式 excludedependency groupIdorg.apache.hive/groupId artifactIdhive-exec/artifactId version3.1.3/version exclusions exclusion groupIdcom.google.guava/groupId artifactIdguava/artifactId /exclusion exclusion groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /exclusion /exclusions /dependency排除后让dependencyManagement里的版本生效。5.5 第五步重新导入并验证运行时加载的版本改完 pom 后在 IDEA 里点 Reload All Maven Projects然后重新跑第 2.3 节的ClasspathChecker。确认输出的 jar 路径和版本号符合预期。如果还是旧版本检查 IDEA 的模块依赖顺序或者执行mvn clean清掉缓存。5.6 第六步跑一个最小化的 Spark Hive 读写测试最终验证不能只看类加载要跑一个真实场景。我一般写一个最小的测试类public class SparkHiveSmokeTest { public static void main(String[] args) { SparkSession spark SparkSession.builder() .appName(smoke-test) .master(local[*]) .config(spark.sql.warehouse.dir, file:///C:/tmp/warehouse) .enableHiveSupport() .getOrCreate(); spark.sql(CREATE TABLE IF NOT EXISTS test_table (id INT, name STRING) USING hive); spark.sql(INSERT INTO test_table VALUES (1, a), (2, b)); spark.sql(SELECT * FROM test_table).show(); // 顺便测一下 Jackson 序列化 ObjectMapper mapper new ObjectMapper(); String json mapper.writeValueAsString( java.util.Collections.singletonMap(key, value)); System.out.println(json); spark.stop(); } }这个测试同时覆盖了 Spark、Hive、Jackson 三条链路。如果都能跑通说明依赖冲突基本解决。6. 几个容易忽略的细节和我的实操心得6.1 Spark 的 shaded 依赖是个隐形杀手Spark 从 2.x 开始对部分依赖做了 shade重定位包名比如com.google.common被重定位到org.spark_project.guava。这意味着Spark 内部的 Guava 调用不会和你的 Guava 冲突但如果你直接依赖了 Spark 的某个模块它暴露出来的 API 可能又引用了未重定位的 Guava。这种情况用dependency:tree看不出来因为 shade 后的依赖在 pom 里是provided或者干脆没有。排查方法看 Spark 的spark-corejar 里有没有org/spark_project/guava目录。如果有说明 Spark 自带了重定位的 Guava你不需要为 Spark 的 Guava 冲突操心只需要管好 Hadoop 和 Hive 的。6.2 Jackson 的jackson-module-scala版本必须和jackson-databind一致Spark 用jackson-module-scala来序列化 Scala 对象。这个模块的版本必须和jackson-databind严格对应否则会报NoSuchMethodError。比如jackson-module-scala_2.12:2.12.7必须配jackson-databind:2.12.7差一个小版本都可能出问题。在dependencyManagement里把这两个一起锁死。6.3 Windows 上 Hive 的winutils.exe问题这个和依赖冲突无关但 Windows 上跑 Hive 一定会遇到java.io.IOException: Could not locate executable null\bin\winutils.exe。解决办法是下载对应 Hadoop 版本的winutils.exe放到一个目录下然后设置环境变量HADOOP_HOME指向该目录并把%HADOOP_HOME%\bin加入PATH。IDEA 的 Run Configuration 里也要在 Environment variables 里加上HADOOP_HOME。这个坑不解决Hive 根本起不来会误以为是依赖问题。6.4 用mvn dependency:analyze找出用了但没声明的依赖有时候冲突的根源是项目直接用了某个传递依赖的类但没在 pom 里显式声明。mvn dependency:analyze会列出 Used undeclared dependencies这些依赖的版本完全由传递路径决定最容易出问题。把它们提升为直接依赖并锁定版本能消除一大类隐患。6.5 打包时用 shade 插件重定位冲突依赖如果项目要打成 fat jar 提交到集群而集群环境又无法控制可以用maven-shade-plugin把冲突的 Guava 或 Jackson 重定位到自己的包名下plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goalsgoalshade/goal/goals configuration relocations relocation patterncom.google.common/pattern shadedPatternmyproject.shaded.guava/shadedPattern /relocation /relocations /configuration /execution /executions /plugin这样你的代码用重定位后的 Guava集群上的 Guava 完全不受影响。代价是 jar 变大而且反射相关的代码要额外处理。6.6 一个我踩过的坑IDEA 缓存导致的假解决有一次我改完 pomdependency:tree显示版本已经对齐但运行还是报旧版本的错。折腾了半小时才发现是 IDEA 的缓存没刷新。解决办法File → Invalidate Caches / Restart然后mvn clean install -U强制更新。在 Windows 上IDEA 的缓存问题比 Linux 上更频繁改完依赖后如果行为不符合预期先清缓存再排查。7. 把依赖管理变成习惯而不是救火这套流程走下来你会发现依赖冲突的解决其实不复杂难的是在项目初期就建立版本管理的意识。我现在新起 Spark 项目时第一件事就是在父 pom 里写好dependencyManagement把 Guava、Jackson、Netty、Log4j 这些高频冲突的库全部锁死版本。这样后面加任何依赖都不会突然引入一个不兼容的版本。另外本地环境和集群环境的依赖要分开管理。本地用 profile 把 Spark 依赖改成 compile集群打包时用 provided配合 shade 插件处理冲突。这样两边互不干扰调试效率高很多。最后说一个判断标准如果mvn dependency:tree里同一个 artifact 出现超过一个版本就应该警惕。不是所有多版本都会出问题但出问题的几乎都是多版本。养成定期跑dependency:tree的习惯比出了事再查要省心得多。
阅读完成 · 觉得有帮助?
咨询建站