1. 程式人生 > >動態生成Java位元組碼之java位元組碼框架ASM的學習

動態生成Java位元組碼之java位元組碼框架ASM的學習

一、什麼是ASM

  ASM是一個java位元組碼操縱框架,它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進位制 class 檔案,也可以在類被載入入 Java 虛擬機器之前動態改變類行為。Java class 被儲存在嚴格格式定義的 .class檔案裡,這些類檔案擁有足夠的元資料來解析類中的所有元素:類名稱、方法、屬性以及 Java 位元組碼(指令)。ASM從類檔案中讀入資訊後,能夠改變類行為,分析類資訊,甚至能夠根據使用者要求生成新類。asm位元組碼增強技術主要是用來反射的時候提升效能的,如果單純用jdk的反射呼叫,效能是非常低下的,而使用位元組碼增強技術後反射呼叫的時間已經基本可以與直接呼叫相當了

  使用ASM框架需要匯入asm的jar包,下載連結:asm-3.2.jar

二、如何使用ASM

  ASM框架中的核心類有以下幾個:

  ①  ClassReader:該類用來解析編譯過的class位元組碼檔案。

  ②  ClassWriter:該類用來重新構建編譯後的類,比如說修改類名、屬性以及方法,甚至可以生成新的類的位元組碼檔案。

  ③  ClassAdapter:該類也實現了ClassVisitor介面,它將對它的方法呼叫委託給另一個ClassVisitor物件。

三、 ASM位元組碼處理框架是用Java開發的而且使用基於訪問者模式生成位元組碼及驅動類到位元組碼的轉換。

      通俗的講,它就是對class檔案的CRUD,經過CRUD後的位元組碼可以轉換為類。ASM的解析方式類似於SAX解析XML檔案,它綜合運用了訪問者模式、職責鏈模式、橋接模式等多種設計模式,相對於其他類似工具如BCEL、SERP、Javassist、CGLIB,它的最大的優勢就在於其效能更高,其jar包僅30K。Hibernate和Spring都使用了cglib代理,而cglib本身就是使用的ASM,可見ASM在各種開源框架都有廣泛的應用。
   ASM是一個強大的框架,利用它我們可以做到:
   1、獲得class檔案的詳細資訊,包括類名、父類名、介面、成員名、方法名、方法引數名、區域性變數名、元資料等
   2、對class檔案進行動態修改,如增加、刪除、修改類方法、在某個方法中新增指令等

   3、CGLIB(動態代理)是對ASM的封裝,簡化了ASM的操作,降低了ASM的使用門檻,

   其中,hibernate的懶載入使用到了asm,spring的AOP也使用到了。你建立一個hibernate對映物件並使用懶載入配置的時候,在記憶體中生成的物件使用的不再是你實現的那個類了,而是hibernate根據位元組碼技術已你的類為模板構造的一個新類,證明就是當你獲得那個物件輸出類名是,不是你自己生成的類名了。spring可能是proxy$xxx,hibernate可能是<你的類名>$xxx$xxx之類的名字。

 AOP 的利器:ASM 3.0 介紹

       隨著 AOP(Aspect Oriented Programming)的發展,程式碼動態生成已然成為 Java 世界中不可或缺的一環。本文將介紹一種小巧輕便的 Java 位元組碼操控框架 ASM,它能方便地生成和改造 Java 程式碼。著名的框架,如 Hibernate 和 Spring 在底層都用到了 ASM。比起傳統的 Java 位元組碼操控框架,BCEL 或者 SERP,它具有更符合現代軟體模式的程式設計模型和更迅捷的效能。

本文主要分為四個部分:首先將 ASM 和其他 Java 類生成方案作對比,然後大致介紹 Java 類檔案的組織,最後針對最新的 ASM 3.0,描述其程式設計框架,並給出一個使用 ASM 進行 AOP 的例子,介紹調整函式內容,生成派生類,以及靜態和動態生成類的方法

   引言

什麼是 ASM ?

ASM 是一個 Java 位元組碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進位制 class 檔案,也可以在類被載入入 Java 虛擬機器之前動態改變類行為。Java class 被儲存在嚴格格式定義的 .class 檔案裡,這些類檔案擁有足夠的元資料來解析類中的所有元素:類名稱、方法、屬性以及 Java 位元組碼(指令)。ASM 從類檔案中讀入資訊後,能夠改變類行為,分析類資訊,甚至能夠根據使用者要求生成新類。

與 BCEL 和 SERL 不同,ASM 提供了更為現代的程式設計模型。對於 ASM 來說,Java class 被描述為一棵樹;使用 “Visitor” 模式遍歷整個二進位制結構;事件驅動的處理方式使得使用者只需要關注於對其程式設計有意義的部分,而不必瞭解 Java 類檔案格式的所有細節:ASM 框架提供了預設的 “response taker”處理這一切。

為什麼要動態生成 Java 類?

動態生成 Java 類與 AOP 密切相關的。AOP 的初衷在於軟體設計世界中存在這麼一類程式碼,零散而又耦合:零散是由於一些公有的功能(諸如著名的 log 例子)分散在所有模組之中;同時改變 log 功能又會影響到所有的模組。出現這樣的缺陷,很大程度上是由於傳統的 面向物件程式設計注重以繼承關係為代表的“縱向”關係,而對於擁有相同功能或者說方面 (Aspect)的模組之間的“橫向”關係不能很好地表達。例如,目前有一個既有的銀行管理系統,包括 Bank、Customer、Account、Invoice 等物件,現在要加入一個安全檢查模組, 對已有類的所有操作之前都必須進行一次安全檢查。

圖 1. ASM – AOP

      然而 Bank、Customer、Account、Invoice 是代表不同的事務,派生自不同的父類,很難在高層上加入關於 Security Checker 的共有功能。對於沒有多繼承的 Java 來說,更是如此。傳統的解決方案是使用 Decorator 模式,它可以在一定程度上改善耦合,而功能仍舊是分散的 —— 每個需要 Security Checker 的類都必須要派生一個 Decorator,每個需要 Security Checker 的方法都要被包裝(wrap)。下面我們以Account類為例看一下 Decorator:

首先,我們有一個 SecurityChecker類,其靜態方法 checkSecurity執行安全檢查功能:

public class SecurityChecker { 
	 public static void checkSecurity() { 
		 System.out.println("SecurityChecker.checkSecurity ..."); 
		 //TODO real security check 
	 } 	
 }


另一個是 Account類:

public class Account { 
	 public void operation() { 
		 System.out.println("operation..."); 
		 //TODO real operation 
	 } 
 }

若想對 operation加入對 SecurityCheck.checkSecurity()呼叫,標準的 Decorator 需要先定義一個Account類的介面:
public interface Account { 
	 void operation(); 
 }

然後把原來的 Account類定義為一個實現類:
<pre name="code" class="java">public class AccountImpl extends Account{ 
	 public void operation() { 
		 System.out.println("operation..."); 
		 //TODO real operation 
	 } 
 }

定義一個 Account類的 Decorator,幷包裝 operation方法:

public class AccountWithSecurityCheck implements Account { 	
	 private  Account account; 
	 public AccountWithSecurityCheck (Account account) { 
		 this.account = account; 
	 } 
	 public void operation() { 
		 SecurityChecker.checkSecurity(); 
		 account.operation(); 
	 } 
 }

這個簡單的例子裡,改造一個類的一個方法還好,如果是變動整個模組,Decorator 很快就會演化成另一個噩夢。動態改變 Java 類就是要解決 AOP 的問題,提供一種得到系統支援的可程式設計的方法,自動化地生成或者增強 Java 程式碼。這種技術已經廣泛應用於最新的 Java 框架內,如 Hibernate,Spring 等。

為什麼選擇 ASM ?

      最直接的改造 Java 類的方法莫過於直接改寫 class 檔案。Java 規範詳細說明了 class 檔案的格式,直接編輯位元組碼確實可以改變 Java 類的行為。直到今天,還有一些 Java 高手們使用最原始的工具,如 UltraEdit 這樣的編輯器對 class 檔案動手術。是的,這是最直接的方法,但是要求使用者對 Java class 檔案的格式了熟於心:小心地推算出想改造的函式相對檔案首部的偏移量,同時重新計算 class 檔案的校驗碼以通過 Java 虛擬機器的安全機制。

Java 5 中提供的 Instrument 包也可以提供類似的功能:啟動時往 Java 虛擬機器中掛上一個使用者定義的 hook 程式,可以在裝入特定類的時候改變特定類的位元組碼,從而改變該類的行為。但是其缺點也是明顯的:

  • Instrument 包是在整個虛擬機器上掛了一個鉤子程式,每次裝入一個新類的時候,都必須執行一遍這段程式,即使這個類不需要改變。
  • 直接改變位元組碼事實上類似於直接改寫 class 檔案,無論是呼叫 ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer),還是Instrument.redefineClasses(ClassDefinition[] definitions),都必須提供新 Java 類的位元組碼。也就是說,同直接改寫 class 檔案一樣,使用 Instrument 也必須瞭解想改造的方法相對類首部的偏移量,才能在適當的位置上插入新的程式碼。

      儘管 Instrument 可以改造類,但事實上,Instrument 更適用於監控和控制虛擬機器的行為。

一種比較理想且流行的方法是使用 java.lang.ref.proxy。我們仍舊使用上面的例子,給 Account類加上 checkSecurity 功能 :

首先,Proxy 程式設計是面向介面的。下面我們會看到,Proxy 並不負責例項化物件,和 Decorator 模式一樣,要把 Account定義成一個介面,然後在AccountImpl裡實現Account介面,接著實現一個 InvocationHandlerAccount方法被呼叫的時候,虛擬機器都會實際呼叫這個InvocationHandlerinvoke方法:

lass SecurityProxyInvocationHandler implements InvocationHandler { 
	 private Object proxyedObject; 
	 public SecurityProxyInvocationHandler(Object o) { 
		 proxyedObject = o; 
	 } 
		
	 public Object invoke(Object object, Method method, Object[] arguments) 
		 throws Throwable { 			
		 if (object instanceof Account && method.getName().equals("opertaion")) { 
			 SecurityChecker.checkSecurity(); 
		 } 
		 return method.invoke(proxyedObject, arguments); 
	 } 
 }

最後,在應用程式中指定 InvocationHandler生成代理物件:
public static void main(String[] args) { 
	 Account account = (Account) Proxy.newProxyInstance( 
		 Account.class.getClassLoader(), 
		 new Class[] { Account.class }, 
		 new SecurityProxyInvocationHandler(new AccountImpl()) 
	 ); 
	 account.function(); 
 }

其不足之處在於:

  • Proxy 是面向介面的,所有使用 Proxy 的物件都必須定義一個介面,而且用這些物件的程式碼也必須是對介面程式設計的:Proxy 生成的物件是介面一致的而不是物件一致的:例子中Proxy.newProxyInstance生成的是實現Account介面的物件而不是 AccountImpl的子類。這對於軟體架構設計,尤其對於既有軟體系統是有一定掣肘的。
  • Proxy 畢竟是通過反射實現的,必須在效率上付出代價:有實驗資料表明,呼叫反射比一般的函式開銷至少要大 10 倍。而且,從程式實現上可以看出,對 proxy class 的所有方法呼叫都要通過使用反射的 invoke 方法。因此,對於效能關鍵的應用,使用 proxy class 是需要精心考慮的,以避免反射成為整個應用的瓶頸。

      ASM 能夠通過改造既有類,直接生成需要的程式碼。增強的程式碼是硬編碼在新生成的類檔案內部的,沒有反射帶來效能上的付出。同時,ASM 與 Proxy 程式設計不同,不需要為增強程式碼而新定義一個介面,生成的程式碼可以覆蓋原來的類,或者是原始類的子類。它是一個普通的 Java 類而不是 proxy 類,甚至可以在應用程式的類框架中擁有自己的位置,派生自己的子類。

相比於其他流行的 Java 位元組碼操縱工具,ASM 更小更快。ASM 具有類似於 BCEL 或者 SERP 的功能,而只有 33k 大小,而後者分別有 350k 和 150k。同時,同樣類轉換的負載,如果 ASM 是 60% 的話,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。

ASM 已經被廣泛應用於一系列 Java 專案:AspectWerkz、AspectJ、BEA WebLogic、IBM AUS、OracleBerkleyDB、Oracle TopLink、Terracotta、RIFE、EclipseME、Proactive、Speedo、Fractal、EasyBeans、BeanShell、Groovy、Jamaica、CGLIB、dynaop、Cobertura、JDBCPersistence、JiP、SonarJ、Substance L&F、Retrotranslator 等。Hibernate 和 Spring 也通過 cglib,另一個更高層一些的自動程式碼生成工具使用了 ASM。

Java 類檔案概述

所謂 Java 類檔案,就是通常用 javac 編譯器產生的 .class 檔案。這些檔案具有嚴格定義的格式。為了更好的理解 ASM,首先對 Java 類檔案格式作一點簡單的介紹。Java 原始檔經過 javac 編譯器編譯之後,將會生成對應的二進位制檔案(如下圖所示)。每個合法的 Java 類檔案都具備精確的定義,而正是這種精確的定義,才使得 Java 虛擬機器得以正確讀取和解釋所有的 Java 類檔案。


圖 2. ASM – Javac 流程

      Java 類檔案是 8 位位元組的二進位制流。資料項按順序儲存在 class 檔案中,相鄰的項之間沒有間隔,這使得 class 檔案變得緊湊,減少儲存空間。在 Java 類檔案中包含了許多大小不同的項,由於每一項的結構都有嚴格規定,這使得 class 檔案能夠從頭到尾被順利地解析。下面讓我們來看一下 Java 類檔案的內部結構,以便對此有個大致的認識。

例如,一個最簡單的 Hello World 程式:

public class HelloWorld { 
	 public static void main(String[] args) { 
		 System.out.println("Hello world"); 
	 } 
 }
經過 javac 編譯後,得到的類檔案大致是:



從上圖中可以看到,一個 Java 類檔案大致可以歸為 10 個項:

  • Magic:該項存放了一個 Java 類檔案的魔數(magic number)和版本資訊。一個 Java 類檔案的前 4 個位元組被稱為它的魔數。每個正確的 Java 類檔案都是以 0xCAFEBABE 開頭的,這樣保證了 Java 虛擬機器能很輕鬆的分辨出 Java 檔案和非 Java 檔案。
  • Version:該項存放了 Java 類檔案的版本資訊,它對於一個 Java 檔案具有重要的意義。因為 Java 技術一直在發展,所以類檔案的格式也處在不斷變化之中。類檔案的版本資訊讓虛擬機器知道如何去讀取並處理該類檔案。
  • Constant Pool:該項存放了類中各種文字字串、類名、方法名和介面名稱、final 變數以及對外部類的引用資訊等常量。虛擬機器必須為每一個被裝載的類維護一個常量池,常量池中儲存了相應型別所用到的所有型別、欄位和方法的符號引用,因此它在 Java 的動態連結中起到了核心的作用。常量池的大小平均佔到了整個類大小的 60% 左右。
  • Access_flag:該項指明瞭該檔案中定義的是類還是介面(一個 class 檔案中只能有一個類或介面),同時還指名了類或介面的訪問標誌,如 public,private, abstract 等資訊。
  • This Class:指向表示該類全限定名稱的字串常量的指標。
  • Super Class:指向表示父類全限定名稱的字串常量的指標。
  • Interfaces:一個指標陣列,存放了該類或父類實現的所有介面名稱的字串常量的指標。以上三項所指向的常量,特別是前兩項,在我們用 ASM 從已有類派生新類時一般需要修改:將類名稱改為子類名稱;將父類改為派生前的類名稱;如果有必要,增加新的實現介面。
  • Fields:該項對類或介面中宣告的欄位進行了細緻的描述。需要注意的是,fields 列表中僅列出了本類或介面中的欄位,並不包括從超類和父介面繼承而來的欄位。
  • Methods:該項對類或介面中宣告的方法進行了細緻的描述。例如方法的名稱、引數和返回值型別等。需要注意的是,methods 列表裡僅存放了本類或本介面中的方法,並不包括從超類和父介面繼承而來的方法。使用 ASM 進行 AOP 程式設計,通常是通過調整 Method 中的指令來實現的。
  • Class attributes:該項存放了在該檔案中類或介面所定義的屬性的基本資訊。

事實上,使用 ASM 動態生成類,不需要像早年的 class hacker 一樣,熟知 class 檔案的每一段,以及它們的功能、長度、偏移量以及編碼方式。ASM 會給我們照顧好這一切的,我們只要告訴 ASM 要改動什麼就可以了 —— 當然,我們首先得知道要改什麼:對類檔案格式瞭解的越多,我們就能更好地使用 ASM 這個利器。

ASM 3.0 程式設計框架

      ASM 通過樹這種資料結構來表示複雜的位元組碼結構,並利用 Push 模型來對樹進行遍歷,在遍歷過程中對位元組碼進行修改。所謂的 Push 模型類似於簡單的 Visitor 設計模式,因為需要處理位元組碼結構是固定的,所以不需要專門抽象出一種 Vistable 介面,而只需要提供 Visitor 介面。所謂 Visitor 模式和 Iterator 模式有點類似,它們都被用來遍歷一些複雜的資料結構。Visitor 相當於使用者派出的代表,深入到演算法內部,由演算法安排訪問行程。Visitor 代表可以更換,但對演算法流程無法干涉,因此是被動的,這也是它和 Iterator 模式由使用者主動調遣演算法方式的最大的區別。

在 ASM 中,提供了一個 ClassReader類,這個類可以直接由位元組陣列或由 class 檔案間接的獲得位元組碼資料,它能正確的分析位元組碼,構建出抽象的樹在記憶體中表示位元組碼。它會呼叫accept方法,這個方法接受一個實現了ClassVisitor介面的物件例項作為引數,然後依次呼叫ClassVisitor介面的各個方法。位元組碼空間上的偏移被轉換成 visit 事件時間上呼叫的先後,所謂 visit 事件是指對各種不同 visit 函式的呼叫,ClassReader知道如何呼叫各種 visit 函式。在這個過程中使用者無法對操作進行干涉,所以遍歷的演算法是確定的,使用者可以做的是提供不同的 Visitor 來對位元組碼樹進行不同的修改。ClassVisitor會產生一些子過程,比如visitMethod會返回一個實現MethordVisitor介面的例項,visitField會返回一個實現FieldVisitor介面的例項,完成子過程後控制返回到父過程,繼續訪問下一節點。因此對於ClassReader來說,其內部順序訪問是有一定要求的。實際上使用者還可以不通過ClassReader類,自行手工控制這個流程,只要按照一定的順序,各個 visit 事件被先後正確的呼叫,最後就能生成可以被正確載入的位元組碼。當然獲得更大靈活性的同時也加大了調整位元組碼的複雜度。

各個 ClassVisitor通過職責鏈 (Chain-of-responsibility) 模式,可以非常簡單的封裝對位元組碼的各種修改,而無須關注位元組碼的位元組偏移,因為這些實現細節對於使用者都被隱藏了,使用者要做的只是覆寫相應的 visit 函式。

ClassAdaptor類實現了 ClassVisitor介面所定義的所有函式,當新建一個 ClassAdaptor物件的時候,需要傳入一個實現了 ClassVisitor介面的物件,作為職責鏈中的下一個訪問者 (Visitor),這些函式的預設實現就是簡單的把呼叫委派給這個物件,然後依次傳遞下去形成職責鏈。當用戶需要對位元組碼進行調整時,只需從ClassAdaptor類派生出一個子類,覆寫需要修改的方法,完成相應功能後再把呼叫傳遞下去。這樣,使用者無需考慮位元組偏移,就可以很方便的控制位元組碼。

每個 ClassAdaptor類的派生類可以僅封裝單一功能,比如刪除某函式、修改欄位可見性等等,然後再加入到職責鏈中,這樣耦合更小,重用的概率也更大,但代價是產生很多小物件,而且職責鏈的層次太長的話也會加大系統呼叫的開銷,使用者需要在低耦合和高效率之間作出權衡。使用者可以通過控制職責鏈中 visit 事件的過程,對類檔案進行如下操作:

  1. 刪除類的欄位、方法、指令:只需在職責鏈傳遞過程中中斷委派,不訪問相應的 visit 方法即可,比如刪除方法時只需直接返回 null,而不是返回由visitMethod方法返回的MethodVisitor物件。

class DelLoginClassAdapter extends ClassAdapter { 
	 public DelLoginClassAdapter(ClassVisitor cv) { 
		 super(cv); 
	 } 

	 public MethodVisitor visitMethod(final int access, final String name, 
		 final String desc, final String signature, final String[] exceptions) { 
		 if (name.equals("login")) { 
			 return null; 
		 } 
		 return cv.visitMethod(access, name, desc, signature, exceptions); 
	 } 
 }


2、修改類、欄位、方法的名字或修飾符:在職責鏈傳遞過程中替換呼叫引數。

 class AccessClassAdapter extends ClassAdapter { 
	 public AccessClassAdapter(ClassVisitor cv) { 
		 super(cv); 
	 } 

	 public FieldVisitor visitField(final int access, final String name, 
        final String desc, final String signature, final Object value) { 
        int privateAccess = Opcodes.ACC_PRIVATE; 
        return cv.visitField(privateAccess, name, desc, signature, value); 
    } 
 }

3、增加新的類、方法、欄位

ASM 的最終的目的是生成可以被正常裝載的 class 檔案,因此其框架結構為客戶提供了一個生成位元組碼的工具類 —— ClassWriter。它實現了ClassVisitor介面,而且含有一個toByteArray()函式,返回生成的位元組碼的位元組流,將位元組流寫回檔案即可生產調整後的 class 檔案。一般它都作為職責鏈的終點,把所有 visit 事件的先後呼叫(時間上的先後),最終轉換成位元組碼的位置的調整(空間上的前後),如下例:

ClassWriter  classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
 ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter); 
 ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor); 
	
 ClassReader classReader = new ClassReader(strFileName); 
 classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);

綜上所述,ASM 的時序圖如下:

圖 4. ASM – 時序圖

使用 ASM3.0 進行 AOP 程式設計

      我們還是用上面的例子,給 Account類加上 security check 的功能。與 proxy 程式設計不同,ASM 不需要將 Account宣告成介面,Account可以仍舊是一個實現類。ASM 將直接在 Account類上動手術,給Account類的operation方法首部加上對 SecurityChecker.checkSecurity的呼叫。

首先,我們將從 ClassAdapter繼承一個類。ClassAdapter是 ASM 框架提供的一個預設類,負責溝通ClassReaderClassWriter。如果想要改變ClassReader處讀入的類,然後從ClassWriter處輸出,可以重寫相應的ClassAdapter函式。這裡,為了改變 Account類的operation 方法,我們將重寫visitMethdod方法。

class AddSecurityCheckClassAdapter extends ClassAdapter {

    public AddSecurityCheckClassAdapter(ClassVisitor cv) {
        //Responsechain 的下一個 ClassVisitor,這裡我們將傳入 ClassWriter,
        // 負責改寫後代碼的輸出
        super(cv); 
    } 
    
    // 重寫 visitMethod,訪問到 "operation" 方法時,
    // 給出自定義 MethodVisitor,實際改寫方法內容
    public MethodVisitor visitMethod(final int access, final String name, 
        final String desc, final String signature, final String[] exceptions) { 
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
        MethodVisitor wrappedMv = mv; 
        if (mv != null) { 
            // 對於 "operation" 方法
            if (name.equals("operation")) { 
                // 使用自定義 MethodVisitor,實際改寫方法內容
                wrappedMv = new AddSecurityCheckMethodAdapter(mv); 
            } 
        } 
        return wrappedMv; 
    } 
}


下一步就是定義一個繼承自 MethodAdapter的 AddSecurityCheckMethodAdapter,在“operation”方法首部插入對SecurityChecker.checkSecurity()的呼叫。

class AddSecurityCheckMethodAdapter extends MethodAdapter { 
	 public AddSecurityCheckMethodAdapter(MethodVisitor mv) { 
		 super(mv); 
	 } 

	 public void visitCode() { 
		 visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker", 
			"checkSecurity", "()V"); 
	 } 
 }

其中,ClassReader讀到每個方法的首部時呼叫 visitCode(),在這個重寫方法裡,我們用 visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V");插入了安全檢查功能。

最後,我們將整合上面定義的 ClassAdapterClassReader和 ClassWriter產生修改後的Account類檔案 :

<span style="font-family: "microsoft yahei"; font-size: 15px;">i</span>mport java.io.File; 
 import java.io.FileOutputStream; 
 import org.objectweb.asm.*; 
    
 public class Generator{ 
	 public static void main() throws Exception { 
		 ClassReader cr = new ClassReader("Account"); 
		 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
		 ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw); 
		 cr.accept(classAdapter, ClassReader.SKIP_DEBUG); 
		 byte[] data = cw.toByteArray(); 
		 File file = new File("Account.class"); 
		 FileOutputStream fout = new FileOutputStream(file); 
		 fout.write(data); 
		 fout.close(); 
	 } 
 }<span style="font-family:microsoft yahei;"><span style="font-size: 15px;">
執行完這段程式後,我們會得到一個新的 Account.class 檔案,如果我們使用下面程式碼:</span></span>
<span style="font-family:microsoft yahei;"><span style="font-size: 15px;">
 </span></span>public class Main { 
	 public static void main(String[] args) { 
		 Account account = new Account(); 
		 account.operation(); 
	 } 
 }

使用這個 Account,我們會得到下面的輸出:
SecurityChecker.checkSecurity ... 
 operation...

也就是說,在 Account原來的 operation內容執行之前,進行了 SecurityChecker.checkSecurity()檢查。

將動態生成類改造成原始類 Account 的子類

上面給出的例子是直接改造 Account類本身的,從此 Account類的 operation方法必須進行 checkSecurity 檢查。但事實上,我們有時仍希望保留原來的Account類,因此把生成類定義為原始類的子類是更符合 AOP 原則的做法。下面介紹如何將改造後的類定義為Account的子類Account$EnhancedByASM。其中主要有兩項工作 :

  • 改變 Class Description, 將其命名為 Account$EnhancedByASM,將其父類指定為 Account
  • 改變建構函式,將其中對父類建構函式的呼叫轉換為對 Account建構函式的呼叫。

在 AddSecurityCheckClassAdapter類中,將重寫 visit方法:

public void visit(final int version, final int access, final String name, 
		 final String signature, final String superName, 
		 final String[] interfaces) { 
	 String enhancedName = name + "$EnhancedByASM";  // 改變類命名
	 enhancedSuperName = name; // 改變父類,這裡是”Account”
	 super.visit(version, access, enhancedName, signature, 
	 enhancedSuperName, interfaces); 
 }

改進 visitMethod方法,增加對建構函式的處理:
public MethodVisitor visitMethod(final int access, final String name, 
	 final String desc, final String signature, final String[] exceptions) { 
	 MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); 
	 MethodVisitor wrappedMv = mv; 
	 if (mv != null) { 
		 if (name.equals("operation")) { 
			 wrappedMv = new AddSecurityCheckMethodAdapter(mv); 
		 } else if (name.equals("<init>")) { 
			 wrappedMv = new ChangeToChildConstructorMethodAdapter(mv, 
				 enhancedSuperName); 
		 } 
	 } 
	 return wrappedMv; 
 }


這裡 ChangeToChildConstructorMethodAdapter將負責把 Account的建構函式改造成其子類Account$EnhancedByASM的建構函式:
class ChangeToChildConstructorMethodAdapter extends MethodAdapter { 
	 private String superClassName; 

	 public ChangeToChildConstructorMethodAdapter(MethodVisitor mv, 
		 String superClassName) { 
		 super(mv); 
		 this.superClassName = superClassName; 
	 } 

	 public void visitMethodInsn(int opcode, String owner, String name, 
		 String desc) { 
		 // 呼叫父類的建構函式時
		 if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { 
			 owner = superClassName; 
		 } 
		 super.visitMethodInsn(opcode, owner, name, desc);// 改寫父類為 superClassName 
	 } 
 }

最後演示一下如何在執行時產生並裝入產生的 Account$EnhancedByASM。 我們定義一個 Util 類,作為一個類工廠負責產生有安全檢查的Account類:
public class SecureAccountGenerator { 

    private static AccountGeneratorClassLoader classLoader = 
        new AccountGeneratorClassLoade(); 
    
    private static Class secureAccountClass; 
    
    public Account generateSecureAccount() throws ClassFormatError, 
        InstantiationException, IllegalAccessException { 
        if (null == secureAccountClass) {            
            ClassReader cr = new ClassReader("Account"); 
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
            ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
            cr.accept(classAdapter, ClassReader.SKIP_DEBUG); 
            byte[] data = cw.toByteArray(); 
            secureAccountClass = classLoader.defineClassFromClassFile( 
               "Account$EnhancedByASM",data); 
        } 
        return (Account) secureAccountClass.newInstance(); 
    } 
    
    private static class AccountGeneratorClassLoader extends ClassLoader {
        public Class defineClassFromClassFile(String className, 
            byte[] classFile) throws ClassFormatError { 
            return defineClass("Account$EnhancedByASM", classFile, 0, 
	        classFile.length());
        } 
    } 
}

靜態方法 SecureAccountGenerator.generateSecureAccount()在執行時動態生成一個加上了安全檢查的Account子類。著名的 Hibernate 和 Spring 框架,就是使用這種技術實現了 AOP 的“無損注入”。

小結

最後,我們比較一下 ASM 和其他實現 AOP 的底層技術:

表 1. AOP 底層技術比較
AOP 底層技術 功能 效能 面向介面程式設計 程式設計難度
直接改寫 class 檔案 完全控制類 無明顯效能代價 不要求 高,要求對 class 檔案結構和 Java 位元組碼有深刻了解
JDK Instrument 完全控制類 無論是否改寫,每個類裝入時都要執行 hook 程式 不要求 高,要求對 class 檔案結構和 Java 位元組碼有深刻了解
JDK Proxy 只能改寫 method 反射引入效能代價 要求
ASM 幾乎能完全控制類 無明顯效能代價 不要求 中,能操縱需要改寫部分的 Java 位元組碼

相關推薦

動態生成Java位元組java位元組框架ASM學習

一、什麼是ASM   ASM是一個java位元組碼操縱框架,它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進位制 class 檔案,也可以在類被載入入 Java 虛擬機器之前動態改變類行為。Java class 被儲存在嚴格格式定義的 .class

Java:IO流:探究位元組流和字元流

前言---- 本來不想寫前言了,但是寫完,發現寫得太好了,遇到就好好看一看啊!!!! 注:歡迎轉載,轉載請註明來處 目錄 一. 簡單理解什麼是流? 二.位元組輸入流 三.位元組輸出流 四.流的正確關閉方式 五.字元輸入流 六.字元輸出流 七.位元組流和字元流的

java集合系列HashMap源

實現 幫助 成員變量 eno dea after 一次 == 處的 java集合系列之HashMap源碼   HashMap的源碼可真不好消化!!!   首先簡單介紹一下HashMap集合的特點。HashMap存放鍵值對,鍵值對封裝在Node(代碼如下,比較簡單,不再介紹)

JAVA並發ReentrantLock源(二)

pat success next ava 並且 skip eas link lease   上一篇我們講到了ReentrantLock通過使用AQS框架實現了tryAcquire、tryRelease方法,從ReentrantLock層面分析源碼,本次我們將進一步深入AQS

深入JAVA虛擬機字節執行引擎

內存布局 出現 編譯程序 方法調用 virt cdi ati special 成了 前言:class文件結構、類加載機制、類加載器、運行時數據區這四個java技術體系中非常重要的知識,學習完了這些以後,我們知道一個類是通過類加載器加載到虛擬機,存儲到運行時數據區,而且我們也

反射包 java.lang.reflect⑦ Java 中 Proxy 動態代理類 探祕(三)

這個系列好久沒有續作了,你以為完了,錯了。這個動態代理有說不完的知識點,我也是在不斷的學習中才瞭解得到更多的知識。但無可否認的一點是它需更多其他的知識的支援,比如設計模式,設計思想。工作越久越覺得設計模式這個東西的重要性。  其實動態代理的前兩個例子只是簡單的列出了它的一個

一篇文章能夠看懂基礎源代JAVA

不可 condition 多個 訪問權限 自增 一個數 abs gen amp java程序開發使用的工具類包:JDK(java development kit)java程序運行需要使用的虛擬機:JVM,只需要安裝JRE (java runtime environment)

java基礎系列ConcurrentHashMap源分析(基於jdk1.8)

threshold 主存 類比 tile num method 過程 參數 nsf 1、前提   在閱讀這篇博客之前,希望你對HashMap已經是有所理解的;另外你對java的cas操作也是有一定了解的,因為在這個類中大量使用到了cas相關的操作來保證線程安全的。   

Java並發AQS源分析(二)

next dac mage bool 需要 狀態 false 兩個 繼續 我在Java並發之AQS源碼分析(一)這篇文章中,從源碼的角度深度剖析了 AQS 獨占鎖模式下的獲取鎖與釋放鎖的邏輯,如果你把這部分搞明白了,再看共享鎖的實現原理,思路就會清晰很多。下面我們繼續從源碼

死磕 java並發包LongAdder源分析

ica sys offset ktr 遷移 對比 .get unsafe join() 問題 (1)java8中為什麽要新增LongAdder? (2)LongAdder的實現方式? (3)LongAdder與AtomicLong的對比? 簡介 LongAdder是java

Java虛擬機Java內存區域

器) 輪換 .com 虛擬 解釋器 控制 虛擬機 關心 分配 Java虛擬機運行時數據區域 ⑴背景:對於c/c++來說程序員來說,需要經常去關心內存運行情況,但對於Java程序員,只需要在必要時關心內存運行情況,這是因為在Java虛擬機自動內存管理機制的幫助下,不再Ja

Java Killer系列Java經典面試套路講解

Java Killer系列之Java經典面試套路講解 java編程語言是目前應用較為廣泛的一門計算機編程語言,目前java市場需求量有增無減。java作為目前IT軟件開發行業的重要技術之一,人才市場出現大量缺口,所以從事java相關工作,還是非常有前景的。

Java異常解決--java.lang.NullPointerException

null 異常 bsp ray lan 判斷 常常 類型 對象 1、java.lang.NullPointerException【空指針異常】一般報java.lang.NullPointerException的原因有以下幾種 :一般常常都是你引用了一個未 new 變量 或者

Java面試準備Java基礎

import shc 程序設計 不依賴 字符串常量 而是 修復 註意 克隆對象 1.Java 語言的優點 面向對象,平臺無關,內存管理,安全性,多線程,Java 是解釋型的 2.Java 和 C++的區別 多重繼承(java接口多重,類不支持,C++支持) 自動內存管理

JDK動態代理[2]----JDK動態代理的底層實現Proxy源分析

sco 不可 -- 例如 mis tfs err eno entity 在上一篇裏為大家簡單介紹了什麽是代理模式?為什麽要使用代理模式?並用例子演示了一下靜態代理和動態代理的實現,分析了靜態代理和動態代理各自的優缺點。在這一篇中筆者打算深入源碼為大家剖析JDK動態代理實現的

Java面試題Java基礎

formate spa 多重繼承 輸出 不能 類名 gre collect extend 1、作用域public,private,protected,以及不寫時的區別 答:區別如下: 作用域 當前類 同一package 子孫類

java成神——java中string的用法

基本 ble sta first stand pat concat lower a+b java中String的用法 String基本用法 String分割 String拼接 String截取 String換行符和format格式化 String反轉字符串和去除空白字符

Java開發知識Java入門

許可 可用 詳解 運行 不包含 不可用 bean ejb ads              Java開發知識之Java入門 一丶了解JAVA的版本   JAVA 有三個版本   JAVA SE:  標準版,開發桌面跟商務應用程序 JAVA SE 包括了Java的核心類庫,集

Java開發知識Java的繼承多態跟接口*

們的 class 參數順序 程序員 數據 父類 com 應該 手機           Java開發知識之Java的繼承多態跟接口 一丶繼承   1.繼承的寫法   在Java中繼承的 關鍵字是 extends 代表一個類繼承另一個類. 繼承的含義以及作用: 繼承就是基於某

Java開發知識Java的異常處理

ssa run 範圍 com 行處理 意思 操作 spa exce       Java開發知識之Java的異常處理 一丶異常概述   在講解異常之前,我們要搞清楚.什麽是異常. 通俗理解就是我們編寫的程序出問題了.進行處理的一種手段. 比如我們的QQ.有的時候就崩潰了.比