JVM类加载器深度解析
[toc]
# 引言
因为项目历史包袱和兼容性原因,近期重构的项目无法简单地通过修改依赖版本解决问题。结合问题根因和解决思路,笔者最终通过类覆盖的方式解决了问题,所以借此机会来简单聊聊Java的类加载机制。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# 问题说明
首先笔者接手的项目包含以下两个依赖:
- flink-streaming-java_2.11
- flink-connector-oracle-cdc
第一个jar包中的源代码需要引用Guava工具包,对应的Guava内置版本为18.x,而第二个jar包内部的Guava版本为30.x。由于依赖加载顺序的问题,笔者构建打包的项目最终jar包包含:
- flink-streaming-java_2.11
- flink-connector-oracle-cdc
- flink-shaded-guava 30.x
这就导致第一个jar包依赖的Guava 18.x版本的类在运行时找不到,使得项目在启动时抛出了java.lang.NoClassDefFoundError异常:

# 详解不同的解决方案和类加载机制
# 魔改源码
基于此类问题,我们的解决思路很简单,即让两个都需要Guava的jar包都依赖于同一个版本的Guava。而要解决这个问题,就必须修改jar包的源代码。
对应修改源代码的方式有两种,第一种则是直接下载flink-streaming-java_2.11在GitHub上的源代码并执行如下步骤:
- 修改内部guava版本
- 将所有类的依赖改为该版本
- 打包到本地
- 开发项目从本地拉取魔改后的jar包

# 基于类覆盖
上述方案可能相对麻烦一些,对于java类加载机制比较熟悉的读者可能都知道,Java类加载有着严格的双亲委派机制,即发起一个类加载请求时,该请求会不断向上委托,直到最顶层的类加载器,然后从最顶层的Bootstrap类加载器开始尝试加载,若加载失败则依次向下传递尝试。
这也就是为什么我们重写相同包相同名称的String类没有用,因为String的类加载请求永远都会直接委托给最上级的Bootstrap类加载器加载,只要它完成加载,后续的其他类加载器就不会再尝试加载同名类:

同理,在这类加载过程中也有这样一种情况,即类加载优先从根加载器开始,若没有则在当前类路径(classpath)中加载,如果没有再从第三方的jar包中加载这个类。所以要想解决我们项目的问题也很简单,即在当前项目中创建同包同名的类,并将依赖改为使用最新版本。
以笔者本次的工程为例,对应的落地步骤为:
- 创建
DebeziumSourceFunction.java,包路径与flink-streaming-java_2.11中该类保持一致:org.apache.flink.streaming.connectors.jdbc; - 修改该文件中的Guava依赖import语句,改为引用Guava 30.x版本
此时项目启动时就会优先加载我们的覆盖后的类:

这一点,我们也可以通过反编译打包后的jar包印证版本的修改情况:

注意这种方法相比前者简单一些,但涉及版本兼容性、维护困难等潜在风险,读者需要考虑到这一点。
# 小结
本文基于一个实际的兼容性项目问题,深入探讨了JVM的类加载机制和类覆盖解决方案的工作原理,希望对读者有所帮助。
我是 SharkChili ,Java 开发者,Java Guide 开源项目维护者。欢迎关注我的公众号:写代码的SharkChili,也欢迎您了解我的开源项目 mini-redis:https://github.com/shark-ctrl/mini-redis。
为方便与读者交流,现已创建读者群。关注上方公众号获取我的联系方式,添加时备注加群即可加入。
# 参考
Flink 编译 1.14 版本的 cdc connector: https://www.cnblogs.com/Springmoon-venn/p/15951496.html
在IDEA本地开发时Flink CDC和Flink的guava版本冲突解决办法: https://avoid.overfit.cn/post/98881147c77648d6941da2002a15103c
Java 如何覆盖第三方 jar 包中的类: https://jishuzhan.net/article/1887414024333692929