附录0-实现Java类的热替换

实现 Java 类的热替换

什么是热替换及其实现原理

  • 热替换是在不停止正在运行的系统的情况下进行类(对象)的升级替换;
  • 这要求虚拟机中要存在同一个类的两个不同版本。可我们知道,我们是无法将同一个类加载两遍的,想要实现这点,我们需要让虚拟机认为这是两个不同的类,即用两个不同的类加载器去加载这个类不同版本的 class 文件;
  • 因此,这个工作就不能由系统提供给我们的启动类加载器,扩展类加载器或者应用程序类加载器来完成,因为这三个类加载器在同一个虚拟机中只有一份,不仅如此,我们还要跳过这些类加载器;
  • 想要跳过这些类加载器可不是只要不用这些类加载器就行了,还需要我们跳过双亲委派模型,否则类的加载还会被委派到这些个类加载器,如果恰好某个类之前是由这三个类加载器中的一个加载的,虚拟机就不会再次加载新版本的类了,就无法实现类的热替换了。

实现简单的 Java 类热替换

需求分析

现有一 Foo 类,可以在控制台持续打印:Hello world! version one,我们将在该类运行时,将其 .class 文件替换为修改后的 Foo 类的 .class 文件,修改后的 Foo 会在控制台持续打印:Hello world! version two。也就是说,替换之后,控制台打印的内容发生变化,就说明类的热替换实现成功。

Foo 类的实现:

public class Foo {
    public void sayHello() {
        System.out.println("Hello world! version one");
        // System.out.println("Hello world! version two");  // 之后替换成这个
    }
}

然后我们通过如下程序运行 Foo 类:

public class Task extends TimerTask {
    @Override
    public void run() {
        String basePath = "C:\\Users\\Bean\\IdeaProjects\\USTJ\\target\\classes";
        // 每执行一次任务都 new 一个新的类加载器
        HotswapClassLoader cl = new HotswapClassLoader(
            basePath, new String[]{"com.jvm.ch7.hotswap.Foo"});
        try {
            // 通过我们自己实现的类加载器加载 Foo 类
            Class cls = cl.loadClass("com.jvm.ch7.hotswap.Foo", true); 
            Object foo = cls.newInstance();
            Method method = cls.getMethod("sayHello", new Class[]{});
            method.invoke(foo, new Object[]{});
        } catch (Exception e) {
        	e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new Task(), 0, 1000);
    }
}

实现类加载器

HotswapClassLoader 的实现如下,具体的讲解已被写入注释中:

public class HotswapClassLoader extends ClassLoader {
    private String basePath;
    private HashSet<String> loadedClass;  // 用来记录被这个类加载器所加载的类

    public HotswapClassLoader(String basePath, String[] classList) {
        // 跳过父类加载器,把它设为null
        super(null);
        this.basePath = basePath;
        loadedClass = new HashSet<>();
        // 该类加载器在初始化的时候会直接把应该它负责加载的类加载好,
        // 这样之后 loadClass 时,会在第一步检验该类是否已经被加载时发现该类已经被加载过了,
        // 就无需执行 loadClass 之后的流程,直接返回虚拟机中被加载好的类即可,
        // 这样虽然初始化的时间长了点,但是之后 loadClass 时会比较省时间
        loadClassByMe(classList);
    }

    /**
     * 加载给定的的 classList 中的类到虚拟机
     */
    private void loadClassByMe(String[] classList) {
        for (int i = 0; i < classList.length; i++) {
            Class cls = loadClassDirectly(classList[i]);
            if (cls != null) {
                loadedClass.add(classList[i]);
            }
        }
    }

    /**
     * 通过文件名直接加载类,得到Class对象
     */
    private Class loadClassDirectly(String className) {
        Class cls = null;
        StringBuilder sb = new StringBuilder(basePath);
        String classPath = className.replace(".", File.separator) + ".class";
        sb.append(File.separator + classPath);
        File file = new File(sb.toString());
        InputStream fin = null;
        try {
            fin = new FileInputStream(file);
            // 将字节流转化成内存中的Class对象
            cls = instantiateClass(className, fin, (int) file.length());
            return cls;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fin != null) {
                try {
                    fin.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 将字节流转化成内存中的Class对象啊,使用defineClass方法!
     */
    private Class instantiateClass(String name, InputStream fin, int len) {
        byte[] buffer = new byte[len];
        try {
            fin.read(buffer);
            return defineClass(name, buffer, 0, len);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fin != null) {
                try {
                    fin.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 覆盖原有的loadClass规则,
     */
    public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class cls = null;
        // 应该由 HotswapClassLoader 负责加载的类会通过下面这一行得到类的 Class 对象,
        // 因为早在 HotswapClassLoader 类加载器执行构造函数时,它们就被加载好了
        cls = findLoadedClass(name);
        // 只有在这个类没有被加载,且!这个类不是当前这个类加载器负责加载的时候,才去使用启动类加载器
        if (cls == null && !loadedClass.contains(name)) {
            cls = findSystemClass(name);
        }
        if (cls == null) {
            throw new ClassNotFoundException(name);
        }
        // resolveClass是进行连接操作的,即"验证+准备+解析",之后就可以进行初始化了
        if (resolve) {
            resolveClass(cls);
        }
        return cls;
    }
}
上一页