天天看點

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹

原文連結:http://blog.csdn.net/qq_27376871/article/details/51613066

一、什麼是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
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹

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

Account

類為例看一下 Decorator:

首先,我們有一個 

SecurityChecker

類,其靜态方法 

checkSecurity

執行安全檢查功能:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. public class SecurityChecker {   
  2.      public static void checkSecurity() {   
  3.          System.out.println("SecurityChecker.checkSecurity ...");   
  4.          //TODO real security check   
  5.      }    
  6.  }  

另一個是 

Account

類:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. public class Account {   
  2.      public void operation() {   
  3.          System.out.println("operation...");   
  4.          //TODO real operation   
  5.      }   
  6.  }  

若想對 

operation

加入對 

SecurityCheck.checkSecurity()

調用,标準的 Decorator 需要先定義一個

Account

類的接口:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. public interface Account {   
  2.      void operation();   
  3.  }  

然後把原來的 

Account

類定義為一個實作類:

<pre name="code" class="java">public class AccountImpl extends Account{ 
	 public void operation() { 
		 System.out.println("operation..."); 
		 //TODO real operation 
	 } 
 }      

定義一個 

Account

類的 Decorator,并包裝 

operation

方法:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. public class AccountWithSecurityCheck implements Account {    
  2.      private  Account account;   
  3.      public AccountWithSecurityCheck (Account account) {   
  4.          this.account = account;   
  5.      }   
  6.      public void operation() {   
  7.          SecurityChecker.checkSecurity();   
  8.          account.operation();   
  9.      }   
  10.  }  

這個簡單的例子裡,改造一個類的一個方法還好,如果是變動整個子產品,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

接口,接着實作一個 

InvocationHandler

Account

方法被調用的時候,虛拟機都會實際調用這個

InvocationHandler

invoke

方法:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. lass SecurityProxyInvocationHandler implements InvocationHandler {   
  2.      private Object proxyedObject;   
  3.      public SecurityProxyInvocationHandler(Object o) {   
  4.          proxyedObject = o;   
  5.      }   
  6.      public Object invoke(Object object, Method method, Object[] arguments)   
  7.          throws Throwable {               
  8.          if (object instanceof Account && method.getName().equals("opertaion")) {   
  9.              SecurityChecker.checkSecurity();   
  10.          }   
  11.          return method.invoke(proxyedObject, arguments);   
  12.      }   
  13.  }  

最後,在應用程式中指定 

InvocationHandler

生成代理對象:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. public static void main(String[] args) {   
  2.      Account account = (Account) Proxy.newProxyInstance(   
  3.          Account.class.getClassLoader(),   
  4.          new Class[] { Account.class },   
  5.          new SecurityProxyInvocationHandler(new AccountImpl())   
  6.      );   
  7.      account.function();   
  8.  }  

其不足之處在于:

  • 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位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹

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

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

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. public class HelloWorld {   
  2.      public static void main(String[] args) {   
  3.          System.out.println("Hello world");   
  4.      }   
  5.  }  

經過 javac 編譯後,得到的類檔案大緻是:

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹

從上圖中可以看到,一個 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

    對象。

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. class DelLoginClassAdapter extends ClassAdapter {   
  2.      public DelLoginClassAdapter(ClassVisitor cv) {   
  3.          super(cv);   
  4.      }   
  5.      public MethodVisitor visitMethod(final int access, final String name,   
  6.          final String desc, final String signature, final String[] exceptions) {   
  7.          if (name.equals("login")) {   
  8.              return null;   
  9.          }   
  10.          return cv.visitMethod(access, name, desc, signature, exceptions);   
  11.      }   
  12.  }  

2、修改類、字段、方法的名字或修飾符:在職責鍊傳遞過程中替換調用參數。

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. class AccessClassAdapter extends ClassAdapter {   
  2.  public AccessClassAdapter(ClassVisitor cv) {   
  3.      super(cv);   
  4.  }   
  5.  public FieldVisitor visitField(final int access, final String name,   
  6.        final String desc, final String signature, final Object value) {   
  7.        int privateAccess = Opcodes.ACC_PRIVATE;   
  8.        return cv.visitField(privateAccess, name, desc, signature, value);   
  9.    }   
  10. }  

3、 增加新的類、方法、字段

ASM 的最終的目的是生成可以被正常裝載的 class 檔案,是以其架構結構為客戶提供了一個生成位元組碼的工具類 —— 

ClassWriter

。它實作了

ClassVisitor

接口,而且含有一個

toByteArray()

函數,傳回生成的位元組碼的位元組流,将位元組流寫回檔案即可生産調整後的 class 檔案。一般它都作為職責鍊的終點,把所有 visit 事件的先後調用(時間上的先後),最終轉換成位元組碼的位置的調整(空間上的前後),如下例:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. ClassWriter  classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);   
  2.  ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter);   
  3.  ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor);   
  4.  ClassReader classReader = new ClassReader(strFileName);   
  5.  classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);  

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

圖 4. ASM – 時序圖
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹

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

      我們還是用上面的例子,給 

Account

類加上 security check 的功能。與 proxy 程式設計不同,ASM 不需要将 

Account

聲明成接口,

Account

可以仍舊是一個實作類。ASM 将直接在 

Account

類上動手術,給

Account

類的

operation

方法首部加上對 

SecurityChecker.checkSecurity

的調用。

首先,我們将從 

ClassAdapter

繼承一個類。

ClassAdapter

是 ASM 架構提供的一個預設類,負責溝通

ClassReader

ClassWriter

。如果想要改變

ClassReader

處讀入的類,然後從

ClassWriter

處輸出,可以重寫相應的

ClassAdapter

函數。這裡,為了改變 

Account

類的

operation

 方法,我們将重寫

visitMethdod

方法。

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. class AddSecurityCheckClassAdapter extends ClassAdapter {  
  2.     public AddSecurityCheckClassAdapter(ClassVisitor cv) {  
  3.         //Responsechain 的下一個 ClassVisitor,這裡我們将傳入 ClassWriter,  
  4.         // 負責改寫後代碼的輸出  
  5.         super(cv);   
  6.     }   
  7.     // 重寫 visitMethod,通路到 "operation" 方法時,  
  8.     // 給出自定義 MethodVisitor,實際改寫方法内容  
  9.     public MethodVisitor visitMethod(final int access, final String name,   
  10.         final String desc, final String signature, final String[] exceptions) {   
  11.         MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);  
  12.         MethodVisitor wrappedMv = mv;   
  13.         if (mv != null) {   
  14.             // 對于 "operation" 方法  
  15.             if (name.equals("operation")) {   
  16.                 // 使用自定義 MethodVisitor,實際改寫方法内容  
  17.                 wrappedMv = new AddSecurityCheckMethodAdapter(mv);   
  18.             }   
  19.         }   
  20.         return wrappedMv;   
  21.     }   
  22. }  

下一步就是定義一個繼承自 

MethodAdapter

的 

AddSecurityCheckMethodAdapter

,在“

operation

”方法首部插入對

SecurityChecker.checkSecurity()

的調用。

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. class AddSecurityCheckMethodAdapter extends MethodAdapter {   
  2.      public AddSecurityCheckMethodAdapter(MethodVisitor mv) {   
  3.          super(mv);   
  4.      }   
  5.      public void visitCode() {   
  6.          visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker",   
  7.             "checkSecurity", "()V");   
  8.      }   
  9.  }  

其中,

ClassReader

讀到每個方法的首部時調用 

visitCode()

,在這個重寫方法裡,我們用 

visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V");

插入了安全檢查功能。

最後,我們将內建上面定義的 

ClassAdapter

ClassReader

和 

ClassWriter

産生修改後的

Account

類檔案 :

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. <span style="font-family: "microsoft yahei"; font-size: 15px;">i</span>mport java.io.File;   
  2.  import java.io.FileOutputStream;   
  3.  import org.objectweb.asm.*;   
  4.  public class Generator{   
  5.      public static void main() throws Exception {   
  6.          ClassReader cr = new ClassReader("Account");   
  7.          ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);   
  8.          ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);   
  9.          cr.accept(classAdapter, ClassReader.SKIP_DEBUG);   
  10.          byte[] data = cw.toByteArray();   
  11.          File file = new File("Account.class");   
  12.          FileOutputStream fout = new FileOutputStream(file);   
  13.          fout.write(data);   
  14.          fout.close();   
  15.      }   
  16.  }<span style="font-family:microsoft yahei;"><span style="font-size: 15px;">  
  17. 執行完這段程式後,我們會得到一個新的 Account.class 檔案,如果我們使用下面代碼:</span></span>  

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. <span style="font-family:microsoft yahei;"><span style="font-size: 15px;">  
  2.  </span></span>public class Main {   
  3.      public static void main(String[] args) {   
  4.          Account account = new Account();   
  5.          account.operation();   
  6.      }   
  7.  }  

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

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. SecurityChecker.checkSecurity ...   
  2.  operation...  

也就是說,在 

Account

原來的 

operation

内容執行之前,進行了 

SecurityChecker.checkSecurity()

檢查。

将動态生成類改造成原始類 Account 的子類

上面給出的例子是直接改造 

Account

類本身的,從此 

Account

類的 

operation

方法必須進行 checkSecurity 檢查。但事實上,我們有時仍希望保留原來的

Account

類,是以把生成類定義為原始類的子類是更符合 AOP 原則的做法。下面介紹如何将改造後的類定義為

Account

的子類

Account$EnhancedByASM

。其中主要有兩項工作 :

  • 改變 Class Description, 将其命名為 

    Account$EnhancedByASM

    ,将其父類指定為 

    Account

  • 改變構造函數,将其中對父類構造函數的調用轉換為對 

    Account

    構造函數的調用。

在 

AddSecurityCheckClassAdapter

類中,将重寫 

visit

方法:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. public void visit(final int version, final int access, final String name,   
  2.          final String signature, final String superName,   
  3.          final String[] interfaces) {   
  4.      String enhancedName = name + "$EnhancedByASM";  // 改變類命名  
  5.      enhancedSuperName = name; // 改變父類,這裡是”Account”  
  6.      super.visit(version, access, enhancedName, signature,   
  7.      enhancedSuperName, interfaces);   
  8.  }  

改進 

visitMethod

方法,增加對構造函數的處理:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. public MethodVisitor visitMethod(final int access, final String name,   
  2.      final String desc, final String signature, final String[] exceptions) {   
  3.      MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);   
  4.      MethodVisitor wrappedMv = mv;   
  5.      if (mv != null) {   
  6.          if (name.equals("operation")) {   
  7.              wrappedMv = new AddSecurityCheckMethodAdapter(mv);   
  8.          } else if (name.equals("<init>")) {   
  9.              wrappedMv = new ChangeToChildConstructorMethodAdapter(mv,   
  10.                  enhancedSuperName);   
  11.          }   
  12.      }   
  13.      return wrappedMv;   
  14.  }  

這裡 

ChangeToChildConstructorMethodAdapter

将負責把 

Account

的構造函數改造成其子類

Account$EnhancedByASM

的構造函數:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. class ChangeToChildConstructorMethodAdapter extends MethodAdapter {   
  2.      private String superClassName;   
  3.      public ChangeToChildConstructorMethodAdapter(MethodVisitor mv,   
  4.          String superClassName) {   
  5.          super(mv);   
  6.          this.superClassName = superClassName;   
  7.      }   
  8.      public void visitMethodInsn(int opcode, String owner, String name,   
  9.          String desc) {   
  10.          // 調用父類的構造函數時  
  11.          if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) {   
  12.              owner = superClassName;   
  13.          }   
  14.          super.visitMethodInsn(opcode, owner, name, desc);// 改寫父類為 superClassName   
  15.      }   
  16.  }  

最後示範一下如何在運作時産生并裝入産生的 

Account$EnhancedByASM

。 我們定義一個 

Util

 類,作為一個類工廠負責産生有安全檢查的

Account

類:

[java]  view plain  copy  

動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
動态生成java位元組碼之java位元組碼架構asm  AOP 的利器:ASM 3.0 介紹
  1. public class SecureAccountGenerator {   
  2.     private static AccountGeneratorClassLoader classLoader =   
  3.         new AccountGeneratorClassLoade();   
  4.     private static Class secureAccountClass;   
  5.     public Account generateSecureAccount() throws ClassFormatError,   
  6.         InstantiationException, IllegalAccessException {   
  7.         if (null == secureAccountClass) {              
  8.             ClassReader cr = new ClassReader("Account");   
  9.             ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);   
  10.             ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);  
  11.             cr.accept(classAdapter, ClassReader.SKIP_DEBUG);   
  12.             byte[] data = cw.toByteArray();   
  13.             secureAccountClass = classLoader.defineClassFromClassFile(   
  14.                "Account$EnhancedByASM",data);   
  15.         }   
  16.         return (Account) secureAccountClass.newInstance();   
  17.     }   
  18.     private static class AccountGeneratorClassLoader extends ClassLoader {  
  19.         public Class defineClassFromClassFile(String className,   
  20.             byte[] classFile) throws ClassFormatError {   
  21.             return defineClass("Account$EnhancedByASM", classFile, 0,   
  22.             classFile.length());  
  23.         }   
  24.     }   
  25. }  

靜态方法 

SecureAccountGenerator.generateSecureAccount()

在運作時動态生成一個加上了安全檢查的

Account

子類。著名的 Hibernate 和 Spring 架構,就是使用這種技術實作了 AOP 的“無損注入”。

小結

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

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