程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 深入探索Java熱部署

深入探索Java熱部署

編輯:關於JAVA

簡介

在 Java 開發領域,熱部署一直是一個難以解決的問題,目前的 Java 虛擬機只能實現 方法體的修改熱部署,對於整個類的結構修改,仍然需要重啟虛擬機,對類重新加載才能完成更新操作。 對於某些大型的應用來說,每次的重啟都需要花費大量的時間成本。雖然 osgi 架構的出現,讓模塊重啟 成為可能,但是如果模塊之間有調用關系的話,這樣的操作依然會讓應用出現短暫的功能性休克。本文將 探索如何在不破壞 Java 虛擬機現有行為的前提下,實現某個單一類的熱部署,讓系統無需重啟就完成某 個類的更新。

類加載的探索

首先談一下何為熱部署(hotswap),熱部署是在不重啟 Java 虛擬機的前提下,能自動偵測到 class 文件的變化,更新運行時 class 的行為。Java 類是通過 Java 虛擬機加載的,某個類的 class 文件在被 classloader 加載後,會生成對應的 Class 對象,之後就可 以創建該類的實例。默認的虛擬機行為只會在啟動時加載類,如果後期有一個類需要更新的話,單純替換 編譯的 class 文件,Java 虛擬機是不會更新正在運行的 class。如果要實現熱部署,最根本的方式是修 改虛擬機的源代碼,改變 classloader 的加載行為,使虛擬機能監聽 class 文件的更新,重新加載 class 文件,這樣的行為破壞性很大,為後續的 JVM 升級埋下了一個大坑。

另一種友好的方法是 創建自己的 classloader 來加載需要監聽的 class,這樣就能控制類加載的時機,從而實現熱部署。本 文將具體探索如何實現這個方案。首先需要了解一下 Java 虛擬機現有的加載機制。目前的加載機制,稱 為雙親委派,系統在使用一個 classloader 來加載類時,會先詢問當前 classloader 的父類是否有能力 加載,如果父類無法實現加載操作,才會將任務下放到該 classloader 來加載。這種自上而下的加載方 式的好處是,讓每個 classloader 執行自己的加載任務,不會重復加載類。但是這種方式卻使加載順序 非常難改變,讓自定義 classloader 搶先加載需要監聽改變的類成為了一個難題。

不過我們可以 換一個思路,雖然無法搶先加載該類,但是仍然可以用自定義 classloader 創建一個功能相同的類,讓 每次實例化的對象都指向這個新的類。當這個類的 class 文件發生改變的時候,再次創建一個更新的類 ,之後如果系統再次發出實例化請求,創建的對象講指向這個全新的類。

下面來簡單列舉一下需 要做的工作。

創建自定義的 classloader,加載需要監聽改變的類,在 class 文件發生改變的時 候,重新加載該類。

改變創建對象的行為,使他們在創建時使用自定義 classloader 加載的 class。

自定義加載器的實現

自定義加載器仍然需要執行類加載的功能。這裡卻存在一個 問題,同一個類加載器無法同時加載兩個相同名稱的類,由於不論類的結構如何發生變化,生成的類名不 會變,而 classloader 只能在虛擬機停止前銷毀已經加載的類,這樣 classloader 就無法加載更新後的 類了。這裡有一個小技巧,讓每次加載的類都保存成一個帶有版本信息的 class,比如加載 Test.class 時,保存在內存中的類是 Test_v1.class,當類發生改變時,重新加載的類名是 Test_v2.class。但是真 正執行加載 class 文件創建 class 的 defineClass 方法是一個 native 的方法,修改起來又變得很困 難。所以面前還剩一條路,那就是直接修改編譯生成的 class 文件。

利用 ASM 修改 class 文件

可以修改字節碼的框架有很多,比如 ASM,CGLIB。本文使用的是 ASM。先來介紹一下 class 文 件的結構,class 文件包含了以下幾類信息,一個是類的基本信息,包含了訪問權限信息,類名信息,父 類信息,接口信息。第二個是類的變量信息。第三個是方法的信息。ASM 會先加載一個 class 文件,然 後嚴格順序讀取類的各項信息,用戶可以按照自己的意願定義增強組件修改這些信息,最後輸出成一個新 的 class。

首先看一下如何利用 ASM 修改類信息。

清單 1. 利用 ASM 修改字節碼

ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
ClassReader cr = null;     
String enhancedClassName = classSource.getEnhancedName(); 
try { 
    cr = new ClassReader(new FileInputStream( 
            classSource.getFile())); 
} catch (IOException e) { 
    e.printStackTrace(); 
    return null; 
} 
ClassVisitor cv = new EnhancedModifier(cw, 
        className.replace(".", "/"), 
        enhancedClassName.replace(".", "/")); 
cr.accept(cv, 0);

ASM 修改字節碼文件的流程是一個責任鏈模式,首先使用一個 ClassReader 讀入字節碼,然後利用 ClassVisitor 做個性化的修改,最後利用 ClassWriter 輸出修改 後的字節碼。

之前提過,需要將讀取的 class 文件的類名做一些修改,加載成一個全新名字的派 生類。這裡將之分為了 2 個步驟。

第一步,先將原來的類變成接口。

清單 2. 重定義的 原始類

   public Class<?> redefineClass(String className){ 
       ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
       ClassReader cr = null; 
       ClassSource cs = classFiles.get(className); 
       if(cs==null){ 
           return null; 
       } 
       try { 
           cr = new ClassReader(new FileInputStream(cs.getFile())); 
       } catch (IOException e) { 
           e.printStackTrace(); 
           return null; 
       } 
       ClassModifier cm = new ClassModifier(cw); 
       cr.accept(cm, 0); 
       byte[] code = cw.toByteArray(); 
       return defineClass(className, code, 0, code.length); 
}

首先 load 原始類的 class 文件,此處定義了一個增強組件 ClassModifier,作用是修改原 始類的類型,將它轉換成接口。原始類的所有方法邏輯都會被去掉。

第二步,生成的派生類都實 現這個接口,即原始類,並且復制原始類中的所有方法邏輯。之後如果該類需要更新,會生成一個新的派 生類,也會實現這個接口。這樣做的目的是不論如何修改,同一個 class 的派生類都有一個共同的接口 ,他們之間的轉換變得對外不透明。

清單 3. 定義一個派生類

   // 在 class 文件發

生改變時重新定義這個類
   private Class<?> redefineClass(String className, ClassSource classSource){ 
       ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
       ClassReader cr = null; 
       classSource.update(); 
       String enhancedClassName = classSource.getEnhancedName();       
       try { 
           cr = new ClassReader( 
                   new FileInputStream(classSource.getFile())); 
       } catch (IOException e) { 
           e.printStackTrace(); 
           return null; 
       } 
       EnhancedModifier em = new EnhancedModifier(cw, className.replace(".", "/"), 
               enhancedClassName.replace(".", "/")); 
       ExtendModifier exm = new ExtendModifier(em, className.replace(".", "/"), 
               enhancedClassName.replace(".", "/")); 
       cr.accept(exm, 0); 
       byte[] code = cw.toByteArray(); 
       classSource.setByteCopy(code); 
       Class<?> clazz = defineClass(enhancedClassName, code, 0, code.length); 
       classSource.setClassCopy(clazz); 
       return clazz; 
}

再次 load 原始類的 class 文件,此處定義了兩個增強組件,一個是 EnhancedModifier,這 個增強組件的作用是改變原有的類名。第二個增強組件是 ExtendModifier,這個增強組件的作用是改變 原有類的父類,讓這個修改後的派生類能夠實現同一個原始類(此時原始類已經轉成接口了)。

自定義 classloader 還有一個作用是監聽會發生改變的 class 文件,classloader 會管理一個定時 器,定時依次掃描這些 class 文件是否改變。

改變創建對象的行為

Java 虛擬機常見的創 建對象的方法有兩種,一種是靜態創建,直接 new 一個對象,一種是動態創建,通過反射的方法,創建 對象。

由於已經在自定義加載器中更改了原有類的類型,把它從類改成了接口,所以這兩種創建 方法都無法成立。我們要做的是將實例化原始類的行為變成實例化派生類。

對於第一種方法,需 要做的是將靜態創建,變為通過 classloader 獲取 class,然後動態創建該對象。

清單 4. 替換 後的指令集所對應的邏輯

// 原始邏輯   
  Greeter p = new Greeter(); 
// 改變後的邏輯
  IGreeter p = (IGreeter)MyClassLoader.getInstance().
  findClass("com.example.Greeter").newInstance();

這裡又需要用到 ASM 來修改 class 文 件了。查找到所有 new 對象的語句,替換成通過 classloader 的形式來獲取對象的形式。

清單 5. 利用 ASM 修改方法體

@Override
public void visitTypeInsn(int opcode, String type) { 
    if(opcode==Opcodes.NEW && type.equals(className)){ 
        List<LocalVariableNode> variables = node.localVariables; 
        String compileType = null; 
        for(int i=0;i<variables.size();i++){ 
            LocalVariableNode localVariable = variables.get(i); 
            compileType = formType(localVariable.desc); 
            if(matchType(compileType)&&!valiableIndexUsed[i]){ 
                valiableIndexUsed[i] = true; 
                break; 
            } 
        } 
    mv.visitMethodInsn(Opcodes.INVOKESTATIC, CLASSLOAD_TYPE, 
        "getInstance", "()L"+CLASSLOAD_TYPE+";"); 
    mv.visitLdcInsn(type.replace("/", ".")); 
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, CLASSLOAD_TYPE, 
        "findClass", "(Ljava/lang/String;)Ljava/lang/Class;"); 
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", 
        "newInstance", "()Ljava/lang/Object;"); 
    mv.visitTypeInsn(Opcodes.CHECKCAST, compileType); 
    flag = true; 
    } else { 
        mv.visitTypeInsn(opcode, type); 
    } 
 }

查看本欄目

對於第二種創建方法,需要通過修改 Class.forName()和 ClassLoader.findClass()的行為 ,使他們通過自定義加載器加載類。使用 JavaAgent 攔截默認加載器的行為

之前實現的類加載器 已經解決了熱部署所需要的功能,可是 JVM 啟動時,並不會用自定義的加載器加載 classpath 下的所有 class 文件,取而代之的是通過應用加載器去加載。如果在其之後用自定義加載器重新加載已經加載的 class,有可能會出現 LinkageError 的 exception。所以必須在應用啟動之前,重新替換已經加載的 class。如果在 jdk1.4 之前,能使用的方法只有一種,改變 jdk 中 classloader 的加載行為,使它指 向自定義加載器的加載行為。好在 jdk5.0 之後,我們有了另一種侵略性更小的辦法,這就是 JavaAgent 方法,JavaAgent 可以在 JVM 啟動之後,應用啟動之前的短暫間隙,提供空間給用戶做一些特殊行為。 比較常見的應用,是利用 JavaAgent 做面向方面的編程,在方法間加入監控日志等。

JavaAgent 的實現很容易,只要在一個類裡面,定義一個 premain 的方法。

清單 6. 一個簡單的 JavaAgent

public class ReloadAgent { 
   public static void premain(String agentArgs, Instrumentation inst){ 
       GeneralTransformer trans = new GeneralTransformer(); 
       inst.addTransformer(trans); 
   } 
}

然後編寫一個 manifest 文件,將 Premain-Class屬性設置成定義一個擁有 premain方法的類 名即可。

生成一個包含這個 manifest 文件的 jar 包。

manifest-Version: 1.0
Premain-Class: com.example.ReloadAgent 
Can-Redefine-Classes: true

最後需要在執行應用的參數中增加 -javaagent參數 , 加入這個 jar。同時可以為 Javaagent增加參數,下圖中的參數是測試代碼中 test project 的絕對路徑。這樣在 執行應用的之前,會優先執行 premain方法中的邏輯,並且預解析需要加載的 class。

圖 1. 增 加執行參數

這裡利用 JavaAgent替換原始字節碼,阻止原始字節碼被 Java 虛擬機加載。只需要實現 一個 ClassFileTransformer的接口,利用這個實現類完成 class 替換的功能。

清單 7. 替換 class

@Override
public byte [] transform(ClassLoader paramClassLoader, String paramString, 
     Class<?> paramClass, ProtectionDomain paramProtectionDomain, 
     byte [] paramArrayOfByte) throws IllegalClassFormatException { 
    String className = paramString.replace("/", "."); 
    if(className.equals("com.example.Test")){ 
        MyClassLoader cl = MyClassLoader.getInstance(); 
        cl.defineReference(className, "com.example.Greeter"); 
        return cl.getByteCode(className); 
    }else if(className.equals("com.example.Greeter")){ 
        MyClassLoader cl = MyClassLoader.getInstance(); 
        cl.redefineClass(className); 
        return cl.getByteCode(className); 
    } 
    return null; 
 }

至此,所有的工作大功告成,欣賞一下 hotswap 的結果吧。

圖 2. Test 執行結果

結束語

解決 hotswap 是個困難的課題,本文解決的僅僅是讓新實例化的對象使用新的邏輯, 並不能改變已經實例化對象的行為,如果 JVM 能夠重新設計 class 的生命周期,支持運行時重新更新一 個 class,hotswap 就會成為 Java 的一個閃亮新特性。官方的 JVM 一直沒有解決熱部署這個問題,可 能也是由於無法完全克服其中的諸多難點,希望未來的 Jdk 能解決這個問題,讓 Java 應用對於更新更 友好,避免不斷重啟應用浪費的時間。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved