欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入學(xué)習(xí)Java 熱部署的知識(shí)

 更新時(shí)間:2019年06月12日 09:29:17   作者:丁 志君  
對(duì)于Java應(yīng)用程序來(lái)說(shuō),熱部署就是在運(yùn)行時(shí)更新Java類文件。在基于Java的應(yīng)用服務(wù)器實(shí)現(xiàn)熱部署的過(guò)程中,類裝入器扮演著重要的角色。大多數(shù)基于Java的應(yīng)用服務(wù)器,包括EJB服務(wù)器和Servlet容器,都支持熱部署。,需要的朋友可以參考下

簡(jiǎn)介

在 Java 開發(fā)領(lǐng)域,熱部署一直是一個(gè)難以解決的問(wèn)題,目前的 Java 虛擬機(jī)只能實(shí)現(xiàn)方法體的修改熱部署,對(duì)于整個(gè)類的結(jié)構(gòu)修改,仍然需要重啟虛擬機(jī),對(duì)類重新加載才能完成更新操作。對(duì)于某些大型的應(yīng)用來(lái)說(shuō),每次的重啟都需要花費(fèi)大量的時(shí)間成本。雖然 osgi 架構(gòu)的出現(xiàn),讓模塊重啟成為可能,但是如果模塊之間有調(diào)用關(guān)系的話,這樣的操作依然會(huì)讓應(yīng)用出現(xiàn)短暫的功能性休克。本文將探索如何在不破壞 Java 虛擬機(jī)現(xiàn)有行為的前提下,實(shí)現(xiàn)某個(gè)單一類的熱部署,讓系統(tǒng)無(wú)需重啟就完成某個(gè)類的更新。

類加載的探索

首先談一下何為熱部署(hotswap),熱部署是在不重啟 Java 虛擬機(jī)的前提下,能自動(dòng)偵測(cè)到 class 文件的變化,更新運(yùn)行時(shí) class 的行為。Java 類是通過(guò) Java 虛擬機(jī)加載的,某個(gè)類的 class 文件在被 classloader 加載后,會(huì)生成對(duì)應(yīng)的 Class 對(duì)象,之后就可以創(chuàng)建該類的實(shí)例。

默認(rèn)的虛擬機(jī)行為只會(huì)在啟動(dòng)時(shí)加載類,如果后期有一個(gè)類需要更新的話,單純替換編譯的 class 文件,Java 虛擬機(jī)是不會(huì)更新正在運(yùn)行的 class。

如果要實(shí)現(xiàn)熱部署,最根本的方式是修改虛擬機(jī)的源代碼,改變 classloader 的加載行為,使虛擬機(jī)能監(jiān)聽 class 文件的更新,重新加載 class 文件,這樣的行為破壞性很大,為后續(xù)的 JVM 升級(jí)埋下了一個(gè)大坑。

另一種友好的方法是創(chuàng)建自己的 classloader 來(lái)加載需要監(jiān)聽的 class,這樣就能控制類加載的時(shí)機(jī),從而實(shí)現(xiàn)熱部署。本文將具體探索如何實(shí)現(xiàn)這個(gè)方案。首先需要了解一下 Java 虛擬機(jī)現(xiàn)有的加載機(jī)制。

目前的加載機(jī)制,稱為雙親委派,系統(tǒng)在使用一個(gè) classloader 來(lái)加載類時(shí),會(huì)先詢問(wèn)當(dāng)前 classloader 的父類是否有能力加載,如果父類無(wú)法實(shí)現(xiàn)加載操作,才會(huì)將任務(wù)下放到該 classloader 來(lái)加載。

這種自上而下的加載方式的好處是,讓每個(gè) classloader 執(zhí)行自己的加載任務(wù),不會(huì)重復(fù)加載類。但是這種方式卻使加載順序非常難改變,讓自定義 classloader 搶先加載需要監(jiān)聽改變的類成為了一個(gè)難題。

不過(guò)我們可以換一個(gè)思路,雖然無(wú)法搶先加載該類,但是仍然可以用自定義 classloader 創(chuàng)建一個(gè)功能相同的類,讓每次實(shí)例化的對(duì)象都指向這個(gè)新的類。當(dāng)這個(gè)類的 class 文件發(fā)生改變的時(shí)候,再次創(chuàng)建一個(gè)更新的類,之后如果系統(tǒng)再次發(fā)出實(shí)例化請(qǐng)求,創(chuàng)建的對(duì)象講指向這個(gè)全新的類。

下面來(lái)簡(jiǎn)單列舉一下需要做的工作。

  • 創(chuàng)建自定義的 classloader,加載需要監(jiān)聽改變的類,在 class 文件發(fā)生改變的時(shí)候,重新加載該類。
  • 改變創(chuàng)建對(duì)象的行為,使他們?cè)趧?chuàng)建時(shí)使用自定義 classloader 加載的 class。

自定義加載器的實(shí)現(xiàn)

自定義加載器仍然需要執(zhí)行類加載的功能。這里卻存在一個(gè)問(wèn)題,同一個(gè)類加載器無(wú)法同時(shí)加載兩個(gè)相同名稱的類,由于不論類的結(jié)構(gòu)如何發(fā)生變化,生成的類名不會(huì)變,而 classloader 只能在虛擬機(jī)停止前銷毀已經(jīng)加載的類,這樣 classloader 就無(wú)法加載更新后的類了。

這里有一個(gè)小技巧,讓每次加載的類都保存成一個(gè)帶有版本信息的 class,比如加載 Test.class 時(shí),保存在內(nèi)存中的類是 Test_v1.class,當(dāng)類發(fā)生改變時(shí),重新加載的類名是 Test_v2.class。但是真正執(zhí)行加載 class 文件創(chuàng)建 class 的 defineClass 方法是一個(gè) native 的方法,修改起來(lái)又變得很困難。所以面前還剩一條路,那就是直接修改編譯生成的 class 文件。

利用 ASM 修改 class 文件

可以修改字節(jié)碼的框架有很多,比如 ASM,CGLIB。本文使用的是 ASM。先來(lái)介紹一下 class 文件的結(jié)構(gòu),class 文件包含了以下幾類信息,一個(gè)是類的基本信息,包含了訪問(wèn)權(quán)限信息,類名信息,父類信息,接口信息。第二個(gè)是類的變量信息。第三個(gè)是方法的信息。ASM 會(huì)先加載一個(gè) class 文件,然后嚴(yán)格順序讀取類的各項(xiàng)信息,用戶可以按照自己的意愿定義增強(qiáng)組件修改這些信息,最后輸出成一個(gè)新的 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é)碼文件的流程是一個(gè)責(zé)任鏈模式,首先使用一個(gè) ClassReader 讀入字節(jié)碼,然后利用 ClassVisitor 做個(gè)性化的修改,最后利用 ClassWriter 輸出修改后的字節(jié)碼。

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

第一步,先將原來(lái)的類變成接口。

清單 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 文件,此處定義了一個(gè)增強(qiáng)組件 ClassModifier,作用是修改原始類的類型,將它轉(zhuǎn)換成接口。原始類的所有方法邏輯都會(huì)被去掉。

第二步,生成的派生類都實(shí)現(xiàn)這個(gè)接口,即原始類,并且復(fù)制原始類中的所有方法邏輯。之后如果該類需要更新,會(huì)生成一個(gè)新的派生類,也會(huì)實(shí)現(xiàn)這個(gè)接口。這樣做的目的是不論如何修改,同一個(gè) class 的派生類都有一個(gè)共同的接口,他們之間的轉(zhuǎn)換變得對(duì)外不透明。

清單 3. 定義一個(gè)派生類

// 在 class 文件發(fā)生改變時(shí)重新定義這個(gè)類
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 文件,此處定義了兩個(gè)增強(qiáng)組件,一個(gè)是 EnhancedModifier,這個(gè)增強(qiáng)組件的作用是改變?cè)械念惷?。第二個(gè)增強(qiáng)組件是 ExtendModifier,這個(gè)增強(qiáng)組件的作用是改變?cè)蓄惖母割?,讓這個(gè)修改后的派生類能夠?qū)崿F(xiàn)同一個(gè)原始類(此時(shí)原始類已經(jīng)轉(zhuǎn)成接口了)。

自定義 classloader 還有一個(gè)作用是監(jiān)聽會(huì)發(fā)生改變的 class 文件,classloader 會(huì)管理一個(gè)定時(shí)器,定時(shí)依次掃描這些 class 文件是否改變。

改變創(chuàng)建對(duì)象的行為

Java 虛擬機(jī)常見的創(chuàng)建對(duì)象的方法有兩種,一種是靜態(tài)創(chuàng)建,直接 new 一個(gè)對(duì)象,一種是動(dòng)態(tài)創(chuàng)建,通過(guò)反射的方法,創(chuàng)建對(duì)象。

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

對(duì)于第一種方法,需要做的是將靜態(tài)創(chuàng)建,變?yōu)橥ㄟ^(guò) classloader 獲取 class,然后動(dòng)態(tài)創(chuàng)建該對(duì)象。

清單 4. 替換后的指令集所對(duì)應(yīng)的邏輯

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

這里又需要用到 ASM 來(lái)修改 class 文件了。查找到所有 new 對(duì)象的語(yǔ)句,替換成通過(guò) classloader 的形式來(lái)獲取對(duì)象的形式。

清單 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); 
} 
}

對(duì)于第二種創(chuàng)建方法,需要通過(guò)修改 Class.forName()和 ClassLoader.findClass()的行為,使他們通過(guò)自定義加載器加載類。

使用 JavaAgent 攔截默認(rèn)加載器的行為

之前實(shí)現(xiàn)的類加載器已經(jīng)解決了熱部署所需要的功能,可是 JVM 啟動(dòng)時(shí),并不會(huì)用自定義的加載器加載 classpath 下的所有 class 文件,取而代之的是通過(guò)應(yīng)用加載器去加載。

如果在其之后用自定義加載器重新加載已經(jīng)加載的 class,有可能會(huì)出現(xiàn) LinkageError 的 exception。所以必須在應(yīng)用啟動(dòng)之前,重新替換已經(jīng)加載的 class。如果在 jdk1.4 之前,能使用的方法只有一種,改變 jdk 中 classloader 的加載行為,使它指向自定義加載器的加載行為。

好在 jdk5.0 之后,我們有了另一種侵略性更小的辦法,這就是 JavaAgent 方法,JavaAgent 可以在 JVM 啟動(dòng)之后,應(yīng)用啟動(dòng)之前的短暫間隙,提供空間給用戶做一些特殊行為。比較常見的應(yīng)用,是利用 JavaAgent 做面向方面的編程,在方法間加入監(jiān)控日志等。

JavaAgent 的實(shí)現(xiàn)很容易,只要在一個(gè)類里面,定義一個(gè) premain 的方法。

清單 6. 一個(gè)簡(jiǎn)單的 JavaAgent

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

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

生成一個(gè)包含這個(gè) manifest 文件的 jar 包。

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

最后需要在執(zhí)行應(yīng)用的參數(shù)中增加 -javaagent參數(shù) , 加入這個(gè) jar。同時(shí)可以為 Javaagent增加參數(shù),下圖中的參數(shù)是測(cè)試代碼中 test project 的絕對(duì)路徑。這樣在執(zhí)行應(yīng)用的之前,會(huì)優(yōu)先執(zhí)行 premain方法中的邏輯,并且預(yù)解析需要加載的 class。

圖 1. 增加執(zhí)行參數(shù)

這里利用 JavaAgent替換原始字節(jié)碼,阻止原始字節(jié)碼被 Java 虛擬機(jī)加載。只需要實(shí)現(xiàn) 一個(gè) ClassFileTransformer的接口,利用這個(gè)實(shí)現(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é)果吧。

圖 2. Test 執(zhí)行結(jié)果

結(jié)束語(yǔ)

解決 hotswap 是個(gè)困難的課題,本文解決的僅僅是讓新實(shí)例化的對(duì)象使用新的邏輯,并不能改變已經(jīng)實(shí)例化對(duì)象的行為,如果 JVM 能夠重新設(shè)計(jì) class 的生命周期,支持運(yùn)行時(shí)重新更新一個(gè) class,hotswap 就會(huì)成為 Java 的一個(gè)閃亮新特性。官方的 JVM 一直沒(méi)有解決熱部署這個(gè)問(wèn)題,可能也是由于無(wú)法完全克服其中的諸多難點(diǎn),希望未來(lái)的 Jdk 能解決這個(gè)問(wèn)題,讓 Java 應(yīng)用對(duì)于更新更友好,避免不斷重啟應(yīng)用浪費(fèi)的時(shí)間。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • springboot 項(xiàng)目容器啟動(dòng)后如何自動(dòng)執(zhí)行指定方法

    springboot 項(xiàng)目容器啟動(dòng)后如何自動(dòng)執(zhí)行指定方法

    這篇文章主要介紹了springboot 項(xiàng)目容器啟動(dòng)后如何自動(dòng)執(zhí)行指定方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • JpaRepository?實(shí)現(xiàn)簡(jiǎn)單條件查詢

    JpaRepository?實(shí)現(xiàn)簡(jiǎn)單條件查詢

    這篇文章主要介紹了JpaRepository?實(shí)現(xiàn)簡(jiǎn)單條件查詢,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • Java 實(shí)戰(zhàn)范例之精美網(wǎng)上音樂(lè)平臺(tái)的實(shí)現(xiàn)

    Java 實(shí)戰(zhàn)范例之精美網(wǎng)上音樂(lè)平臺(tái)的實(shí)現(xiàn)

    讀萬(wàn)卷書不如行萬(wàn)里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+vue+Springboot+ssm+mysql+maven+redis實(shí)現(xiàn)一個(gè)前后端分離的精美網(wǎng)上音樂(lè)平臺(tái),大家可以在過(guò)程中查缺補(bǔ)漏,提升水平
    2021-11-11
  • Java下http下載文件客戶端和上傳文件客戶端實(shí)例代碼

    Java下http下載文件客戶端和上傳文件客戶端實(shí)例代碼

    這篇文章主要介紹了Java下http下載文件客戶端和上傳文件客戶端實(shí)例代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下
    2017-12-12
  • MyBatis-Plus?分頁(yè)不生效的解決方法

    MyBatis-Plus?分頁(yè)不生效的解決方法

    本文主要介紹了MyBatis-Plus?分頁(yè)不生效的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-05-05
  • Spring Boot利用Thymeleaf發(fā)送Email的方法教程

    Spring Boot利用Thymeleaf發(fā)送Email的方法教程

    spring Boot默認(rèn)就是使用thymeleaf模板引擎的,下面這篇文章主要給大家介紹了關(guān)于在Spring Boot中利用Thymeleaf發(fā)送Email的方法教程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。
    2017-08-08
  • Java連接數(shù)據(jù)庫(kù)的步驟介紹

    Java連接數(shù)據(jù)庫(kù)的步驟介紹

    這篇文章介紹了Java連接數(shù)據(jù)庫(kù)的步驟,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-04-04
  • spring注入在有常量的情況下使用@AllArgsConstructor操作

    spring注入在有常量的情況下使用@AllArgsConstructor操作

    這篇文章主要介紹了spring注入在有常量的情況下使用@AllArgsConstructor操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • java修飾類的使用方法以及使用技巧(分享)

    java修飾類的使用方法以及使用技巧(分享)

    下面小編就為大家?guī)?lái)一篇java修飾類的使用方法以及使用技巧(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2017-01-01
  • Hadoop 使用IntelliJ IDEA 進(jìn)行遠(yuǎn)程調(diào)試代碼的配置方法

    Hadoop 使用IntelliJ IDEA 進(jìn)行遠(yuǎn)程調(diào)試代碼的配置方法

    這篇文章主要介紹了Hadoop 使用IntelliJ IDEA 進(jìn)行遠(yuǎn)程調(diào)試代碼的配置方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-04-04

最新評(píng)論