1. 程式人生 > >探秘 Java 熱部署

探秘 Java 熱部署

無需重啟 last 是否 指定 CA 什麽 結構 解決 替換

技術分享圖片

# 前言

在之前的 深入淺出 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!!!!!

探秘 Java 熱部署