EMR 6.6.0(Spark3.2.0)运行Spark-Scala任务报IncompatibleClassChangeError
问题场景
Spark-Scala任务在EMR 5.36.0(Spark 2.4.8、Scala 2.11)环境运行正常,但切换到EMR 6.6.0(Spark 3.2.0、Scala 2.12.15)时触发以下错误:
Exception in thread "main" java.lang.IncompatibleClassChangeError: Class org.json.JSONArray does not implement the requested interface java.lang.Iterable
对应的核心代码片段:
import scala.collection.JavaConverters._ val configJSON: JSONArray = fetchConfigs(configServerUri, configFiles) configJSON.asScala.foreach(propertySource => { propertySource.asInstanceOf[JSONObject].getJSONObject("source").toMap.asScala.foreach(kv => { sparkAppConf.set(configReMap.getOrElse(kv._1, kv._1), kv._2.toString) }) })
pom中依赖的是org.json:json:20220924(该版本JSONArray已实现Iterable<Object>接口),且本地Spark 3.2.0环境运行正常。
问题原因
EMR 6.6.0集群的系统类路径中内置了旧版本的org.json库(如20140107等早期版本),这些版本的JSONArray并未实现Iterable接口。当任务提交到集群时,类加载器优先加载了系统自带的旧版本JSONArray类,而非你打包的新版本类,导致代码调用asScala时触发类不兼容错误。
本地环境正常是因为没有旧版本的org.json包干扰,类加载器直接使用了你依赖的新版本。
解决方法
方法1:用Maven Shade插件重命名org.json包(推荐)
通过重命名你依赖的org.json包,彻底避免和集群自带库的冲突。在pom.xml中添加Shade插件配置:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <relocations> <relocation> <pattern>org.json</pattern> <!-- 替换为你的自定义包名 --> <shadedPattern>com.yourteam.shaded.org.json</shadedPattern> </relocation> </relocations> </configuration> </execution> </executions> </plugin> </plugins> </build>
重新打包后,插件会自动将jar中的org.json类重命名为你指定的包路径,集群类加载器会优先加载这个重命名后的类,不会再和系统自带库冲突。
方法2:强制指定依赖加载顺序
提交Spark任务时,显式指定org.json jar的路径,并配置驱动和Executor优先加载该jar:
spark-submit \ --class com.yourcompany.YourMainClass \ --jars /path/to/json-20220924.jar \ --driver-class-path /path/to/json-20220924.jar \ --conf spark.executor.extraClassPath=json-20220924.jar \ your-application.jar
注意:这种方式依赖集群的类加载顺序配置,稳定性不如Shade插件,若集群有严格的类加载优先级限制可能失效。
方法3:替换为Spark内置的JSON处理工具
Spark 3.x内置了Jackson等JSON解析库,直接使用这些工具可以完全消除外部依赖冲突:
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import scala.collection.JavaConverters._ val mapper = new ObjectMapper() // 假设fetchConfigs返回的是JSON字符串,若不是则先转为字符串 val configJsonNode: JsonNode = mapper.readTree(fetchConfigs(configServerUri, configFiles).toString) if (configJsonNode.isArray) { configJsonNode.elements().asScala.foreach(propertySourceNode => { val sourceNode = propertySourceNode.get("source") sourceNode.fields().asScala.foreach(kv => { val key = configReMap.getOrElse(kv.getKey, kv.getKey) sparkAppConf.set(key, kv.getValue.asText()) }) }) }
这种方式不需要额外引入org.json依赖,直接复用Spark自带的Jackson库,从根源上避免依赖冲突。
内容的提问来源于stack exchange,提问作者Ankit Gupta

