2019.12.10笔记——Spring Boot热部署的使用和实现自己的热部署(类加载器相关)

Spring Boot热部署

热部署的使用

  • 引入依赖
<!-- spring boot热部署的依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 启动项目

在这里插入图片描述

  • 修改代码

在项目运行的过程中我们可以修改程序的代码

  • 编译代码

接着我们需要把修改的代码重新编译,在idea中可以通过下面的操作实现编译,点击下面锤子的按钮
在这里插入图片描述
接着项目就会重新启动,自然就会运用我们修改后的代码
在这里插入图片描述
在这里插入图片描述

热部署机制(原理)

在分析热部署的机制之前我们需要了解一下java的类加载机制,这个可以参考《类加载和JVM性能调优监控工具》这篇文章。

总之我们必须知道两个机制全盘负责委托机制和双亲委派机制,还有java中三种类加载器,BootstrapClassLoader、ExtensionClassLoader和ApplicationClassLoader,还有它们负责加载的类的类型。

现在我们想一想热部署是如何实现的,首先我们修改了java文件,然后将这个java文件编译成了class文件,相当于我们修改了class文件。那么JVM中class文件自然就发生了改变,但是这显然是不行的,因为这些修改的类已经加载到我们的JVM中了,它们的class类对象已经是存在的了。如果我们再需要创建这个类,会通过全盘负责委托机制和双亲委派机制找到可以加载的类加载器,显然这个类加载器是有之前加载过的缓存的,它也不会尝试重新加载,那么我们修改后的class文件其实是没有用到的。

自然无法实现热部署,那么我们如果需要实现热部署,就需要打破全盘负责委托机制和双亲委派机制。那么,问题来了,如何实现呢?

这里先提出一个简单的思路,我们自己实现一个类加载器,然后我们的类通过这个类加载器去加载,并且能够加载到我们修改后的class文件。

如果我们需要实现一个自己的类加载器需要继承一个抽象类ClassLoader,注意它的loadClass方法,它的基本流程如下:

  1. 通过缓存拿到class对象
  2. 如果存在父类加载器调用父类加载器的loadClass方法
  3. 最后依然没有拿到class对象才会自己去加载
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 通过缓存拿到class对象
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                	// 调用父类加载器的loadClass方法
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

这里就体现了双亲委派机制的逻辑,我们所实现的类加载器就必须打破双亲委派机制,因为如果不打破的话我们自己写的代码写的类必然会交给ApplicationClassLoader类加载器去加载,这样的话就实现不了热部署的效果。

所以我们可以重写这个方法的逻辑,还有就是我们可以保证每一次都可以从我们自己的类加载器的缓存中拿到class对象,这样也算是打破了双亲委派机制,这里的实现很多,不过目的都是一致的。

其次就是我们必须解决new对象所使用的类加载器的问题,new对象所使用的类加载器可以通过全盘负责委托机制判断,简单来说就是new关键字在哪里使用的那么new出来的对象就是通过new出来的位置使用的类加载器加载的。

所以我们的自己写的类对象一定要在通过我们自己实现的类加载器加载出来的类中new出来,只有这样才能保证new关键字使用的是我们自己实现的类加载器。

自己实现热部署

基本思路如下:

  1. 通过自己实现的类加载器加载我们自己写的类
  2. 开启一个文件监听
  3. 一旦我们修改文件就通过自己实现的类加载器重新类,刷新缓存
  • 自定义类加载器

下面是自定义的类加载器,我们自己的代码的类就是通过这个类加载器加载的对象,它也间接打破了双亲委派机制,在这个类的构造方法中会将所有我们自己的类加载一遍。

注意这里的类加载首先是扫描项目找到所有的class文件,获得class文件的字节流,然后通过defineClass方法将class文件转换成class对象,并且把生成的class对象放在我们的类加载器的缓存中。

public class MyClassLoader extends ClassLoader {

    //目的 让缓存里面永远能返回一个Class对象 这样就不需要走父类加载器了
    //在构造方法里面加载类  loadClass

    //项目的根路径
    public  String rootPath;

    //所有需要由我这个类加载器加载的类存在这个集合
    public List<String> clazzs;
    //两个classloader  一个是负责加载 需要被热部署的代码的
    //一个是加载系统的一些类的

        //classPaths: 需要被热部署的加载器去加载的目录
    public MyClassLoader(String rootPath,String... classPaths) throws Exception{
        this.rootPath = rootPath;
        this.clazzs = new ArrayList<>();

        for (String classPath : classPaths) {
            scanClassPath(new File(classPath));
        }
    }


    //扫描项目里面传进来的一些class
    public void scanClassPath(File file) throws Exception{
        if (file.isDirectory()){
            for (File file1 : file.listFiles()) {
                scanClassPath(file1);
            }
        }else{
            String fileName = file.getName();
            String filePath = file.getPath();
            String  endName = fileName.substring(fileName.lastIndexOf(".")+1);
            if (endName.equals("class")){
                //现在我们加载到的是一个Class文件
                //如何吧一个Class文件 加载成一个Class对象????
                InputStream inputStream = new FileInputStream(file);
                byte[] bytes = new byte[(int) file.length()];
                inputStream.read(bytes);

                String className = fileNameToClassName(filePath);
                //文件名转类名
                //类名
                 defineClass(className, bytes, 0, bytes.length);
                clazzs.add(className);
                //loadClass 是从当前ClassLoader里面去获取一个Class对象
            }
        }

    }


    public  String fileNameToClassName(String filePath){
        //d: //project//com//luban//xxxx
        String className = filePath.replace(rootPath,"").replaceAll("\\\\",".");
//        com.luban.className.class
        className  =  className.substring(1,className.lastIndexOf("."));
        return className;
        //com.luban.classNamexxxx
    }
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> loadClass = findLoadedClass(name);
        //第一情况 这个类 不需要由我们加载
        //第二种情况 这个类需要由我们加载 但是 确实加载不到
        if (loadClass ==null){
            if (!clazzs.contains(name)){
                loadClass = getSystemClassLoader().loadClass(name);
            }else{
                throw  new ClassNotFoundException("没有加载到类");
            }
        }
        return loadClass;
    }

        //先做热替换  先加载单个Class

        //new Test().xxx

        //当文件被修改的时候再进行热部署

    public static void main(String[] args)  throws Exception{

            //双亲委派机制

        Application.run(MyClassLoader.class);

//        new Test();

        //给一个程序入口

//         while (true){
//             MyClassLoader myClassLoader = new MyClassLoader(rootPath,rootPath+"/com");
//             Class<?> aClass = myClassLoader.loadClass("com.Test");
//             aClass.getMethod("test").invoke(aClass.newInstance());
//             new Test().test();
//
//
//
//             Thread.sleep(2000);
//         }


    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 文件监听器

这里通过引入的依赖使用的文件监听器,依赖如下

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

文件监听器对象,如果我们class文件发生变化,这个监听器会主动常见类加载器对象重新加载我们的类对象。

public class FileListener  extends FileAlterationListenerAdaptor{

    @Override
    public void onFileChange(File file) {
        if (file.getName().indexOf(".class")!= -1){

            try {
                MyClassLoader myClassLoader = new MyClassLoader(Application.rootPath,Application.rootPath+"/com");
                   Application.stop();
                   Application.start0(myClassLoader);

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 程序入口

这是我们的程序入口对象,就是为了解决全盘负责委托机制所带来的影响,因为我们不能直接在main方法中new对象,否则使用的就是ApplicationClassLoader类加载器去加载了,所以我们提供一个类去执行我们的启动代码,并且保证我们的这个类对象使用的是我们自定义的类加载加载的就够了。

public class Application {

    public static String rootPath;

    public  static void run(Class<?> clazz) throws Exception{
        String rootPath = MyClassLoader.class.getResource("/").getPath().replaceAll("%20"," ");
        //   /    \
        rootPath = new File(rootPath).getPath();
        Application.rootPath = rootPath;
        MyClassLoader myClassLoader = new MyClassLoader(rootPath,rootPath+"/com");
        //用我们自己的类加载器加载程序入口
        startFileListener(rootPath);
        start0(myClassLoader);

    }

    public static   void  startFileListener(String rootPath) throws Exception {
        FileAlterationObserver fileAlterationObserver = new FileAlterationObserver(rootPath);
        fileAlterationObserver.addListener(new FileListener());
        FileAlterationMonitor fileAlterationMonitor = new FileAlterationMonitor(500);
        fileAlterationMonitor.addObserver(fileAlterationObserver);
        fileAlterationMonitor.start();

        //要实现文件监听:  写一个线程 去定时监听某个路径下所有的文件
        //如果文件发生改动 就回调监听器

//        fileAlterationMonitor.getInterval()
    }

    //新的classload
    public static void start(){
        System.out.println("启动我们的应用程序");
        //Tomcat tomcat = new Tomcat();

        //Controller ...xxxx
        new Test().test();
    }

    public static void stop(){

//        ApplicationContext.stop;
        System.out.println("程序退出");
        //告诉jvm需要gc了
        System.gc();
        //告诉jvm可以清除对象引用
        System.runFinalization();
    }

//    @Override
//    protected void finalize() throws Throwable {
//        super.finalize();
//    }

    public static void start0(MyClassLoader classLoader) throws Exception {

        Class<?> aClass = classLoader.loadClass("com.Application");

        aClass.getMethod("start").invoke(aClass.newInstance());

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

Spring Boot热部署原理

Spring Boot的热部署原理和我们自己实现的热部署基本相似,有一点区别的就是它借助了spring的事件监听。

基本流程如下:

  1. springboot中也存在一个文件监听器,这个监听器同样会监听我们是否改变了class文件,如果发现我们改变了class文件的内容会发布一个ClassPathChangedEvent事件;
  2. springboot会调用restart方法处理这个事件;
  3. 首先是通过stop方法关掉我们的spring容器,再通知我们的gc垃圾回收;
  4. 接着通过start方法实现项目的重启,通过类加载器重新加载class文件,然后通过反射重新执行我们的main方法。
  • 文件监听器

我们先来看看是如何注入文件监听器的,还是打开热部署依赖下的spring.factories文件,有这么一个类LocalDevToolsAutoConfiguration,文件监听器正是通过此类注入spring容器的。
在这里插入图片描述
下图就是注入的文件监听器,一个FileSystemWatcher对象
在这里插入图片描述
在这里插入图片描述
类中的这个方法正是添加了一个文件监听器
在这里插入图片描述
其中这个监听器的onChange方法会发布一个ClassPathChangedEvent事件
在这里插入图片描述
简单来说就是文件监听器监听到class文件改变事件后会发布一个ClassPathChangedEvent事件

同样还是那个自动配置类中存在这么一个方法会处理文件监听器发布的ClassPathChangedEvent事件,调用restart方法,在这个方法中就会实现热部署的操作。
在这里插入图片描述

  • 类加载器

还是承接上面的restart方法,其中注意这里call方法中的实现
在这里插入图片描述
首先是stop方法

  1. 拿到我们spring的容器对象
  2. 调用spring容器对象的close对象关闭spring
  3. 调用gc方法提醒标记,实现垃圾回收

在这里插入图片描述
接着是start方法
在这里插入图片描述
同样还是逻辑在doStart方法中,主要就是两件事

  1. 通过类加载器加载我们需要的class文件,具体实现可参考我们自己实现的类加载器
  2. 然后就是重新启动我们的应用程序,这个在relaunch方法中实现的

在这里插入图片描述
启动我们的app,这里是通过反射调用我们的main方法的
在这里插入图片描述
在这里插入图片描述

posted @ 2020-10-31 20:16  牧之丨  阅读(522)  评论(0编辑  收藏  举报