您好,登錄后才能下訂單哦!
Java中怎么實現(xiàn)熱部署,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。
類加載的探索
首先談一下何為熱部署(hotswap),熱部署是在不重啟 Java 虛擬機的前提下,能自動偵測到 class 文件的變化,更新運行時 class 的行為。Java 類是通過 Java 虛擬機加載的,某個類的 class 文件在被 classloader 加載后,會生成對應(yīng)的 Class 對象,之后就可以創(chuàng)建該類的實例。
默認(rèn)的虛擬機行為只會在啟動時加載類,如果后期有一個類需要更新的話,單純替換編譯的 class 文件,Java 虛擬機是不會更新正在運行的 class。
如果要實現(xiàn)熱部署,最根本的方式是修改虛擬機的源代碼,改變 classloader 的加載行為,使虛擬機能監(jiān)聽 class 文件的更新,重新加載 class 文件,這樣的行為破壞性很大,為后續(xù)的 JVM 升級埋下了一個大坑。
另一種友好的方法是創(chuàng)建自己的 classloader 來加載需要監(jiān)聽的 class,這樣就能控制類加載的時機,從而實現(xiàn)熱部署。本文將具體探索如何實現(xiàn)這個方案。首先需要了解一下 Java 虛擬機現(xiàn)有的加載機制。
目前的加載機制,稱為雙親委派,系統(tǒng)在使用一個 classloader 來加載類時,會先詢問當(dāng)前 classloader 的父類是否有能力加載,如果父類無法實現(xiàn)加載操作,才會將任務(wù)下放到該 classloader 來加載。
這種自上而下的加載方式的好處是,讓每個 classloader 執(zhí)行自己的加載任務(wù),不會重復(fù)加載類。但是這種方式卻使加載順序非常難改變,讓自定義 classloader 搶先加載需要監(jiān)聽改變的類成為了一個難題。
不過我們可以換一個思路,雖然無法搶先加載該類,但是仍然可以用自定義 classloader 創(chuàng)建一個功能相同的類,讓每次實例化的對象都指向這個新的類。當(dāng)這個類的 class 文件發(fā)生改變的時候,再次創(chuàng)建一個更新的類,之后如果系統(tǒng)再次發(fā)出實例化請求,創(chuàng)建的對象講指向這個全新的類。
下面來簡單列舉一下需要做的工作。
創(chuàng)建自定義的 classloader,加載需要監(jiān)聽改變的類,在 class 文件發(fā)生改變的時候,重新加載該類。
改變創(chuàng)建對象的行為,使他們在創(chuàng)建時使用自定義 classloader 加載的 class。
自定義加載器的實現(xiàn)
自定義加載器仍然需要執(zhí)行類加載的功能。這里卻存在一個問題,同一個類加載器無法同時加載兩個相同名稱的類,由于不論類的結(jié)構(gòu)如何發(fā)生變化,生成的類名不會變,而 classloader 只能在虛擬機停止前銷毀已經(jīng)加載的類,這樣 classloader 就無法加載更新后的類了。
這里有一個小技巧,讓每次加載的類都保存成一個帶有版本信息的 class,比如加載 Test.class 時,保存在內(nèi)存中的類是 Test_v1.class,當(dāng)類發(fā)生改變時,重新加載的類名是 Test_v2.class。但是真正執(zhí)行加載 class 文件創(chuàng)建 class 的 defineClass 方法是一個 native 的方法,修改起來又變得很困難。所以面前還剩一條路,那就是直接修改編譯生成的 class 文件。
利用 ASM 修改 class 文件
可以修改字節(jié)碼的框架有很多,比如 ASM,CGLIB。本文使用的是 ASM。先來介紹一下 class 文件的結(jié)構(gòu),class 文件包含了以下幾類信息,一個是類的基本信息,包含了訪問權(quán)限信息,類名信息,父類信息,接口信息。第二個是類的變量信息。第三個是方法的信息。ASM 會先加載一個 class 文件,然后嚴(yán)格順序讀取類的各項信息,用戶可以按照自己的意愿定義增強組件修改這些信息,最后輸出成一個新的 class。
首先看一下如何利用 ASM 修改類信息。
清單 1. 利用 ASM 修改字節(jié)碼
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 修改字節(jié)碼文件的流程是一個責(zé)任鏈模式,首先使用一個 ClassReader 讀入字節(jié)碼,然后利用 ClassVisitor 做個性化的修改,最后利用 ClassWriter 輸出修改后的字節(jié)碼。
之前提過,需要將讀取的 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,作用是修改原始類的類型,將它轉(zhuǎn)換成接口。原始類的所有方法邏輯都會被去掉。
第二步,生成的派生類都實現(xiàn)這個接口,即原始類,并且復(fù)制原始類中的所有方法邏輯。之后如果該類需要更新,會生成一個新的派生類,也會實現(xiàn)這個接口。這樣做的目的是不論如何修改,同一個 class 的派生類都有一個共同的接口,他們之間的轉(zhuǎn)換變得對外不透明。
清單 3. 定義一個派生類
// 在 class 文件發(fā)生改變時重新定義這個類 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,這個增強組件的作用是改變原有類的父類,讓這個修改后的派生類能夠?qū)崿F(xiàn)同一個原始類(此時原始類已經(jīng)轉(zhuǎn)成接口了)。
自定義 classloader 還有一個作用是監(jiān)聽會發(fā)生改變的 class 文件,classloader 會管理一個定時器,定時依次掃描這些 class 文件是否改變。
改變創(chuàng)建對象的行為
Java 虛擬機常見的創(chuàng)建對象的方法有兩種,一種是靜態(tài)創(chuàng)建,直接 new 一個對象,一種是動態(tài)創(chuàng)建,通過反射的方法,創(chuàng)建對象。
由于已經(jīng)在自定義加載器中更改了原有類的類型,把它從類改成了接口,所以這兩種創(chuàng)建方法都無法成立。我們要做的是將實例化原始類的行為變成實例化派生類。
對于第一種方法,需要做的是將靜態(tài)創(chuàng)建,變?yōu)橥ㄟ^ classloader 獲取 class,然后動態(tài)創(chuàng)建該對象。
清單 4. 替換后的指令集所對應(yīng)的邏輯
// 原始邏輯 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); } }
對于第二種創(chuàng)建方法,需要通過修改 Class.forName()和 ClassLoader.findClass()的行為,使他們通過自定義加載器加載類。
使用 JavaAgent 攔截默認(rèn)加載器的行為
之前實現(xiàn)的類加載器已經(jīng)解決了熱部署所需要的功能,可是 JVM 啟動時,并不會用自定義的加載器加載 classpath 下的所有 class 文件,取而代之的是通過應(yīng)用加載器去加載。
如果在其之后用自定義加載器重新加載已經(jīng)加載的 class,有可能會出現(xiàn) LinkageError 的 exception。所以必須在應(yīng)用啟動之前,重新替換已經(jīng)加載的 class。如果在 jdk1.4 之前,能使用的方法只有一種,改變 jdk 中 classloader 的加載行為,使它指向自定義加載器的加載行為。
好在 jdk5.0 之后,我們有了另一種侵略性更小的辦法,這就是 JavaAgent 方法,JavaAgent 可以在 JVM 啟動之后,應(yīng)用啟動之前的短暫間隙,提供空間給用戶做一些特殊行為。比較常見的應(yīng)用,是利用 JavaAgent 做面向方面的編程,在方法間加入監(jiān)控日志等。
JavaAgent 的實現(xiàn)很容易,只要在一個類里面,定義一個 premain 的方法。
清單 6. 一個簡單的 JavaAgent
public class ReloadAgent { public static void premain(String agentArgs, Instrumentation inst){ GeneralTransformer trans = new GeneralTransformer(); inst.addTransformer(trans); } }
然后編寫一個 manifest 文件,將 Premain-Class屬性設(shè)置成定義一個擁有 premain方法的類名即可。
生成一個包含這個 manifest 文件的 jar 包。
manifest-Version: 1.0 Premain-Class: com.example.ReloadAgent Can-Redefine-Classes: true
最后需要在執(zhí)行應(yīng)用的參數(shù)中增加 -javaagent參數(shù) , 加入這個 jar。同時可以為 Javaagent增加參數(shù),下圖中的參數(shù)是測試代碼中 test project 的絕對路徑。這樣在執(zhí)行應(yīng)用的之前,會優(yōu)先執(zhí)行 premain方法中的邏輯,并且預(yù)解析需要加載的 class。
圖 1. 增加執(zhí)行參數(shù)
這里利用 JavaAgent替換原始字節(jié)碼,阻止原始字節(jié)碼被 Java 虛擬機加載。只需要實現(xiàn) 一個 ClassFileTransformer的接口,利用這個實現(xiàn)類完成 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 的結(jié)果吧。
看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進一步的了解或閱讀更多相關(guān)文章,請關(guān)注億速云行業(yè)資訊頻道,感謝您對億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。