热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

类加载器如何实现类隔离

类加载器如何实现类隔离什么是类隔离技术如何实现类隔离实操1重写findClass重写loadClass总结更多类加载器知识点,可以看一下我之前写过关于类加载器的文章:


类加载器如何实现类隔离

  • 什么是类隔离技术
  • 如何实现类隔离
  • 实操
    • 1 重写 findClass
    • 重写 loadClass
  • 总结



更多类加载器知识点,可以看一下我之前写过关于类加载器的文章:
JVM第六卷—类加载机制
JVM第八卷—类加载与执行子系统的案例与实战




什么是类隔离技术

只要你 Java 代码写的足够多,就一定会出现这种情况:系统新引入了一个中间件的 jar 包,编译的时候一切正常,一运行就报错:java.lang.NoSuchMethodError,然后就哼哧哼哧的开始找解决方法,最后在几百个依赖包里面找的眼睛都快瞎了才找到冲突的 jar,把问题解决之后就开始吐槽中间件为啥搞那么多不同版本的 jar,写代码五分钟,排包排了一整天。

上面这种情况就是 Java 开发过程中常见的情况,原因也很简单,不同 jar 包依赖了某些通用 jar 包(如日志组件)的版本不一样,编译的时候没问题,到了运行时就会因为加载的类跟预期不符合导致报错。举个例子:A 和 B 分别依赖了 C 的 v1 和 v2 版本,v2 版本的 Log 类比 v1 版本新增了 error 方法,现在工程里面同时引入了 A、B 两个 jar 包,以及 C 的 v0.1、v0.2 版本,打包的时候 maven 只能选择一个 C 的版本,假设选择了 v1 版本。到了运行的时候,默认情况下一个项目的所有类都是用同一个类加载器加载的,所以不管你依赖了多少个版本的 C,最终只会有一个版本的 C 被加载到 JVM 中。当 B 要去访问 Log.error,就会发现 Log 压根就没有 error 方法,然后就抛异常java.lang.NoSuchMethodError。这就是类冲突的一个典型案例。
在这里插入图片描述
类冲突的问题如果版本是向下兼容的其实很好解决,把低版本的排除掉就完事了。但要是遇到版本不向下兼容的那就陷入了“救妈妈还是救女朋友”的两难处境了。

为了避免两难选择,有人就提出了类隔离技术来解决类冲突的问题类隔离的原理也很简单,就是让每个模块使用独立的类加载器来加载,这样不同模块之间的依赖就不会互相影响

如下图所示,不同的模块用不同的类加载器加载。为什么这样做就能解决类冲突呢?

这里用到了 Java 的一个机制:不同类加载器加载的类在 JVM 看来是两个不同的类,因为在 JVM 中一个类的唯一标识是 类加载器+类名。通过这种方式我们就能够同时加载 C 的两个不同版本的类,即使它类名是一样的。注意,这里类加载器指的是类加载器的实例,并不是一定要定义两个不同类加载器,例如图中的 PluginClassLoaderA 和 PluginClassLoaderB 可以是同一个类加载器的不同实例。

在这里插入图片描述




如何实现类隔离

前面我们提到类隔离就是让不同模块的 jar 包用不同的类加载器加载,要做到这一点,就需要让 JVM 能够使用自定义的类加载器加载我们写的类以及其关联的类。

那么如何实现呢?

一个很简单的做法就是 JVM 提供一个全局类加载器的设置接口,这样我们直接替换全局类加载器就行了,但是这样无法解决多个自定义类加载器同时存在的问题。

实际上 JVM 提供了一种非常简单有效的方式,我把它称为类加载传导规则:JVM 会选择当前类的类加载器来加载所有该类的引用的类。例如我们定义了 TestA 和 TestB 两个类,TestA 会引用 TestB,只要我们使用自定义的类加载器加载 TestA,那么在运行时,当 TestA 调用到 TestB 的时候,TestB 也会被 JVM 使用 TestA 的类加载器加载

依此类推,只要是 TestA 及其引用类关联的所有 jar 包的类都会被自定义类加载器加载。通过这种方式,我们只要让模块的 main 方法类使用不同的类加载器加载,那么每个模块的都会使用 main 方法类的类加载器加载的,这样就能让多个模块分别使用不同类加载器。这也是 OSGi 和 SofaArk 能够实现类隔离的核心原理。




实操

了解了类隔离的实现原理之后,我们从重写类加载器开始进行实操。要实现自己的类加载器,首先让自定义的类加载器继承 java.lang.ClassLoader,然后重写类加载的方法,这里我们有两个选择,一个是重写 findClass(String name),一个是重写 loadClass(String name)。那么到底应该选择哪个?这两者有什么区别?

下面我们分别尝试重写这两个方法来实现自定义类加载器。


1 重写 findClass

首先我们定义两个类,TestA 会打印自己的类加载器,然后调用 TestB 打印它的类加载器,我们预期是实现重写了 findClass 方法的类加载器 MyClassLoaderParentFirst 能够在加载了 TestA 之后,让 TestB 也自动由 MyClassLoaderParentFirst 来进行加载。

public class TestA {public static void main(String[] args) {TestA testA = new TestA();testA.hello();}public void hello() {System.out.println("TestA: " + this.getClass().getClassLoader());TestB testB = new TestB();testB.hello();}
}
public class TestB {public void hello() {System.out.println("TestB: " + this.getClass().getClassLoader());}
}

然后重写一下 findClass 方法,这个方法先根据文件路径加载 class 文件,然后调用 defineClass 获取 Class 对象。

public class MyClassLoaderParentFirst extends ClassLoader{private Map<String, String> classPathMap &#61; new HashMap<>();public MyClassLoaderParentFirst() {classPathMap.put("com.java.loader.TestA", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestA.class");classPathMap.put("com.java.loader.TestB", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestB.class");}// 重写了 findClass 方法&#64;Overridepublic Class<?> findClass(String name) throws ClassNotFoundException {String classPath &#61; classPathMap.get(name);File file &#61; new File(classPath);if (!file.exists()) {throw new ClassNotFoundException();}byte[] classBytes &#61; getClassData(file);if (classBytes &#61;&#61; null || classBytes.length &#61;&#61; 0) {throw new ClassNotFoundException();}return defineClass(classBytes, 0, classBytes.length);}private byte[] getClassData(File file) {try (InputStream ins &#61; new FileInputStream(file); ByteArrayOutputStream baos &#61; newByteArrayOutputStream()) {byte[] buffer &#61; new byte[4096];int bytesNumRead &#61; 0;while ((bytesNumRead &#61; ins.read(buffer)) !&#61; -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return new byte[] {};}
}

最后写一个 main 方法调用自定义的类加载器加载 TestA&#xff0c;然后通过反射调用 TestA 的 main 方法打印类加载器的信息。

public class MyTest {public static void main(String[] args) throws Exception {MyClassLoaderParentFirst myClassLoaderParentFirst &#61; new MyClassLoaderParentFirst();Class testAClass &#61; myClassLoaderParentFirst.findClass("com.java.loader.TestA");Method mainMethod &#61; testAClass.getDeclaredMethod("main", String[].class);mainMethod.invoke(null, new Object[]{args});}
}

执行的结果如下&#xff1a;

TestA: com.java.loader.MyClassLoaderParentFirst&#64;1d44bcfa
TestB: sun.misc.Launcher$AppClassLoader&#64;18b4aac2

执行的结果并没有如我们期待&#xff0c;TestA 确实是 MyClassLoaderParentFirst 加载的&#xff0c;但是 TestB 还是 AppClassLoader 加载的。这是为什么呢&#xff1f;

要回答这个问题&#xff0c;首先是要了解一个类加载的规则&#xff1a;JVM 在触发类加载时调用的是 ClassLoader.loadClass 方法。这个方法的实现了双亲委派&#xff1a;


  • 委托给父加载器查询
  • 如果父加载器查询不到&#xff0c;就调用 findClass 方法进行加载

明白了这个规则之后&#xff0c;执行的结果的原因就找到了&#xff1a;JVM 确实使用了MyClassLoaderParentFirst 来加载 TestB&#xff0c;但是因为双亲委派的机制&#xff0c;TestB 被委托给了 MyClassLoaderParentFirst 的父加载器 AppClassLoader 进行加载。

你可能还好奇&#xff0c;为什么 MyClassLoaderParentFirst 的父加载器是 AppClassLoader&#xff1f;

因为我们定义的 main 方法类默认情况下都是由 JDK 自带的 AppClassLoader 加载的&#xff0c;根据类加载传导规则&#xff0c;main 类引用的 MyClassLoaderParentFirst 也是由加载了 main 类的AppClassLoader 来加载。由于 MyClassLoaderParentFirst 的父类是 ClassLoader&#xff0c;ClassLoader 的默认构造方法会自动设置父加载器的值为 AppClassLoader。

protected ClassLoader() {this(checkCreateClassLoader(), getSystemClassLoader());
}



重写 loadClass

由于重写 findClass 方法会受到双亲委派机制的影响导致 TestB 被 AppClassLoader 加载&#xff0c;不符合类隔离的目标&#xff0c;所以我们只能重写 loadClass 方法来破坏双亲委派机制。代码如下所示&#xff1a;

public class MyClassLoaderCustom extends ClassLoader {private ClassLoader jdkClassLoader;private Map<String, String> classPathMap &#61; new HashMap<>();public MyClassLoaderCustom(ClassLoader jdkClassLoader) {this.jdkClassLoader &#61; jdkClassLoader;classPathMap.put("com.java.loader.TestA", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestA.class");classPathMap.put("com.java.loader.TestB", "/Users/hansong/IdeaProjects/OhMyJava/CodeRepository/target/classes/com/java/loader/TestB.class");}&#64;Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {Class result &#61; null;try {//这里要使用 JDK 的类加载器加载 java.lang 包里面的类result &#61; jdkClassLoader.loadClass(name);} catch (Exception e) {//忽略}if (result !&#61; null) {return result;}String classPath &#61; classPathMap.get(name);File file &#61; new File(classPath);if (!file.exists()) {throw new ClassNotFoundException();}byte[] classBytes &#61; getClassData(file);if (classBytes &#61;&#61; null || classBytes.length &#61;&#61; 0) {throw new ClassNotFoundException();}return defineClass(classBytes, 0, classBytes.length);}private byte[] getClassData(File file) { //省略 }}

这里注意一点&#xff0c;我们重写了 loadClass 方法也就是意味着所有类包括 java.lang 包里面的类都会通过 MyClassLoaderCustom 进行加载&#xff0c;但类隔离的目标不包括这部分 JDK 自带的类&#xff0c;所以我们用 ExtClassLoader 来加载 JDK 的类&#xff0c;相关的代码就是&#xff1a;result &#61; jdkClassLoader.loadClass(name);

测试代码如下&#xff1a;

public class MyTest {public static void main(String[] args) throws Exception {//这里取AppClassLoader的父加载器也就是ExtClassLoader作为MyClassLoaderCustom的jdkClassLoaderMyClassLoaderCustom myClassLoaderCustom &#61; new MyClassLoaderCustom(Thread.currentThread().getContextClassLoader().getParent());Class testAClass &#61; myClassLoaderCustom.loadClass("com.java.loader.TestA");Method mainMethod &#61; testAClass.getDeclaredMethod("main", String[].class);mainMethod.invoke(null, new Object[]{args});}
}

执行结果如下&#xff1a;

TestA: com.java.loader.MyClassLoaderCustom&#64;1d44bcfa
TestB: com.java.loader.MyClassLoaderCustom&#64;1d44bcfa

可以看到&#xff0c;通过重写了 loadClass 方法&#xff0c;我们成功的让 TestB 也使用MyClassLoaderCustom 加载到了 JVM 中。




总结

类隔离技术是为了解决依赖冲突而诞生的&#xff0c;它通过自定义类加载器破坏双亲委派机制&#xff0c;然后利用类加载传导规则实现了不同模块的类隔离。


推荐阅读
  • 本文讨论了在VMWARE5.1的虚拟服务器Windows Server 2008R2上安装oracle 10g客户端时出现的问题,并提供了解决方法。错误日志显示了异常访问违例,通过分析日志中的问题帧,找到了解决问题的线索。文章详细介绍了解决方法,帮助读者顺利安装oracle 10g客户端。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 个人学习使用:谨慎参考1Client类importcom.thoughtworks.gauge.Step;importcom.thoughtworks.gauge.T ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • Ihavethefollowingonhtml我在html上有以下内容<html><head><scriptsrc..3003_Tes ... [详细]
  • 本文介绍了使用数据库管理员用户执行onstat -l命令来监控GBase8s数据库的物理日志和逻辑日志的使用情况,并强调了对已使用的逻辑日志是否及时备份的重要性。同时提供了监控方法和注意事项。 ... [详细]
  • 超级简单加解密工具的方案和功能
    本文介绍了一个超级简单的加解密工具的方案和功能。该工具可以读取文件头,并根据特定长度进行加密,加密后将加密部分写入源文件。同时,该工具也支持解密操作。加密和解密过程是可逆的。本文还提到了一些相关的功能和使用方法,并给出了Python代码示例。 ... [详细]
  • 微信官方授权及获取OpenId的方法,服务器通过SpringBoot实现
    主要步骤:前端获取到code(wx.login),传入服务器服务器通过参数AppID和AppSecret访问官方接口,获取到OpenId ... [详细]
author-avatar
小葵小小葵_530
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有