热部署学习

概述

热部署对于我们这种开发同学来平不陌生吧,比如在IDEA修改一行代码,会自动热部署,并不需要重启,市面上热部署的框架有很多:Jrebel等。今天我就来学习一下热部署。 

原理

我们知道,java程序编译后会生成class文件,在运行时由类加载器对class文件进行加载生成Calss对象,用于创建实例,而实例会执行我们具体的代码逻辑。那么我们似乎可以想象一种实现热部署的方式,【在系统运行的时候,替换class文件,并且让jvm重新加载该class文件,并将实例化的操作由新的Class操作】,这句话看似容易但是有三个关键性的问题要解决:

  • 如何检测到class文件变更
  • 如何让类加载器重新加载calss文件
  • 如何让对新类的实例化采用新的Class

 

1、对于第一个问题,目前java也很好的支持对于文件的监听:比如commons-io包的FileAlterationListenerAdaptorl类。

2、对于第二个问题,首先要明白Java类是通过JVM加载的,其加载模式为双亲委派模型,如图:

在进行类加载时,首先会自底向上挨个检查是否已经加载了指定类,如果已经加载则直接返回该类的引用。如果到最高层也没有加载过指定类,那么会自顶向下挨个尝试加载,直到用户自定义类加载器,如果还不能成功,就会抛出异常。这里要明确的是一个Calss类是由它的类加载器和它的全限定名唯一确定的,一个类加载器只能加载一个同名类,每个类加载器内部会维护一个namespace命名空间,记录该类加载器已经加载的类,同时在Java默认的类加载器层面作判断,如果已经有了该类,则不再重复加载,如果强行绕过判断并使用自定义类加载器重复加载,JVM 将会抛出 LinkageError:attempted duplicate class definition for name。因此想要重新加载一个class文件生成Class对象,要么我们改变类的命名,比如加上版本号,要么就是使用不同的类加载器去加载。根据双亲委派模型,显然我们不能使用相同的父类加载器去加载同名的类,这就需要实现自定义的类加载器,并且每次要使用新的类加载器对象去加载类生成实例对象。

在此我们看下实现一个自定义类加载器需要实现的关键方法,ClassLoader的loadClass方法:

  protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //检查该class是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //如果没有被加载过交给父类加载器去加载
                    if (parent != null) {
                        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;
        }
    }

 

这样解决第二个问题就变得简单了,我们只需要继承ClassLoader并重写loadClass方法实现自定义类加载器,每次使用新的类加载对象按照自定义的类加载方法去加载class文件即可。

3、如何让接下来对修改后的类的实例调用都是用新的Class对象生成,试着分析一下,首先旧的类加载器和Class对象不能再继续使用,我们可以借助jvm的垃圾回收机制释放掉这部分引用,我们要保证每次实例化对象时候使用新生成的Class对象。

【实例化对象】的含义:

在Java中,类的实例化流程分为两个部分:类的加载和类的实例化。
类的加载又分为显式加载和隐式加载,大家使用new关键字创建类实例时,其实就隐式地包含了类的加载过程。
对于类的显式加载来说,比较常见的是使用Class.forName。
其实,它们都是通过调用ClassLoader类的loadClass方法来完成类的实际加载工作的。
直接调用 ClassLoader 的 loadClass 方法是另外一种不常用的显式加载类的技术。

因此要想保证每次实例化对象时候使用新生成的Class对象,最简单的思路就是控制对象实例化的操作,避免使用new的方式去实例化对象,这个想法是不是很熟悉?没错,就是Spring的思路,我们要将控制反转自己控制对象实例化的操作,并维护实例化后的对象引用,参考Spring,我们需要有一个【容器】去维护所有的实例化对象,这样我们使用自己的类加载器加载完新的class文件生成Class对象后,调用newInstance()方法实例化后将其使用【容器】维护起来,系统内所有的调用都使用【容器】内的对象引用即可。

 

总结一下:

  • 如何检测到class文件变更 --> 使用java本身的能力监听文件夹变化
  • 如何让类加载器重新加载calss文件 --> 监听到文件变化后,使用自定义的类加载器去加载class文件
  • 如何让对新类的实例化采用新的Class --> 使用新加载的Class对象生成实例化引用,并自己维护起来,使接下来的调用使用新的实例化对象。

 

注意:上述实现只是热部署的一种实现方式,每一步都可以有不同的实现,比如第一步,监听的文件不一定是class文件,任何形式的二进制字节流都可以,比如可以是JAR、EAR、WAR;Applet;JSP;groovy脚本等,这是虚拟机提供的能力。比如第二步,自定义类加载的实现方法也是千千万万种,第三步的方式也有多种,只要能保证上述效果即可。

posted @ 2023-03-20 11:28  泉水姐姐。  阅读(38)  评论(0编辑  收藏  举报