订阅更新

写Flink UDF遇到ClassNotFoundException——Java类加载机制

Java手册 第 12 题 约 8 分钟读完

本文目录
  1. 先说结论
  2. 双亲委派模型
  3. Flink的ClassLoader——打破双亲委派
  4. 常见的ClassLoader问题和解决方案
  5. 实战踩坑:Flink UDF的ClassNotFoundException
  6. Spark的ClassLoader策略
  7. 面试高频追问
  8. 总结

先说结论

Java的类加载遵循双亲委派模型:先让父ClassLoader加载,父加载不了再自己加载。这保证了核心类(java.lang.String)只被Bootstrap ClassLoader加载一份。但大数据框架(Flink/Spark)打破了双亲委派——用子ClassLoader优先加载用户代码,避免和框架自带的依赖冲突。这也是为什么Flink开发经常遇到ClassNotFoundException——你的类被'错误'的ClassLoader加载了。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

ClassNotFoundException和NoClassDefFoundError是大数据开发最常见的报错之一。在某元宇宙社交公司写Flink UDF时,本地运行一切正常,提交到YARN集群上就报ClassNotFoundException。原因是Flink的ClassLoader和普通Java应用不一样——Flink打破了双亲委派机制,用户代码和框架代码用不同的ClassLoader加载。面试官问类加载,在大数据语境下就是考你对Flink/Spark JAR包依赖冲突和ClassLoader隔离的理解。

双亲委派模型

# Java ClassLoader的层次结构
#
# Bootstrap ClassLoader(启动类加载器)
#   加载:java.lang.*, java.util.* 等核心类
#   路径:JAVA_HOME/lib/rt.jar
#   ↑ 委派
# Extension ClassLoader(扩展类加载器)
#   加载:javax.*, java.security.* 等扩展类
#   路径:JAVA_HOME/lib/ext/
#   ↑ 委派
# Application ClassLoader(应用类加载器)
#   加载:用户代码、第三方JAR
#   路径:classpath
 
# 双亲委派的加载过程:
# 1. 收到加载com.myapp.MyClass的请求
# 2. Application CL先问Extension CL:你能加载吗?
# 3. Extension CL先问Bootstrap CL:你能加载吗?
# 4. Bootstrap CL:不是核心类,我加载不了
# 5. Extension CL:不是扩展类,我也加载不了
# 6. Application CL:那我来加载
#
# 好处:保证核心类不被用户代码覆盖
# java.lang.String永远是Bootstrap加载的那个

Flink的ClassLoader——打破双亲委派

Flink为什么要打破双亲委派?因为Flink框架自身依赖了大量第三方库(Netty、Jackson、Guava等),用户的UDF代码也可能依赖这些库的不同版本。如果都用同一个ClassLoader加载,版本冲突就不可避免。

# Flink的ClassLoader策略
#
# Flink框架类(flink-core等)
#   由Parent ClassLoader加载(系统ClassLoader)
#
# 用户代码(你的UDF JAR)
#   由Child-first ClassLoader加载(默认配置)
#   子ClassLoader优先加载!先找用户JAR再找框架JAR
#
# 配置项:classloader.resolve-order
# child-first(默认):用户代码优先,解决版本冲突
# parent-first:父加载器优先,兼容双亲委派
 
# Child-first的好处:
# 你的UDF依赖了jackson-2.15
# Flink框架自带了jackson-2.12
# Child-first模式下你的UDF会用2.15(用户优先)
# 不会因为框架的旧版本导致兼容问题

常见的ClassLoader问题和解决方案

问题报错原因解决方案
用户类找不到ClassNotFoundExceptionJAR没打进提交包用maven-shade打fat JAR
类冲突NoSuchMethodError框架和用户依赖了不同版本用shade relocate重命名包
序列化类找不到ClassNotFoundException(反序列化)Checkpoint恢复时类路径变了保持JAR版本一致
UDF注册失败ClassNotFoundException(SQL)Flink SQL的ClassLoader隔离在flink-conf.yaml配置

实战踩坑:Flink UDF的ClassNotFoundException

在某元宇宙社交公司,写了一个Flink UDF用来解析JSON(依赖了fastjson)。本地IDE运行正常,提交到YARN集群后报ClassNotFoundException: com.alibaba.fastjson.JSON。

原因:提交到YARN的JAR包是一个thin JAR(只包含自己的代码),fastjson的依赖没有打进去。YARN集群上的Flink lib目录里也没有fastjson。

<!-- 解决方案:用maven-shade-plugin打fat JAR -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <executions>
    <execution>
      <phase>package</phase>
      <goals><goal>shade</goal></goals>
      <configuration>
        <relocations>
          <!-- 如果和Flink自带的依赖冲突,用relocate重命名 -->
          <relocation>
            <pattern>com.google.common</pattern>
            <shadedPattern>my.shaded.com.google.common</shadedPattern>
          </relocation>
        </relocations>
      </configuration>
    </execution>
  </executions>
</plugin>

后续又遇到的问题:fat JAR打好了不报ClassNotFoundException了,但报了NoSuchMethodError——fastjson和Flink自带的某个JSON库有同名类但方法签名不同。最终用shade的relocate把fastjson重命名为my.shaded.fastjson,彻底隔离了类路径。

Spark的ClassLoader策略

# Spark的ClassLoader也有类似的机制
#
# spark.driver.userClassPathFirst = false (默认)
# spark.executor.userClassPathFirst = false (默认)
#
# 设为true:用户代码优先加载(类似Flink的child-first)
# 设为false:父ClassLoader优先(标准双亲委派)
#
# Spark默认不打破双亲委派(和Flink不同)
# 如果遇到依赖冲突,手动设为true
#
# Spark还支持 spark.jars 和 spark.jars.packages
# 动态添加依赖到ClassPath
spark-submit --jars my-udf.jar,my-deps.jar ...
spark-submit --packages com.alibaba:fastjson:1.2.83 ...

面试高频追问

追问一:ClassNotFoundException和NoClassDefFoundError有什么区别?

ClassNotFoundException是受检异常——调用Class.forName()或ClassLoader.loadClass()时类找不到。NoClassDefFoundError是Error——编译时类存在但运行时类找不到(比如JAR包缺失或类初始化失败)。前者通常是ClassPath配置问题,后者通常是JAR依赖缺失或static块抛异常。

追问二:Tomcat/Flink为什么要打破双亲委派?

因为它们需要加载多个应用/作业,每个应用可能依赖同一个库的不同版本。如果用双亲委派,所有应用共享同一个版本,版本冲突不可避免。打破双亲委派后,每个应用用自己的ClassLoader加载自己的依赖,互不影响。这和YARN的Container隔离思想类似——资源隔离。

追问三:怎么查看一个类是被哪个ClassLoader加载的?

MyClass.class.getClassLoader()返回加载这个类的ClassLoader。在Flink中可以在UDF的open()方法里打印ClassLoader信息,帮助排查类加载问题。也可以用-verbose:class JVM参数打印所有类的加载日志。

总结

类加载面试讲清楚三层:① 双亲委派模型(Bootstrap→Extension→Application,先问父再自己加载,保护核心类);② 大数据框架打破双亲委派的原因(避免用户代码和框架依赖的版本冲突,Flink用child-first、Spark可配userClassPathFirst);③ 常见问题的解决(fat JAR打包、shade relocate重命名冲突包)。能讲出Flink UDF从ClassNotFoundException到NoSuchMethodError再到relocate解决的完整排查链,面试官会觉得你有真实的依赖冲突处理经验。

下一篇聊一个基础但面试必考的题:String拼接在循环里为什么慢?String的底层原理。

你们写Flink/Spark UDF遇到过ClassLoader问题吗?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器