字節碼技術
字節碼技術應用場景
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”的文件,“.java”文件中保存着Java代碼經轉換後的虛擬機指令,當需要使用某個類時,虛擬機將會加載它的“.java”文件,並創建對應的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後引入的,其工作原理的是,如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行,如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器纔會嘗試自己去加載,這就是雙親委派模式,即每個兒子都很懶,每次有活就丟給父親去幹,直到父親說這件事我也幹不了時,兒子自己想辦法去完成,這不就是傳說中的實力坑爹啊?那麼採用這種模式有啥用呢?
類加載器間的關係
我們進一步瞭解類加載器間的關係(並非指繼承關係),主要可以分爲以下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:\\workspace\\jvm\\target\\classes\\com\\dto\\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.dto.User"); Object obj1 = class1.newInstance(); Method method = class1.getMethod("add"); method.invoke(obj1); System.out.println(obj1.getClass()); System.out.println(obj1.getClass().getClassLoader()); } } |