探秘 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!!!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM