MENU

自定义热部署ClassLoader

May 15, 2022 • 学习笔记

自定义一个热部署的ClassLoader

类加载的流程

img

1. 加载

加载阶段(参考java.lang.ClassLoader的loadClass()方法)虚拟机需要完成三件事情

1. 通过类全名来获取一个定义此类的二进制字节流(没有指明一定是从class文件中获取,应该也可以从其他如网络,数据库中数据,动态生成的文件中读取)。
2. 将这个字节流代表的静态存储数据结构转化为动态运行时数据结构。
3. 在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的访问入口。
2. 验证

验证是连接阶段的第一步,这个阶段是为了确保读入的class字节流里面的内容能够符合当前运行的虚拟机的要求,且不会危害虚拟机自身的安全。验证阶段会完成4个检验动作:

1. 文件格式验证:验证字节流是否符合Class文件格式的规范; 例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析), 以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的,如循环、分支等。
4. 符号引用验证:确保解析动作能正确执行,比如不能访问引用类的私有方法、全限定名称是否能找到相关的类。

目的:

1.包括对于文件格式的验证,比如常量中是否有不被支持的常量?文件中是否有不规范的或者附加的其他信息?
2.对于元数据的验证,比如该类是否继承了被final修饰的类?类中的字段,方法是否与父类冲突?是否出现了不合理的重载?
3.对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。
4.对于符号引用的验证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性(private,public等)是否可被当前类访问?
3. 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。

4. 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

5. 初始化

初始化主要分为 类的初始化 和 实例 的初始化

类的初始化:JVM第一次加载class文件时进行,主要是对静态变量初始化语句和静态代码块的执行

实例的初始化:实例创建时才会调用如new, 反射中newInstance()方法,clone()方法,java.io.ObjectInputStream 类的getObject()方法反序列化时。

内置的类加载器为何不能进行热部署

通过源码其实可以看出,JVM并不是一开始就加载所有的类,比如JVM在执行某段代码的时候遇到了一个Class A,先在内存中搜索,没有找到class的信息,这是JVM就会找相应的class文件中去寻找相应的Class A的信息并装入内存中,这就是所谓的类加载过程,只有第一次遇到某个需要运行的类时才会加载,且只会加载一次。这样的话类加载器便不会判断class是否被更改然后进行重新加载。

双亲委派机制

img

为什么叫双亲委派机制

其实这是个翻译的问题,原名是Parents Delegation,其实翻译成父级代理或者上溯委托挺好的,有种迭代的感觉。

(知乎)Java 中的双亲委派的“双”怎么理解 ?

什么是双亲委派机制

JVM中提供了三层的ClassLoader:
Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar。
AppClassLoader:主要负责加载应用程序的主函数类

Java的类加载使用双亲委派模式,即一个类加载器(自定义加载器)在加载类时,先把这个请求委托给自己的父类(AppClassLoder)加载器去执行,如果父类加载器还存在父类加载器(ExtensionClassLoader),就继续向上委托,直到顶层的启动类加载器(BootstrapClassLoader)。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。

总结:先逐级调用最父层的加载器加载,如果没有在逐级调用子类的加载器加载。

双亲委派机制的优势

可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

总结下来就是两点

  1. 避免类的重复加载
  2. 避免java的核心API被篡改

自定义一个热部署ClassLoader

思路
  1. 自定义一个类加载器,破坏双亲委派模式。
  2. 需要有一个监听器,能够监听文件的变动,当文件重新部署后,可以得到通知,然后使用自定义的类加载器去加载它。
一个简易的实现
public class CustomHotFixClassLoader extends ClassLoader {

    public CustomHotFixClassLoader(String bashDir, String classFullNames[]) throws Exception {
        super(null);
        for (String classFullName : classFullNames) {
            StringBuffer filePath = new StringBuffer(bashDir);
            filePath.append(classFullName.replace('.', File.separatorChar));
            filePath.append(".class");
            System.out.println("CustomHotFixClassLoader filePath : " + filePath.toString());
            File classFile = new File(filePath.toString());
            try (FileInputStream fileInputStream = new FileInputStream(classFile)){
                byte[] raw = new byte[(int) classFile.length()];
                fileInputStream.read(raw);
                defineClass(classFullName, raw, 0, raw.length);
            }
        }
    }
//其实重点就是以上部分在defineClass的时候不会判断是否加载过这个class直接覆盖
    
//    @Override
//    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//        Class clz = findLoadedClass(name);
//        if(clz == null)
//            clz = getSystemClassLoader().loadClass(name);
//        if(clz == null)
//            throw new ClassNotFoundException();
//        if (resolve){
//            resolveClass(clz);
//            System.out.println("loadClass() resolve -> true");
//        }
//        return clz;
//    }
}
public void customLoader() throws Exception {
        CustomHotFixClassLoader customHotFixClassLoader =
                new CustomHotFixClassLoader(
                        "D:\\CodeProjects\\IdeaProjects\\Test\\src\\",
                        new String[]{"com.sundae.test.dynamicloadclass.TestClass"});
        Class clz = customHotFixClassLoader.loadClass("com.sundae.test.dynamicloadclass.TestClass");
        Object o = clz.newInstance();
        Method method = clz.getMethod("getString");
        Object result = method.invoke(o);
        System.out.println(result);
}//通过反射调用

可以尝试用个定时任务执行这个方法,在执行过程中改变Test.java中的一些内容,然后javac重新编译它生成class,就会发现功能被动态改变了。

这只是一个演示,设计模式上和效率都有很大的问题。

需要改进的地方
  1. 增加配置文件
  2. 增加文件更改监听器
  3. 改进设计模式(是否可以参考Spring那样对接口自动装载的模式)
思考
  1. 如果class内代码是一个正在执行的重量型的任务是否会出现一些问题。
  2. 关于垃圾回收的问题。