Java插件开发之类加载隔离
背景#
当项目越来越庞大复杂的时候,有时候需要动态引入第三方Jar包,这就导致我们可能会遇到Jar包冲突的问题,如果冲突的jar包是兼容的,程序还能正常执行,但是如果遇到不兼容的情况,那么不管选择哪个版本,都会出问题,导致各种各样的报错,例如 LinkageError, NoSuchMethodError 等.
Jar包模块加载方式#
功能模块化是实现系统能力高可扩展性的常见思路。而模块化又可分为静态模块化和动态模块化两类:
- 静态模块化:指在编译期可以通过引入新的模块扩展系统能力。比如:通过Maven/Gradle引入一个依赖(本质是一组jar文件)。
- 动态模块化:指在JVM运行期可以通过引入新的模块扩展系统能力。比如:利用OSGI系统引入某个bundle(本质是一个jar文件),或者自己利用JDK提供的能力,将某个jar文件中的能力动态加载到运行时环境中。
- 动态模块化主要是用于插件化扩展,例如开发者如果想扩展Solr某一项功能,只需要继承Solr提供的分词接口添加自己的实现,然后把自己的分词jar包拷贝到Solr指定目录,并在solr配置文件中配置,重启即可生效。本篇文章主要介绍动态加载外部Jar包并动态解决Jar包冲突的问题.
如何产生Jar包冲突#
Jar Hell问题引起的原因是当某个ClassLoader的Jar搜索路径中的两个Jar包里存在相同完全限定名的类时,ClassLoader只会从其中一个Jar包中加载该类。其不同版本的实现也使用的是相同的完全限定名。当这些完全限定名相同,但实现不同的Class所在的Jar包被作为第三方依赖同时引入到某个类加载器的Jar搜索路径下时(比如AppClassLoader的搜索路径为ClassPath),依赖冲突就产生了,而且难以解决。例如下图,一个项目引入了外部两个Jar包,A 和 B,但是 A 需要依赖版本号为 0.1 的 C 包,而恰好 B 需要依赖版本号为 0.2 的 C 包,且 C 包的这两个版本无法兼容:
如何解决Jar包冲突#
关于类加载机制以及类加载器的相关知识这里不再赘述,已经有很多大神帮忙总结了,这里重点介绍目前市面上主流解决动态加载Jar包冲突的方法.
- 利用类似于OSGI这样的重框架来解决这类问题,但是这类框架太重太复杂,难以掌握,并且会加重项目的复杂度.
- 利用蚂蚁金服公司开源贡献的SOFAArk,基于 Java 实现的轻量级类隔离容器.
- 自定义类加载器来实现类隔离,例如Tomcat和Flink的实现,自定义类加载器并打破了双亲委派模型.因为Java虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。所以会出现相同类名的实例共存的情况.便达到了类相互隔离的作用.
动态加载Jar包流程#
- 自定义类加载器.
- 将jar文件加载到内存中.
- 自定义ClassLoader将jar文件中的类加载到JVM.
- 通过反射获取到需要调用的类并进行实例化.
- 通过反射获取方法入参对象.
- 通过类的实例对象就可以调用这个Jar中的方法
解决问题的思路:#
- 通过自定义类加载器加载依赖了不兼容的jar及其他依赖的jar,用不同的类加载器实例加载相关的类并创建对象.
- 打破类加载器的双亲委派机制,单独创建出的类加载器要优先自己加载,加载不到则再委派给Parent类加载器进行加载.
- 通过动态监听的方式监听Jar是否被替换从而达到热插拔的效果,这里不做实现,具体可参考Tomcat更新Jsp的方式,每当监听到Jsp文件被修改,便重新加载该Jsp文件.
解决问题#
第一步-编写动态引入的Jar包#
这里使用com.google.guava来模拟Jar包冲突,使用的版本分别为10.0和20.0,其中20.0版本有com.google.common.base.Strings#commonPrefix方法,用于求两个字符串公共前缀,看下图可知是在guava 11.0版本才引入的。也就是说使用10.0版本调用会报出NoSuchMethodError 异常。
可以直接写一个简单的test方法用于测试。
public String test() {
return Strings.commonPrefix("test123456","test789");
}
然后打出Jar包,名字为1.0-SNAPSHOT-all.jar.
第二步-模拟主程序#
这里主程序已经加载了guava 10.0版本的包,里面是不存在Strings#commonPrefix方法的。所以直接使用反射加载Jar包并调用方法。
private static void load() throws NoSuchMethodException, MalformedURLException, InvocationTargetException, IllegalAccessException, ClassNotFoundException, InstantiationException {
File file1 = new File("F:\\develop\\workspace\\1.0-SNAPSHOT-all.jar");
URLClassLoader classloader = new URLClassLoader(new URL[]{file1.toURI().toURL()});
Object o = Class.forName("com.MyTest", true, classloader).newInstance();
Method method = o.getClass().getMethod("test");
Object invoke = method.invoke(o);
System.out.println(invoke);
}
执行后,与预期一样报错
Caused by: java.lang.NoSuchMethodError: 'java.lang.String com.google.common.base.Strings.commonPrefix(java.lang.CharSequence, java.lang.CharSequence)'
第三步-自定义类加载器#
ChildFirstClassLoader.java#
自定义类加载器并破坏双亲委派模型
public class ChildFirstClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
protected ChildFirstClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
/**
* 重写loadClass方法,部分类加载破坏双亲委派模型,(优先加载子类)。
*
* @param name
* @param resolve
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
try {
c = findClass(name);
if (c != null) {
System.out.println("loaded from child, name=" + name);
if (resolve) {
resolveClass(c);
}
return c;
}
} catch (ClassNotFoundException e) {
// Ignore
}
try {
if (getParent() != null) {
c = super.loadClass(name, resolve);
if (c != null) {
System.out.println("loaded from parent, name=" + name);
if (resolve) {
resolveClass(c);
}
return c;
}
}
} catch (ClassNotFoundException e) {
// Ignore
}
try {
c = findSystemClass(name);
if (c != null) {
System.out.println("loaded from system, name=" + name);
if (resolve) {
resolveClass(c);
}
return c;
}
} catch (ClassNotFoundException e) {
// Ignore
}
throw new ClassNotFoundException(name);
}
}
@Override
public URL getResource(String name) {
// first, try and find it via the URLClassloader
URL urlClassLoaderResource = findResource(name);
if (urlClassLoaderResource != null) {
return urlClassLoaderResource;
}
// delegate to super
return super.getResource(name);
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// first get resources from URLClassloader
Enumeration<URL> urlClassLoaderResources = findResources(name);
final List<URL> result = new ArrayList<>();
while (urlClassLoaderResources.hasMoreElements()) {
result.add(urlClassLoaderResources.nextElement());
}
// get parent urls
Enumeration<URL> parentResources = getParent().getResources(name);
while (parentResources.hasMoreElements()) {
result.add(parentResources.nextElement());
}
return new Enumeration<URL>() {
Iterator<URL> iter = result.iterator();
public boolean hasMoreElements() {
return iter.hasNext();
}
public URL nextElement() {
return iter.next();
}
};
}
}
ClassContainer.java#
用于存储需要ChildFirstClassLoader加载的jar包
public class ClassContainer {
private ChildFirstClassLoader childFirstClassLoader;
public ClassContainer() {
}
public ClassContainer(ClassLoader classLoader, String jarPath) {
if (jarPath == null || jarPath.length() == 0) {
return;
}
final URL[] urls = new URL[1];
try {
urls[0] = new File(jarPath).toURI().toURL();
this.childFirstClassLoader = new ChildFirstClassLoader(urls, classLoader);
} catch (MalformedURLException e) {
throw new DelegateCreateException("can not create classloader delegate", e);
}
}
public Class<?> getClass(String name) throws ClassNotFoundException {
return childFirstClassLoader.loadClass(name);
}
public ClassLoader getClassLoader () {
return childFirstClassLoader;
}
}
ThreadContextClassLoaderSwapper.java#
用于切换线程上下文类加载器.因为有些类是使用Thread.currentThread().getContextClassLoader()类加载器来加载,例如java.sql包下的JDBC相关代码,会使用线程上下文类加载器去加载实际的JDBC驱动中的代码.
public class ThreadContextClassLoaderSwapper {
private static final ThreadLocal<ClassLoader> classLoader = new ThreadLocal<>();
// 替换线程上下文类加载器会指定的类加载器,并备份当前的线程上下文类加载器
public static void replace(ClassLoader newClassLoader) {
System.out.println("newClassLoader "+newClassLoader);
System.out.println("Thread.currentThread().getContextClassLoader() "+Thread.currentThread().getContextClassLoader());
classLoader.set(Thread.currentThread().getContextClassLoader());
Thread.currentThread().setContextClassLoader(newClassLoader);
}
// 还原线程上下文类加载器
public static void restore() {
if (classLoader.get() == null) {
return;
}
Thread.currentThread().setContextClassLoader(classLoader.get());
classLoader.set(null);
}
}
第四步用自定义类加载器调用#
private void childFirstClassLoader() throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, InterruptedException {
ClassContainer container = new ClassContainer(getClass().getClassLoader(), "F:\\develop\\workspace\\1.0-SNAPSHOT-all.jar");
ThreadContextClassLoaderSwapper.replace(container.getClassLoader());
Object o = container.getClass("com.MyTest").newInstance();
Method method = o.getClass().getMethod("test");
Object invoke = method.invoke(o);
ThreadContextClassLoaderSwapper.restore();
System.out.println(invoke);
}
运行得出正确结果,而且通过打印的系统日志可以看出自定义的类和依赖的类是由自定义类加载器加载的,做到了类隔离。
总结#
类隔离技术是为了解决依赖冲突而诞生的,它通过自定义类加载器破坏双亲委派机制,然后利用类加载传导规则实现了不同模块的类隔离。
参考#
如何实现Java类隔离加载?
自定义child-first类加载器解决Jar包冲突
利用类加载器解决不兼容的Jar包共存的问题
Java进阶知识点8:高可扩展架构的利器 - 动态模块加载核心技术(ClassLoader、反射、依赖隔离)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)