亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

深入學習Java 熱部署的知識

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

簡介

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

類加載的探索

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

默認的虛擬機行為只會在啟動時加載類,如果后期有一個類需要更新的話,單純替換編譯的 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 來加載類時,會先詢問當前 classloader 的父類是否有能力加載,如果父類無法實現(xiàn)加載操作,才會將任務(wù)下放到該 classloader 來加載。

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

不過我們可以換一個思路,雖然無法搶先加載該類,但是仍然可以用自定義 classloader 創(chuà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,當類發(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 文件,然后嚴格順序讀取類的各項信息,用戶可以按照自己的意愿定義增強組件修改這些信息,最后輸出成一個新的 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é)碼文件的流程是一個責任鏈模式,首先使用一個 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)這個接口,即原始類,并且復制原始類中的所有方法邏輯。之后如果該類需要更新,會生成一個新的派生類,也會實現(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 攔截默認加載器的行為

之前實現(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é)果吧。

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

結(jié)束語

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

以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

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

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

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

    JpaRepository?實現(xiàn)簡單條件查詢

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

    Java 實戰(zhàn)范例之精美網(wǎng)上音樂平臺的實現(xiàn)

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

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

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

    MyBatis-Plus?分頁不生效的解決方法

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

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

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

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

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

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

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

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

    下面小編就為大家?guī)硪黄猨ava修飾類的使用方法以及使用技巧(分享)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-01-01
  • Hadoop 使用IntelliJ IDEA 進行遠程調(diào)試代碼的配置方法

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

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

最新評論