• 探秘 Java 热部署


    # 前言

    在之前的 深入浅出 JVM ClassLoader 一文中,我们说可以通过修改默认的类加载器实现热部署,但在 Java 开发领域,热部署一直是一个难以解决的问题,目前的 Java 虚拟机只能实现方法体的修改热部署,对于整个类的结构修改,仍然需要重启虚拟机,对类重新加载才能完成更新操作。对于某些大型的应用来说,每次的重启都需要花费大量的时间成本,所以,如果能像我们之前说的那样,在不重启虚拟机的情况下更新一个类,在某些业务场景下变得十分重要。比如很多脚本语言就支持热替换,例如 PHP,只要替换了PHP 源文件,这种改动就会立即生效,且无需重启服务器。

    今天我们就来一个简单的热部署,注意:不要小看他,这也是 JSP 支持修改的实现方式。

    # 1. 怎么实现?

    在上篇文章中,我们贴了一幅图:

    热替换基本思路

    我们知道,一个类加载器只能加载一个同名类,在Java默认的类加载器层面作了判断,如果已经有了该类,则不再重复加载,如果强行绕过判断并使用自定义类加载器重复加载(比如调用 defineClass 方法),JVM 将会抛出 LinkageError:attempted duplicate class definition for name。

    但请注意,我们说同一个类加载器不可以加载两个同名的类,但不同的类加载器是可以加载同名的类的,加载完成之后,这两个类虽然同名,但不是同一个 Class 对象,无法进行转换。

    那么我们是否可以利用这个特性,实现热部署呢?如同上图的步骤:使用自定义的类加载器,加载一个类,当需要进行替换类的时候,我们就丢弃之前的类加载器和类,使用新的类加载器去加载新的 Class 文件,然后运行新对象的方法。

    让我们按照这个思路写段代码试试吧!

    
     class AccountMain {
    
      public static void main(String[] args)
          throws ClassNotFoundException, InterruptedException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    
        while (true) {
          ClassLoader loader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
              try {
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
    
                InputStream is = getClass().getResourceAsStream(fileName);
                if (is == null) {
                  return super.loadClass(name);
                }
    
                byte[] b = new byte[is.available()];
    
                is.read(b);
                return defineClass(name, b, 0, b.length);
    
              } catch (IOException e) {
                e.printStackTrace();
                throw new ClassNotFoundException(name);
              }
            }
          };
          
          Class clazz = loader.loadClass("cn.think.in.java.clazz.loader.asm.Account");
          Object account = clazz.newInstance();
          account.getClass().getMethod("operation", new Class[]{}).invoke(account);
          Thread.sleep(20000);
        }
      }
    
    }
    

    上面这个类是一个 mian 方法类,该方法是一个间隔 20 秒的死循环,步骤如下:

    1. 创建一个自定义的 ClassLoader 对象,加载类的步骤不遵守双亲委派模型,而是直接加载。
    2. 使用刚刚创建的类加载器加载指定的类。
    3. 得到刚刚的Class 对象,使用反射创建对象,并调用对象的 operation 方法。

    为什么间隔20秒呢?因为我们要在启动之后,修改类,并重新编译。因此需要20秒时间。

    再看看 Account 类:

    public class Account {
      public void operation() {
        System.out.println("operation...");
        try {
          Thread.sleep(10);
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }
    
    
    

    该类很简单,只有一个方法,就是打印 operation...字符串。

    我们还需要一个类,干什么用呢?我们刚刚说,需要修改 Account 类,然后重新编译,为了方便,我们创建一个类,专门用于执行修改后的 Account 类,因为执行后肯定重新编译了,省的我们去命令行使用 javac 了。

    代码如下:

    class ReCompileAccount {
    
      public static void main(String[] args) {
        new Account().operation();
      }
    }
    
    

    如何测试呢?

    1. 启动AccountMain main 方法。会立刻打印出 operation... 字符串,并开始等待20秒。
    2. 修改 Account 类的字符串为 operation.....new,
    3. 启动 ReCompileAccount 类,目的是重新编译 Accoutn类。
    4. 等待 AccountMain 类的打印。

    不出意外的话,最后结果如下:

    打印结果

    看到了吧,我们已经成功的把Accout 类修改了,并且是在不重启 JVM 的情况下,实现了热部署。就像我们刚刚说的,JSP 支持修改也是这么实现的,每一个 JSP 页面都对应着一个类加载器,当JSP 页面被修改了,就重新创建类加载器,然后使用新的类加载器加载 JSP (JSP 其实就是 Java 类)。

    # 总结

    基于 ClassLoader 的原理,我们实现了 Java 层面的热部署,但大家如果自己实现一遍的话,还是觉得很麻烦,诚然,JSP 使用这种方式没什么问题,因为他是自动编译的。但如果我们自己的应用的话,难道每次修改一个类,都要重新编译一遍,然后在给定的时间里去替换?我们能不能把这些手工活都交给 JVM 呢?实际上,Tocmat 也已经通过这种方式实现过了。限于篇幅,我们将在下一篇文章中讲述。

    good luck!!!!!

  • 相关阅读:
    navicat常用快捷键与SQL基本使用
    【Excel实战】公式应用:如何按照某种类型数量排序
    Typora+Markdown便捷发布blog
    【Vulnhub】DC-2靶机
    【转载】阮一峰网络日志中的JWT入门
    【动态规划】闫氏dp分析
    Markdown Latex数学公式
    【每日一题】两个数组的交集
    【每日一题】前k个高频元素
    HashMap的各种遍历和删除方式总结
  • 原文地址:https://www.cnblogs.com/stateis0/p/9062197.html
Copyright © 2020-2023  润新知