位元組碼技術應用場景
AOP技術、Lombok去除重複程式碼外掛、動態修改class檔案等
位元組技術優勢
Java位元組碼增強指的是在Java位元組碼生成之後,對其進行修改,增強其功能,這種方式相當於對應用程式的二進位制檔案進行修改。Java位元組碼增強主要是為了減少冗餘程式碼,提高效能等。
實現位元組碼增強的主要步驟為:
1、修改位元組碼
在記憶體中獲取到原來的位元組碼,然後通過一些工具(如 ASM,Javaasist)來修改它的byte[]陣列,得到一個新的byte陣列。
2、使修改後的位元組碼生效
有兩種方法:
1) 自定義ClassLoader來載入修改後的位元組碼;
2)替換掉原來的位元組碼:在JVM載入使用者的Class時,攔截,返回修改後的位元組碼;或者在執行時,使用Instrumentation.redefineClasses方法來替換掉原來的位元組碼
常見的位元組碼操作類庫
BCEL
Byte Code Engineering Library(BCEL),這是Apache Software Foundation的Jakarta專案的一部分。BCEL是Java classworking 廣泛使用的一種框架,它可以讓您深入jvm組合語言進行類庫操作的細節。BCEL與javassist有不同的處理位元組碼方法,BCEL在實際的jvm指令層次上進行操作(BCEL擁有豐富的jvm指令集支援) 而javassist所強調的是原始碼級別的工作。
ASM
是一個輕量級Java位元組碼操作框架,直接涉及到JVM底層的操作和指令
高效能,高質量
CGLB
生成類庫,基於ASM實現
javassist
是一個開源的分析,編輯和建立Java位元組碼的類庫。效能較ASM差,跟cglib差不多,但是使用簡單。很多開源框架都在使用它。
Javassist優勢
– 比反射開銷小,效能高。
–javassist效能高於反射,低於ASM
執行時操作位元組碼可以讓我們實現如下功能:
– 動態生成 新的類
– 動態改變某個類的結構 ( 新增 / 刪除 / 修改 新的屬性 / 方法 )
javassist 的最外層的 API 和 JAVA 的反射包中的 API 頗為 類似 。
它 主要 由 CtClass , CtMethod, ,以及 CtField 幾個類組成。用以執行和 JDK 反射 API 中 java.lang.Class, java.lang.reflect.Method, java.lang.reflect.Method .Field 相同的 操作 。
方法操作
– 修改已有方法的方法體(插入程式碼到已有方法體)
– 新增方法 刪除方法
javassist的侷限性
JDK5.0 新語法不支援 ( 包括泛型、列舉 ) ,不支援註解修改,但可以通過底層的 javassist 類來解決,具體參考: javassist.bytecode.annotation
不支援陣列的初始化,如 String[]{"1","2"} ,除非只有陣列的容量為 1
不支援內部類和匿名類
不支援 continue 和 break表示式。
對於繼承關係,有些不支援。例如
class A {}
class B extends A {}
class C extends
B {}
使用Javassist建立類
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { Class<?> clazz = Class.forName("com.itmayiedu.Test0005"); Object newInstance = clazz.newInstance(); Method method = clazz.getDeclaredMethod("sum", int.class, int.class); Object invoke = method.invoke(newInstance, 1, 1); } public void sum(int a, int b) { System.out.println("sum:" + a + b); } public static void main(String[] args) throws CannotCompileException, NotFoundException, IOException { ClassPool pool = ClassPool.getDefault(); // 建立class檔案 CtClass userClass = pool.makeClass("com.itmayiedu.entity.User"); // 建立id屬性 CtField idField = CtField.make("private Integer id;", userClass); // 建立name屬性 CtField nameField = CtField.make("private Integer name;", userClass); // 新增屬性 userClass.addField(idField); // 新增屬性 userClass.addField(nameField); // 建立方法 CtMethod getIdMethod = CtMethod.make("public Integer getId() {return id;}", userClass); // 建立方法 CtMethod setIdMethod = CtMethod.make("public void setId(Integer id) { this.id = id; }", userClass); // 新增方法 userClass.addMethod(getIdMethod); // 新增方法 userClass.addMethod(setIdMethod); // 新增構造器 CtConstructor ctConstructor = new CtConstructor(new CtClass[] { CtClass.intType, pool.get("java.lang.String") }, userClass); // 建立Body ctConstructor.setBody(" {this.id = id;this.name = name;}"); userClass.addConstructor(ctConstructor); userClass.writeFile("F:/test");// 將構造好的類寫入到F:\test 目錄下 }
使用Javassist修改類檔案資訊
public static void main(String[] args) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException, IOException { ClassPool pool = ClassPool.getDefault(); // 需要載入類資訊 CtClass userClass = pool.get("com.itmayiedu.User"); // 需要新增的方法 CtMethod m = new CtMethod(CtClass.intType, "add", new CtClass[] { CtClass.intType, CtClass.intType }, userClass); // 方法許可權 m.setModifiers(Modifier.PUBLIC); // 方法體內容 m.setBody("{System.out.println(\"Test003\"); return $1+$2;}"); userClass.addMethod(m); userClass.writeFile("F:/test");// 將構造好的類寫入到F:\test 目錄下 // 使用反射技術執行方法 Class clazz = userClass.toClass(); Object obj = clazz.newInstance(); // 通過呼叫User 無參建構函式 Method method = clazz.getDeclaredMethod("add", int.class, int.class); Object result = method.invoke(obj, 200, 300); System.out.println(result); }
熱部署的原理是什麼
想要知道熱部署的原理,必須要了解java類的載入過程。一個java類檔案到虛擬機器裡的物件,要經過如下過程。
首先通過java編譯器,將java檔案編譯成class位元組碼,類載入器讀取class位元組碼,再將類轉化為例項,對例項newInstance就可以生成物件。
類載入器ClassLoader功能,也就是將class位元組碼轉換到類的例項。
在java應用中,所有的例項都是由類載入器,載入而來。
一般在系統中,類的載入都是由系統自帶的類載入器完成,而且對於同一個全限定名的java類(如com.csiar.soc.HelloWorld),只能被載入一次,而且無法被解除安裝。
這個時候問題就來了,如果我們希望將java類解除安裝,並且替換更新版本的java類,該怎麼做呢?
既然在類載入器中,java類只能被載入一次,並且無法解除安裝。那是不是可以直接把類載入器給換了?答案是可以的,我們可以自定義類載入器,並重寫ClassLoader的findClass方法。想要實現熱部署可以分以下三個步驟:
1、銷燬該自定義ClassLoader
2、更新class類檔案
3、建立新的ClassLoader去載入更新後的class類檔案。
類載入的機制的層次結構
每個編寫的”.java”擴充名類檔案都儲存著需要執行的程式邏輯,這些”.java”檔案經過Java編譯器編譯成擴充名為”.class”的檔案,”.class”檔案中儲存著Java程式碼經轉換後的虛擬機器指令,當需要使用某個類時,虛擬機器將會載入它的”.class”檔案,並建立對應的class物件,將class檔案載入到虛擬機器的記憶體,這個過程稱為類載入,這裡我們需要了解一下類載入的過程,如下:
Jvm執行class檔案
步驟一、類載入機制
將class檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區中的執行時資料結構,在堆中生成一個代表這個類的java.lang.Class物件,作為方法區類資料的訪問入口,這個過程需要類載入器參與。
當系統執行時,類載入器將.class檔案的二進位制資料從外部儲存器(如光碟,硬碟)調入記憶體中,CPU再從記憶體中讀取指令和資料進行運算,並將運算結果存入記憶體中。記憶體在該過程中充當著"二傳手"的作用,通俗的講,如果沒有記憶體,類載入器從外部儲存裝置調入.class檔案二進位制資料直接給CPU處理,而由於CPU的處理速度遠遠大於調入資料的速度,容易造成資料的脫節,所以需要記憶體起緩衝作用。
類將.class檔案載入至執行時的方法區後,會在堆中建立一個Java.lang.Class物件,用來封裝類位於方法區內的資料結構,該Class物件是在載入類的過程中建立的,每個類都對應有一個Class型別的物件,Class類的構造方法是私有的,只有JVM能夠建立。因此Class物件是反射的入口,使用該物件就可以獲得目標類所關聯的.class檔案中具體的資料結構。
類載入的最終產物就是位於堆中的Class物件(注意不是目標類物件),該物件封裝了類在方法區中的資料結構,並且向使用者提供了訪問方法區資料結構的介面,即Java反射的介面。
步驟二、連線過程
將java類的二進位制程式碼合併到JVM的執行狀態之中的過程
驗證:確保載入的類資訊符合JVM規範,沒有安全方面的問題
準備:正式為類變數(static變數)分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配
解析:虛擬機器常量池的符號引用替換為位元組引用過程
步驟三、初始化
初始化階段是執行類構造器<clinit>()方法的過程。類構造器<clinit>()方法是由編譯器自動收藏類中的所有類變數的賦值動作和靜態語句塊(static塊)中的語句合併產生,程式碼從上往下執行。
當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化
虛擬機器會保、、、證一個類的<clinit>()方法在多執行緒環境中被正確加鎖和同步
當範圍一個Java類的靜態域時,只有真正聲名這個域的類才會被初始化
類載入器的層次結構
啟動(Bootstrap)類載入器
擴充套件(Extension)類載入器
系統(-)類載入器
啟動(Bootstrap)類載入器
啟動類載入器主要載入的是JVM自身需要的類,這個類載入使用C++語言實現的,是虛擬機器自身的一部分,它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath引數指定的路徑下的jar包載入到記憶體中,注意必由於虛擬機器是按照檔名識別載入jar包的,如rt.jar,如果檔名不被虛擬機器識別,即使把jar包丟到lib目錄下也是沒有作用的(出於安全考慮,Bootstrap啟動類載入器只載入包名為java、javax、sun等開頭的類)。
擴充套件(Extension)類載入器
擴充套件類載入器是指Sun公司(已被Oracle收購)實現的sun.misc.Launcher$ExtClassLoader類,由Java語言實現的,是Launcher的靜態內部類,它負責載入<JAVA_HOME>/lib/ext目錄下或者由系統變數-Djava.ext.dir指定位路徑中的類庫,開發者可以直接使用標準擴充套件類載入器。
系統(System)類載入器、
也稱應用程式載入器是指 Sun公司實現的sun.misc.Launcher$AppClassLoader。它負責載入系統類路徑java -classpath或-D java.class.path 指定路徑下的類庫,也就是我們經常用到的classpath路徑,開發者可以直接使用系統類載入器,一般情況下該類載入是程式中預設的類載入器,通過ClassLoader#getSystemClassLoader()方法可以獲取到該類載入器。
在Java的日常應用程式開發中,類的載入幾乎是由上述3種類載入器相互配合執行的,在必要時,我們還可以自定義類載入器,需要注意的是,Java虛擬機器對class檔案採用的是按需載入的方式,也就是說當需要使用該類時才會將它的class檔案載入到記憶體生成class物件,而且載入某個類的class檔案時,Java虛擬機器採用的是雙親委派模式即把請求交由父類處理,它一種任務委派模式,下面我們進一步瞭解它。
理解雙親委派模式
採用雙親委派模式的是好處是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,通過這種層級關可以避免類的重複載入,當父親已經載入了該類時,就沒有必要子ClassLoader再載入一次。其次是考慮到安全因素,java核心api中定義型別不會被隨意替換,假設通過網路傳遞一個名為java.lang.Integer的類,通過雙親委託模式傳遞到啟動類載入器,而啟動類載入器在核心Java API發現這個名字的類,發現該類已被載入,並不會重新載入網路傳遞的過來的java.lang.Integer,而直接返回已載入過的Integer.class,這樣便可以防止核心API庫被隨意篡改。可能你會想,如果我們在classpath路徑下自定義一個名為java.lang.SingleInterge類(該類是胡編的)呢?該類並不存在java.lang中,經過雙親委託模式,傳遞到啟動類載入器中,由於父類載入器路徑下並沒有該類,所以不會載入,將反向委託給子類載入器載入,最終會通過系統類載入器載入該類。但是這樣做是不允許,因為java.lang是核心API包,需要訪問許可權,強制載入將會報出如下異常
java.lang.SecurityException: Prohibited package name: java.lang
所以無論如何都無法載入成功的。下面我們從程式碼層面瞭解幾個Java中定義的類載入器及其雙親委派模式的實現,它們類圖關係如下
雙親委派模式是在Java 1.2後引入的,其工作原理的是,如果一個類載入器收到了類載入請求,它並不會自己先去載入,而是把這個請求委託給父類的載入器去執行,如果父類載入器還存在其父類載入器,則進一步向上委託,依次遞迴,請求最終將到達頂層的啟動類載入器,如果父類載入器可以完成類載入任務,就成功返回,倘若父類載入器無法完成此載入任務,子載入器才會嘗試自己去載入,這就是雙親委派模式,即每個兒子都很懶,每次有活就丟給父親去幹,直到父親說這件事我也幹不了時,兒子自己想辦法去完成,這不就是傳說中的實力坑爹啊?那麼採用這種模式有啥用呢?
雙親委派模式優勢
採用雙親委派模式的是好處是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,通過這種層級關可以避免類的重複載入,當父親已經載入了該類時,就沒有必要子ClassLoader再載入一次。其次是考慮到安全因素,java核心api中定義型別不會被隨意替換,假設通過網路傳遞一個名為java.lang.Integer的類,通過雙親委託模式傳遞到啟動類載入器,而啟動類載入器在核心Java API發現這個名字的類,發現該類已被載入,並不會重新載入網路傳遞的過來的java.lang.Integer,而直接返回已載入過的Integer.class,這樣便可以防止核心API庫被隨意篡改。可能你會想,如果我們在classpath路徑下自定義一個名為java.lang.SingleInterge類(該類是胡編的)呢?該類並不存在java.lang中,經過雙親委託模式,傳遞到啟動類載入器中,由於父類載入器路徑下並沒有該類,所以不會載入,將反向委託給子類載入器載入,最終會通過系統類載入器載入該類。但是這樣做是不允許,因為java.lang是核心API包,需要訪問許可權,強制載入將會報出如下異常
類載入器間的關係
我們進一步瞭解類載入器間的關係(並非指繼承關係),主要可以分為以下4點
啟動類載入器,由C++實現,沒有父類。
擴充類載入器(ExtClassLoader),由Java語言實現,父類載入器為null
系統類載入器(AppClassLoader),由Java語言實現,父類載入器為ExtClassLoader
自定義類載入器,父類載入器肯定為AppClassLoader。
類載入器常用方法
loadClass(String)
該方法載入指定名稱(包括包名)的二進位制型別,該方法在JDK1.2之後不再建議使用者重寫但使用者可以直接呼叫該方法,loadClass()方法是ClassLoader類自己實現的,該方法中的邏輯就是雙親委派模式的實現,其原始碼如下,loadClass(String name, boolean resolve)是一個過載方法,resolve引數代表是否生成class物件的同時進行解析相關操作。
正如loadClass方法所展示的,當類載入請求到來時,先從快取中查詢該類物件,如果存在直接返回,如果不存在則交給該類載入去的父載入器去載入,倘若沒有父載入則交給頂級啟動類載入器去載入,最後倘若仍沒有找到,則使用findClass()方法去載入(關於findClass()稍後會進一步介紹)。從loadClass實現也可以知道如果不想重新定義載入類的規則,也沒有複雜的邏輯,只想在執行時載入自己指定的類,那麼我們可以直接使用this.getClass().getClassLoder.loadClass("className"),這樣就可以直接呼叫ClassLoader的loadClass方法獲取到class物件。
findClass(String)
在JDK1.2之前,在自定義類載入時,總會去繼承ClassLoader類並重寫loadClass方法,從而實現自定義的類載入類,但是在JDK1.2之後已不再建議使用者去覆蓋loadClass()方法,而是建議把自定義的類載入邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被呼叫的,當loadClass()方法中父載入器載入失敗後,則會呼叫自己的findClass()方法來完成類載入,這樣就可以保證自定義的類載入器也符合雙親委託模式。需要注意的是ClassLoader類中並沒有實現findClass()方法的具體程式碼邏輯,取而代之的是丟擲ClassNotFoundException異常,同時應該知道的是findClass方法通常是和defineClass方法一起使用的(稍後會分析)
defineClass(byte[] b, int off, int len)
defineClass()方法是用來將byte位元組流解析成JVM能夠識別的Class物件(ClassLoader中已實現該方法邏輯),通過這個方法不僅能夠通過class檔案例項化class物件,也可以通過其他方式例項化class物件,如通過網路接收一個類的位元組碼,然後轉換為byte位元組流建立對應的Class物件,defineClass()方法通常與findClass()方法一起使用,一般情況下,在自定義類載入器時,會直接覆蓋ClassLoader的findClass()方法並編寫載入規則,取得要載入類的位元組碼後轉換成流,然後呼叫defineClass()方法生成類的Class物件
resolveClass(Class≺?≻ c)
使用該方法可以使用類的Class物件建立完成也同時被解析。前面我們說連結階段主要是對位元組碼進行驗證,為類變數分配記憶體並設定初始值同時將位元組碼檔案中的符號引用轉換為直接引用。
熱部署
對於Java應用程式來說,熱部署就是在執行時更新Java類檔案。
熱部署的原理是什麼
想要知道熱部署的原理,必須要了解java類的載入過程。一個java類檔案到虛擬機器裡的物件,要經過如下過程。
首先通過java編譯器,將java檔案編譯成class位元組碼,類載入器讀取class位元組碼,再將類轉化為例項,對例項newInstance就可以生成物件。
類載入器ClassLoader功能,也就是將class位元組碼轉換到類的例項。
在java應用中,所有的例項都是由類載入器,載入而來。
一般在系統中,類的載入都是由系統自帶的類載入器完成,而且對於同一個全限定名的java類(如com.csiar.soc.HelloWorld),只能被載入一次,而且無法被解除安裝。
這個時候問題就來了,如果我們希望將java類解除安裝,並且替換更新版本的java類,該怎麼做呢?
既然在類載入器中,java類只能被載入一次,並且無法解除安裝。那是不是可以直接把類載入器給換了?答案是可以的,我們可以自定義類載入器,並重寫ClassLoader的findClass方法。想要實現熱部署可以分以下三個步驟:
1、銷燬該自定義ClassLoader
2、更新class類檔案
3、建立新的ClassLoader去載入更新後的class類檔案。
熱部署與熱載入
Java熱部署與Java熱載入的聯絡和區別
Java熱部署與熱載入的聯絡
1.不重啟伺服器編譯/部署專案
2.基於Java的類載入器實現
Java熱部署與熱載入的區別
部署方式
熱部署在伺服器執行時重新部署專案
熱載入在執行時重新載入class
實現原理
熱部署直接重新載入整個應用
熱載入在執行時重新載入class
使用場景
熱部署更多的是在生產環境使用
熱載入則更多的實在開發環境使用
相關程式碼
User沒有被修改類
public class User {
public void add() { System.out.println("addV1,沒有修改過..."); }
} |
User更新類
public class User {
public void add() { System.out.println("我把之前的user add方法修改啦!"); }
} |
自定義類載入器
public class MyClassLoader extends ClassLoader {
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { // 檔名稱 String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; // 獲取檔案輸入流 InputStream is = this.getClass().getResourceAsStream(fileName); // 讀取位元組 byte[] b = new byte[is.available()]; is.read(b); // 將byte位元組流解析成jvm能夠識別的Class物件 return defineClass(name, b, 0, b.length); } catch (Exception e) { throw new ClassNotFoundException(); }
}
}
|
更新程式碼
public class Hotswap {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException, InterruptedException { loadUser(); System.gc(); Thread.sleep(1000);// 等待資源回收 // 需要被熱部署的class檔案 File file1 = new File("F:\\test\\User.class"); // 之前編譯好的class檔案 File file2 = new File( "F:\\itmayiedujiangke2018-02-24\\itmayiedu_itmayiedu_day_17\\target\\classes\\com\\itmayiedu\\User.class"); boolean isDelete = file2.delete();// 刪除舊版本的class檔案 if (!isDelete) { System.out.println("熱部署失敗."); return; } file1.renameTo(file2); System.out.println("update success!"); loadUser(); }
public static void loadUser() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException { MyClassLoader myLoader = new MyClassLoader(); Class<?> class1 = myLoader.findClass("com.itmayiedu.User"); Object obj1 = class1.newInstance(); Method method = class1.getMethod("add"); method.invoke(obj1); System.out.println(obj1.getClass()); System.out.println(obj1.getClass().getClassLoader()); } } |