一 什么是类隔离技术

只要你 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 classPathMap = 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 方法    @Override    public Class> findClass(String name) throws ClassNotFoundException {        String classPath = classPathMap.get(name);        File file = new File(classPath);        if (!file.exists()) {            throw new ClassNotFoundException();        }        byte[] classBytes = getClassData(file);        if (classBytes == null || classBytes.length == 0) {            throw new ClassNotFoundException();        }        return defineClass(classBytes, 0, classBytes.length);    }    private byte[] getClassData(File file) {        try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new                ByteArrayOutputStream()) {            byte[] buffer = new byte[4096];            int bytesNumRead = 0;            while ((bytesNumRead = ins.read(buffer)) != -1) {                baos.write(buffer, 0, bytesNumRead);            }            return baos.toByteArray();        } catch (FileNotFoundException e) {            e.printStackTrace();        } catch (IOException e) {            e.printStackTrace();        }        return new byte[] {};    }}

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

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

执行的结果如下:

TestA: com.java.loader.MyClassLoaderParentFirst@1d44bcfaTestB: sun.misc.Launcher$AppClassLoader@18b4aac2

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

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

委托给父加载器查询

如果父加载器查询不到,就调用 findClass 方法进行加载

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

你可能还好奇,为什么 MyClassLoaderParentFirst 的父加载器是 AppClassLoader?因为我们定义的 main 方法类默认情况下都是由 JDK 自带的 AppClassLoader 加载的,根据类加载传导规则,main 类引用的 MyClassLoaderParentFirst 也是由加载了 main 类的AppClassLoader 来加载。由于 MyClassLoaderParentFirst 的父类是 ClassLoader,ClassLoader 的默认构造方法会自动设置父加载器的值为 AppClassLoader。

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

2 重写 loadClass

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

public class MyClassLoaderCustom extends ClassLoader {    private ClassLoader jdkClassLoader;    private Map classPathMap = new HashMap<>();    public MyClassLoaderCustom(ClassLoader jdkClassLoader) {        this.jdkClassLoader = 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");    }    @Override    protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {        Class result = null;        try {            //这里要使用 JDK 的类加载器加载 java.lang 包里面的类            result = jdkClassLoader.loadClass(name);        } catch (Exception e) {            //忽略        }        if (result != null) {            return result;        }        String classPath = classPathMap.get(name);        File file = new File(classPath);        if (!file.exists()) {            throw new ClassNotFoundException();        }        byte[] classBytes = getClassData(file);        if (classBytes == null || classBytes.length == 0) {            throw new ClassNotFoundException();        }        return defineClass(classBytes, 0, classBytes.length);    }    private byte[] getClassData(File file) { //省略 }}

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

测试代码如下:

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

执行结果如下:

TestA: com.java.loader.MyClassLoaderCustom@1d44bcfaTestB: com.java.loader.MyClassLoaderCustom@1d44bcfa

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

三 总结

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

参考资料

深入探讨 Java 类加载器

(https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html)

关注➕转发 谢谢

java简单通讯录的实现02person类_Java自定义类加载器实现不同版本的类加载相关推荐

  1. java简单通讯录的实现02person类_Java中Math类的简单介绍

    我想对于Math类大家一定很熟悉了,是Java提供的一个用来进行简单数学运算的工具类.对于Math类来说,常用的方法有: 加法 public static int addExact(int x, in ...

  2. java简单通讯录的实现02person类_java实现简单控制台通讯录

    通过主菜单对各级子菜单进行控制,并实现添加记录,查找记录,删除记录,修改记录,排序记录,以及退出系统功能的实现.一共六部分的功能模块. 上面的图就是每个模块具有的功能,而且还用到了正则表达式判断输入的 ...

  3. java简单通讯录的实现02person类_用java实现简单的小游戏(你一定玩过)

    用java实现简单的小游戏(你一定玩过) 对于java初学者来说,通过一些学习小游戏来对swing学习以及对java基础的学习是一个好的方法,同时也给学习带来了很多的乐趣,接下来就给大家分享一个jav ...

  4. java加载自己写的类_java 自定义类加载器从磁盘或网络加载类

    一.编写自定义类加载器类 package com.mybatis.entity; import java.io.ByteArrayOutputStream; import java.io.File; ...

  5. java简单通讯录实现

    java简单通讯录实现 功能点: 添加联系人(联系人:编号,姓名,手机号,QQ,邮箱地址)添加时需要检查手机号和邮箱地址格式是否正确,若不正确,不允许添加. 联系人查询(输入姓名或电话查询) 显示联系 ...

  6. java虚拟机预先加载哪些类_Java虚拟机JVM学习02 类的加载概述

    Java虚拟机JVM学习02 类的加载概述 类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对 ...

  7. java 的方法是静态的类_Java中单例模式和静态方法类的区别

    最近翻看了一些资料,发现JAVA的单例模式并不简单:PHP并没有线程安全的问题,一个请求在结束后生命周期就结束了,PHP设计单例模式仅仅是为了如果在同一个页面多次处理,可以不用重复创建对象而已:JAV ...

  8. java的io流的file类_java IO流 (一) File类的使用

    1.File类的理解 * 1. File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹) * 2. File类声明在java.io包下 * 3. File类中涉及到关于文件或文件目录的创建.删 ...

  9. java反射出抽象类的实现类_java利用反射模式调用实现类

    本文主讲,java利用反射模式调用接口的实现类.抽象类的继承子类.下面请听一一道来 1.第一步在src下创建com.newer.reflex包 2.在com.newer.reflex包下面建立IRef ...

最新文章

  1. 你哪来这么多事(大结局):职工信息删除
  2. 什么是人工神经网络?
  3. php模拟getua_php实现进行远程抓取百度网页内容,并伪装服务器端ip
  4. 【杂谈】如何应对烦人的开源库版本依赖-做一个心平气和的程序员?
  5. Zookeeper3.4.11+Hadoop2.7.6+Hbase2.0.0搭建分布式集群
  6. RocketMQ消息支持的模式-消息同步发送
  7. Java开发技术大杂烩(三)之电商项目优化、rabbitmq、Git、OSI、VIM、Intellj IDEA、HTTP、JS、Java...
  8. HDU2022 海选女主角【入门】
  9. 如何让你的Android SDK下载或者升级快如闪电
  10. spring扩展点四:SmartInitializingSingleton 补充
  11. 基于stc15f2k60s2芯片单片机编程(可调时钟)
  12. android开发地图找房,androidsdk | 百度地图API SDK
  13. 化学计算机模拟计算,计算机化学与分子设计课件.ppt
  14. 【node.js】一个基于HTPP的服务
  15. 手机 app GDPR 合规的9个关键步骤
  16. 2021年高考成绩查询湖北状元,2020年湖北高考状元名单资料,湖北高考状元分数学校名单介绍...
  17. 利用python和tushare,统计股市每天上涨的概率
  18. 如何解除计算机上的安全警报,Win7安全警报怎么关闭?Win7关闭安全警报的方法...
  19. 华为基本配置命令(整理)
  20. Kotlin知识归纳(一) —— 基础语法

热门文章

  1. 【arduino】玩CyberPi童芯派之真点灯,点亮板载RGB灯,GPIO扩展芯片AW9523B驱动
  2. Linux数据报文接收发送总结7
  3. 【Flocking、PPO无人机群控制算法】基于Flocking和PPO深度强化学习的无人机群控制算法的MATLAB仿真
  4. 相移波束形成算法的MATLAB仿真
  5. 什么是OOP(面向对象编程)?
  6. ios自定义日期、时间、城市选择器
  7. python进阶资源整理
  8. Django 学习笔记之七 实现分页
  9. 控件联动(三级联动)
  10. Swift 3.0 beta 6权限访问修改