zoukankan      html  css  js  c++  java
  • Java使用自定义类加载器实现热部署

    // 2020-08-01:之前的代码 findClass 写成 loadClass 了,弄错了。

    热部署:

         热部署就是在不重启应用的情况下,当类的定义即字节码文件修改后,能够替换该Class创建的对象。一般情况下,类的加载都是由系统自带的类加载器完成,且对于同一个全限定名的java类,只能被加载一次,而且无法被卸载。可以使用自定义的 ClassLoader 替换系统的加载器,创建一个新的 ClassLoader,再用它加载 Class,得到的 Class 对象就是新的(因为不是同一个类加载器),再用该 Class 对象创建一个实例,从而实现动态更新。如:修改 JSP 文件即生效,就是利用自定义的 ClassLoader 实现的。

        还需要创建一个守护线程,不断地检查class文件是否被修改过,通过判断文件的上次修改时间实现。

    演示:

    原来的程序:

    修改后重新编译:

    代码:

     dynamic.ClassLoader:

    package dynamic;
    
    import java.io.*;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.util.concurrent.TimeUnit;
    
    public class ClassLoadStudy {
        public static void main(String[] args) throws Exception {
            HotDeploy hot = new HotDeploy("dynamic.Task");
            hot.monitor();
            while (true) {
                TimeUnit.SECONDS.sleep(2);
                hot.getTask().run();
            }
        }
    }
    
    /**
     * 热部署类
     */
    class HotDeploy {
        private static volatile Runnable instance;
        private final String FILE_NAME;
        private final String CLASS_NAME;
    
        public HotDeploy(String name) {
            CLASS_NAME = name; // 类的完全限定名
            name = name.replaceAll("\.", "/") + ".class";
            FILE_NAME = (getClass().getResource("/") + name).substring(6); // 判断class文件修改时间使用,substring(6)去掉开头的file:/
        }
    
        /**
         * 获取一个任务
         * @return
         */
        public Runnable getTask() {
            if (instance == null) { // 双重检查锁,单例,线程安全
                synchronized (this) {
                    if (instance == null) {
                        try {
                            instance = createTask();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
            return instance;
        }
    
        /**
         * 创建一个任务,重新加载 class 文件
         */
        private Runnable createTask() {
            try {
                Class<?> clazz = new MyClassLoader(null).loadClass(CLASS_NAME);
                if (clazz != null)
                    return (Runnable)clazz.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    
    
        /**
         * 监视器,监视class文件是否被修改过,如果是的话,则重新加载
         */
        public void monitor() {
            Thread t = new Thread(()->{
                try {
                    long lastModified = Files.getLastModifiedTime(Path.of(FILE_NAME)).toMillis();
                    while(true) {
                        TimeUnit.SECONDS.sleep(1);
                        long now = Files.getLastModifiedTime(Path.of(FILE_NAME)).toMillis();
                        if(now != lastModified) { // 如果class文件被修改过了
                            System.out.println("yes");
                            lastModified = now;
                            instance = createTask(); // 重新加载
                        }
                    }
                } catch (InterruptedException | IOException e) {
                    e.printStackTrace();
                }
            });
            t.setDaemon(true); // 守护进程
            t.start();
        }
    
    
        /**
         * 自定义类加载器
         */
        private class MyClassLoader extends ClassLoader {
            private MyClassLoader(ClassLoader parent) {
                super(parent);
            }
    
            @Override
            public Class<?> findClass(String name) throws ClassNotFoundException {
                try {
                    byte[] b = Files.readAllBytes(Path.of(FILE_NAME));
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        }
    }
    

      

    动态改变的 Task 类,dynamic.Task:

    package dynamic;
    
    public class Task implements Runnable {
        @Override
        public void run() {
            System.out.print("=> ");
        }
    }
    

      

    遇到的坑:

    刚开始自定义类加载器时,重写的是 loadClass(String name) 方法,但不断地报错,后来明白了,因为 Task 类实现了 Java.lang.Runnable 接口,且重写 loadClass 方法破坏了双亲委派机制,导致了自定义的类加载器去加载 java.lang.Runnable,但被Java安全机制禁止了所以会报错。defineClass调用preDefineClass,preDefineClass 会检查包名,如果以java开头,就会抛出异常,因为让用户自定义的类加载器来加载Java自带的类库会引起混乱。

    于是又重写findClass 方法,但还是不行,findClass方法总是得不到执行,因为编译好的类是在 classpath 下的,而自定义的 ClassLoader 的父加载器是 AppClassLoader,由于双亲委派机制,类就会被 Application ClassLoader来加载了。因此自定义的 findClass 方法就不会被执行。解决方法是,向构造器 ClassLoader(ClassLoader parent) 传入null,或传入 getSystemClassLoader().getParent(),这样就可以保证,目标类被自定义加载器加载,而java.lang.Runnable被BootStrap类加载器加载了。当然,如果被加载的类如果不在classpath下,就不会出现这些问题了。

    还有就是路径问题:

    • path不以 / 开头时,默认是从此类所在的包下取资源;path 以 / 开头时,则是从ClassPath根下获取;

      • URL getClass.getResource(String path)

      • InputStream getClass().getResourceAsStream(String path)

      • getResource("") 返回当前类所在的包的路径

      • getResource("/") 返回当前的 classpath 根据路径

    • path 不能以 / 开始,path 是从 classpath 根开始算的, 因为classloader 不是用户自定义的类,所以没有相对路径的配置文件可以获取,所以默认都是从哪个classpath 路径下读取,自然就没有必要以 / 开头了 。

      • URL Class.getClassLoader().getResource(String path)

      • InputStream Class.getClassLoader().getResourceAsStream(String path)

  • 相关阅读:
    Java 递归、尾递归、非递归、栈 处理 三角数问题
    vmware虚拟机迁移导致的eth0消失问题
    c语言输入输出
    改动虚拟机镜像的rootpassword
    面试题之变态跳台阶
    努力是种病:放慢节奏,才能快速奔跑(转)
    做技术到底可以做到哪种地步-技术为什么越走越窄 (转)
    wpf 客户端【JDAgent桌面助手】开发详解(三) 瀑布流效果实现与UI虚拟化优化大数据显示
    我为什么写博客(转)
    入行必读:互联网行业薪酬等级!看看你值多少钱?(转)
  • 原文地址:https://www.cnblogs.com/yuanyb/p/12066388.html
Copyright © 2011-2022 走看看