AOP技術、Lombok去除重複代碼插件、動態修改class文件等java
Java字節碼加強指的是在Java字節碼生成以後,對其進行修改,加強其功能,這種方式至關於對應用程序的二進制文件進行修改。Java字節碼加強主要是爲了減小冗餘代碼,提升性能等。api
實現字節碼加強的主要步驟爲:數組
1、修改字節碼緩存
在內存中獲取到原來的字節碼,而後經過一些工具(如 ASM,Javaasist)來修改它的byte[]數組,獲得一個新的byte數組。安全
2、使修改後的字節碼生效服務器
有兩種方法:網絡
1) 自定義ClassLoader來加載修改後的字節碼;數據結構
2)替換掉原來的字節碼:在JVM加載用戶的Class時,攔截,返回修改後的字節碼;或者在運行時,使用Instrumentation.redefineClasses方法來替換掉原來的字節碼多線程
Byte Code Engineering Library(BCEL),這是Apache Software Foundation的Jakarta項目的一部分。BCEL是Java classworking 普遍使用的一種框架,它可讓您深刻jvm彙編語言進行類庫操做的細節。BCEL與javassist有不一樣的處理字節碼方法,BCEL在實際的jvm指令層次上進行操做(BCEL擁有豐富的jvm指令集支持) 而javassist所強調的是源代碼級別的工做。框架
是一個輕量級Java字節碼操做框架,直接涉及到JVM底層的操做和指令
高性能,高質量
生成類庫,基於ASM實現
是一個開源的分析,編輯和建立Java字節碼的類庫。性能較ASM差,跟cglib差很少,可是使用簡單。不少開源框架都在使用它。
– 比反射開銷小,性能高。
–javassist性能高於反射,低於ASM
運行時操做字節碼可讓咱們實現以下功能:
– 動態生成 新的類
– 動態改變某個類的結構 ( 添加 / 刪除 / 修改 新的屬性 / 方法 )
javassist 的最外層的 API 和 JAVA 的反射包中的 API 頗爲 相似 。
它 主要 由 CtClass , CtMethod, ,以及 CtField 幾個類組成。用以執行和 JDK 反射 API 中 java.lang.Class, java.lang.reflect.Method, java.lang.reflect.Method .Field 相同的 操做 。
方法操做
– 修改已有方法的方法體(插入代碼到已有方法體)
– 新增方法 刪除方法
JDK5.0 新語法不支持 ( 包括泛型、枚舉 ) ,不支持註解修改,但能夠經過底層的 javassist 類來解決,具體參考: javassist.bytecode.annotation
不支持數組的初始化,如 String[]{"1","2"} ,除非只有數組的容量爲 1
不支持內部類和匿名類
不支持 continue 和 break表達式。
對於繼承關係,有些不支持。例如
class A {}
class B extends A {}
class C extends B {}
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 目錄下 }
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方法。想要實現熱部署能夠分如下三個步驟:
一、銷燬該自定義ClassLoader
二、更新class類文件
三、建立新的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)類加載器
系統(-)類加載器
啓動類加載器主要加載的是JVM自身須要的類,這個類加載使用C++語言實現的,是虛擬機自身的一部分,它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數指定的路徑下的jar包加載到內存中,注意必因爲虛擬機是按照文件名識別加載jar包的,如rt.jar,若是文件名不被虛擬機識別,即便把jar包丟到lib目錄下也是沒有做用的(出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類)。
擴展類加載器是指Sun公司(已被Oracle收購)實現的sun.misc.Launcher$ExtClassLoader類,由Java語言實現的,是Launcher的靜態內部類,它負責加載<JAVA_HOME>/lib/ext目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫,開發者能夠直接使用標準擴展類加載器。
也稱應用程序加載器是指 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。
該方法加載指定名稱(包括包名)的二進制類型,該方法在JDK1.2以後再也不建議用戶重寫但用戶能夠直接調用該方法,loadClass()方法是ClassLoader類本身實現的,該方法中的邏輯就是雙親委派模式的實現,其源碼以下,loadClass(String name, boolean resolve)是一個重載方法,resolve參數表明是否生成class對象的同時進行解析相關操做。
正如loadClass方法所展現的,當類加載請求到來時,先從緩存中查找該類對象,若是存在直接返回,若是不存在則交給該類加載去的父加載器去加載,假若沒有父加載則交給頂級啓動類加載器去加載,最後假若仍沒有找到,則使用findClass()方法去加載(關於findClass()稍後會進一步介紹)。從loadClass實現也能夠知道若是不想從新定義加載類的規則,也沒有複雜的邏輯,只想在運行時加載本身指定的類,那麼咱們能夠直接使用this.getClass().getClassLoder.loadClass("className"),這樣就能夠直接調用ClassLoader的loadClass方法獲取到class對象。
在JDK1.2以前,在自定義類加載時,總會去繼承ClassLoader類並重寫loadClass方法,從而實現自定義的類加載類,可是在JDK1.2以後已再也不建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中,從前面的分析可知,findClass()方法是在loadClass()方法中被調用的,當loadClass()方法中父加載器加載失敗後,則會調用本身的findClass()方法來完成類加載,這樣就能夠保證自定義的類加載器也符合雙親委託模式。須要注意的是ClassLoader類中並無實現findClass()方法的具體代碼邏輯,取而代之的是拋出ClassNotFoundException異常,同時應該知道的是findClass方法一般是和defineClass方法一塊兒使用的(稍後會分析)
defineClass()方法是用來將byte字節流解析成JVM可以識別的Class對象(ClassLoader中已實現該方法邏輯),經過這個方法不只可以經過class文件實例化class對象,也能夠經過其餘方式實例化class對象,如經過網絡接收一個類的字節碼,而後轉換爲byte字節流建立對應的Class對象,defineClass()方法一般與findClass()方法一塊兒使用,通常狀況下,在自定義類加載器時,會直接覆蓋ClassLoader的findClass()方法並編寫加載規則,取得要加載類的字節碼後轉換成流,而後調用defineClass()方法生成類的Class對象
使用該方法可使用類的Class對象建立完成也同時被解析。前面咱們說連接階段主要是對字節碼進行驗證,爲類變量分配內存並設置初始值同時將字節碼文件中的符號引用轉換爲直接引用。
對於Java應用程序來講,熱部署就是在運行時更新Java類文件。
想要知道熱部署的原理,必需要了解java類的加載過程。一個java類文件到虛擬機裏的對象,要通過以下過程。
首先經過java編譯器,將java文件編譯成class字節碼,類加載器讀取class字節碼,再將類轉化爲實例,對實例newInstance就能夠生成對象。
類加載器ClassLoader功能,也就是將class字節碼轉換到類的實例。
在java應用中,全部的實例都是由類加載器,加載而來。
通常在系統中,類的加載都是由系統自帶的類加載器完成,並且對於同一個全限定名的java類(如com.csiar.soc.HelloWorld),只能被加載一次,並且沒法被卸載。
這個時候問題就來了,若是咱們但願將java類卸載,而且替換更新版本的java類,該怎麼作呢?
既然在類加載器中,java類只能被加載一次,而且沒法卸載。那是否是能夠直接把類加載器給換了?答案是能夠的,咱們能夠自定義類加載器,並重寫ClassLoader的findClass方法。想要實現熱部署能夠分如下三個步驟:
一、銷燬該自定義ClassLoader
二、更新class類文件
三、建立新的ClassLoader去加載更新後的class類文件。
Java熱部署與Java熱加載的聯繫和區別
Java熱部署與熱加載的聯繫
1.不重啓服務器編譯/部署項目
2.基於Java的類加載器實現
Java熱部署與熱加載的區別
部署方式
熱部署在服務器運行時從新部署項目
熱加載在運行時從新加載class
實現原理
熱部署直接從新加載整個應用
熱加載在運行時從新加載class
使用場景
熱部署更多的是在生產環境使用
熱加載則更多的實在開發環境使用
public class User {
public void add() { System.out.println("addV1,沒有修改過..."); }
} |
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()); } } |