JAVA 位元組碼操作利器javassist
1、簡介
javassist是一個開源的分析、編輯和建立java位元組碼的類庫。不需要了解虛擬機器指令,就能動態生成類或者改變類的結構。
2、下載
(2)使用的版本是javassist-3.18.0-GA。
Javassist是一個執行位元組碼操作的強而有力的驅動程式碼庫。它允許開發者自由的在一個已經編譯好的類中新增新的方法,或者是修改已有的方法。但是,和其他的類似庫不同的是,Javassist並不要求開發者對位元組碼方面具有多麼深入的瞭解,同樣的,它也允許開發者忽略被修改的類本身的細節和結構。
位元組碼驅動通常被用來執行對於已經編譯好的類的修改,或者由程式自動建立執行類等等等等相關方面的操作。這就要求位元組碼引擎具備無論是在執行時或是編譯時都能修改程式的能力。當下有些技術便是使用位元組碼來強化已經存在的Java類的,也有的則是使用它來使用或者產生一些由系統在執行時動態建立的類。舉例而言,JDO1.0規範就使用了位元組碼技術對資料庫中的表進行處理和預編譯,並進而包裝成Java類。特別是在面向物件驅動的系統開發中,相當多的框架體系使用位元組碼以使我們更好的獲得程式的範型性和動態性。而某些EJB容器,比如JBOSS專案,則通過在執行中動態的建立和載入EJB,從而戲劇性的縮短了部署EJB的週期。這項技術是如此的引人入勝,以至於在JDK中也有了標準的java.lang.reflect.Proxy類來執行相關的操作。
但是,儘管如此,編寫位元組碼對於框架程式開發者們而言,卻是一個相當不受歡迎的繁重任務。學習和使用位元組碼在某種程度上就如同使用匯編語言。這使得於大多數開發者而言,儘管在程式上可以獲得相當多的好處,可攀登它所需要的難度則足以冷卻這份熱情。不僅如此,在程式中使用位元組碼操作也大大的降低了程式的可讀性和可維護性。
這是一塊很好的奶油麵包,但是我們卻只能隔著櫥窗流口水…難道我們只能如此了嗎?
所幸的是,我們還有Javassist。Javassist是一個可以執行位元組碼操作的函式庫,可是儘管如此,它卻是簡單而便與理解的。他允許開發者對自己的程式自由的執行位元組碼層的操作,當然了,你並不需要對位元組碼有多深的瞭解,或者,你根本就不需要了解。
二、具體使用
Javassist的最外層的API和JAVA的反射包中的API頗為類似。它使你可以在裝入ClassLoder之前,方便的檢視類的結構。它主要由CtClass,,CtMethod,,以及CtField幾個類組成。用以執行和JDK反射API中java.lang.Class,,java.lang.reflect.Method,, java.lang.reflect.Method .Field相同的操作。這些類可以使你在目標類被載入前,輕鬆的獲得它的結構,函式,以及屬性。此外,不僅僅是在功能上,甚至在結構上,這些類的執行函式也和反射的API大體相同。比如getName,getSuperclass,getMethods,,getSignature,等等。如果你對JAVA的反射機制有所瞭解的話,使用Javassist的這一層將會是輕鬆而快樂的。
接下來我們將給出一個使用Javassist來讀取org.geometry.Point.class的相關資訊的例子(當然了,千萬不要忘記引入javassist.*包):
ClassPool pool = ClassPool.getDefault();
CtClass pt = pool.get("org.geometry.Point");
System.out.println(pt.getSuperclass().getName());
其中,ClassPool是CtClass 的建立工廠。它在class path中查詢CtClass的位置,併為每一個分析請求建立一個CtClass例項。而“getSuperclass().getName()”則展示出org.geometry.Point.class所繼承的父類的名字。但是,和反射的API不盡相同的是,Javassist並不提供構造的能力,換句話說,我們並不能就此得到一個org.geometry.Point.class類的例項。另一方面,在該類沒有例項化前,Javassist也不提供對目標類的函式的呼叫介面和獲取屬性的值的方法。在分析階段,它僅僅提供對目標類的類定義修改,而這點,卻是反射API所無法做到的。
1、目標類和其父類之間的關係
例1:
pt.setSuperclass(pool.get("Figure"));
這樣做將修改目標類和其父類之間的關係。我們將使org.geometry.Point.clas改繼承自Figure類。當然了,就一致性而言,必須確保Figure類和原始的父類之間的相容性。
2、往目標類中新增一個新的方法
而往目標類中新增一個新的方法則更加的簡單了。首先我們來看位元組碼是如何形成的:
CtMethod m = CtNewMethod.make("public int xmove(int dx) { x += dx; }", pt);
pt.addMethod(m);
CtMethod類的讓我們要新增一個方法只需要寫一段小小的函式。這可是一個天大的好訊息,開發者們再也不用為了實現這麼一個小小的操作而寫一大段的虛擬機器指令序列了。Javassist將使用一個它自帶的編譯器來幫我們完成這一切。最後,千萬別忘了指示Javassist把已經寫好的位元組碼存入到你的目標類裡:
pt.writeFile();
writeFile方法可以幫我們把修改好了的定義寫到目標類的.class檔案裡。當然了,我們甚至可以在該目標類載入的時候完成這一切,Javassist可以很好的和ClassLoader協同工作,我們不久就將看到這一點。
3、動態生成一個類
(1)生成的目標類Emp.java
package com.study.javassist;
public class Emp {
private String ename;
private int eno;
public Emp(){
ename="yy";
eno=001;
}
public String getEname() {
return ename;
}
public void setEname(String ename) {
this.ename = ename;
}
public int getEno() {
return eno;
}
public void setEno(int eno) {
this.eno = eno;
}
//新增一個自定義方法
public void printInfo(){
System.out.println("begin!");
System.out.println(ename);
System.out.println(eno);
System.out.println("over!");
}
}
(2)主類GenerateNewClassByJavassist.java
package com.study.javassist;
import java.io.File;
import java.io.FileOutputStream;
import java.lang.reflect.Modifier;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
/**
*使用javassit動態生成一個java類
* @author yy
* @version 1.0
*
*/
public class GenerateNewClassByJavassist {
public static void main(String[] args) throws Exception{
//ClassPool:CtClass物件的容器
ClassPool pool = ClassPool.getDefault();
//通過ClassPool生成一個public新類Emp.java
CtClass ctClass = pool.makeClass("com.study.javassist.Emp");
//新增欄位
//首先新增欄位private String ename
CtField enameField = new CtField(pool.getCtClass("java.lang.String"),"ename",ctClass);
enameField.setModifiers(Modifier.PRIVATE);
ctClass.addField(enameField);
//其次新增欄位privtae int eno
CtField enoField = new CtField(pool.getCtClass("int"),"eno",ctClass);
enoField.setModifiers(Modifier.PRIVATE);
ctClass.addField(enoField);
//為欄位ename和eno新增getXXX和setXXX方法
ctClass.addMethod(CtNewMethod.getter("getEname", enameField));
ctClass.addMethod(CtNewMethod.setter("setEname", enameField));
ctClass.addMethod(CtNewMethod.getter("getEno", enoField));
ctClass.addMethod(CtNewMethod.setter("setEno", enoField));
//新增建構函式
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{}, ctClass);
//為建構函式設定函式體
StringBuffer buffer = new StringBuffer();
buffer.append("{\n")
.append("ename=\"yy\";\n")
.append("eno=001;\n}");
ctConstructor.setBody(buffer.toString());
//把建構函式新增到新的類中
ctClass.addConstructor(ctConstructor);
//新增自定義方法
CtMethod ctMethod = new CtMethod(CtClass.voidType,"printInfo",new CtClass[]{},ctClass);
//為自定義方法設定修飾符
ctMethod.setModifiers(Modifier.PUBLIC);
//為自定義方法設定函式體
StringBuffer buffer2 = new StringBuffer();
buffer2.append("{\nSystem.out.println(\"begin!\");\n")
.append("System.out.println(ename);\n")
.append("System.out.println(eno);\n")
.append("System.out.println(\"over!\");\n")
.append("}");
ctMethod.setBody(buffer2.toString());
ctClass.addMethod(ctMethod);
//為了驗證效果,下面使用反射執行方法printInfo
Class<?> clazz = ctClass.toClass();
Object obj = clazz.newInstance();
obj.getClass().getMethod("printInfo", new Class[]{}).invoke(obj, new Object[]{});
//把生成的class檔案寫入檔案
byte[] byteArr = ctClass.toBytecode();
FileOutputStream fos = new FileOutputStream(new File("D://Emp.class"));
fos.write(byteArr);
fos.close();
}
}
4、Javassist與AOP思想
設計Javassist對目標類的子函式體的操作API的設想立足與ASPect-Oriented Programming(AOP)思想。Javassist允許把具有耦合關係的語句作為一個整體,它允許在一個插入語句中呼叫或獲取其他函式或者及屬性值。它將自動的對這些語句進行優先順序分解並執行巢狀操作。
如下例所示,清單1首先包含了一個CtMethod,它主要針對Screen類的draw方法。然後,我們定義一個Point類,該類有一個move操作,用來實現該Point的移動。當然了,在移動前,我們希望可以通過draw方法得到該point目前的位置,那麼,我們需要對該move方法加增如下的定義:
{ System.out.println("move"); $_ = $proceed($$); }
這樣,在執行move之前,我們就可以打印出它的位置了。請注意這裡的呼叫語句,它是如下格式的:
$_ = $proceed($$);
基與如上情況,CtMethod的關於methord的操作其實被劃分成瞭如下步驟,首先,CtMethod的methord將掃描插入語句(程式碼)本身。一旦發現了子函式,則建立一個ExprEditor例項來分析並執行這個子函式的操作。這個操作將在整個插入語句執行之前完成。而假如這個例項存在某個static的屬性,那麼methord將率先檢測對插入語句進行檢測。然後,在執行插入到目標類---如上例的point類---之前,該static屬性將自動的替換插入語句(程式碼)中所有的相關的部分。不過,值得注意的是,以上的替換操作,將在Javassist把插入語句(程式碼)轉變為位元組碼之後完成。
5、特殊字元
在替換的語句(程式碼)中,我們也有可能需要用到一些特殊變數來完成對某個子函式的呼叫,而這個時候我們就需要使用關鍵字“$”了。在Javassist中,“$”用來申明此後的某個詞為特殊引數,而“$_”則用來申明此後的某個詞為函式的回傳值。每一個特殊引數在被呼叫時應該是這個樣子的“$1,$2,$3…”但是,特別的,目標類本身在被呼叫時,則被表示為“$0”。這種使用格式讓開發者在填寫使用子函式的引數時輕鬆了許多。比如如下的例子:
{ System.out.println("move"); $_ = $proceed($1, 0); }
請注意,該子函式的第2個引數為0。
另外一個特殊型別則是$arg,它實際上是一個容納了函式所有呼叫引數的Object佇列。當Javassist在掃描該$arg時,如果發現某一個引數為JAVA的基本型別,則它將自動的對該引數進行包裝,並放入佇列。比如,當它發現某一個引數為int型別時,它將使用java.lang.integer 類來包裝這個int引數,並存入引數佇列。和Java的反射包:java.lang.reflect.Methord類中的invoke方法相比,$args明顯要省事的多。
6、函式的頭或尾插入程式碼
Javassist也同樣允許開發者在某個函式的頭,或者某個函式的尾上插入某段語句(程式碼)。比如,它有一個insertBefore方法用以在某函式的呼叫前執行某個操作,它的使用大致是這個樣子的:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Screen");
CtMethod cm = cc.getDeclaredMethod("draw", new CtClass[0]);
cm.insertBefore("{ System.out.println($1); System.out.println($2); }");
cc.writeFile();
以上例子允許我們在draw函式呼叫之前執行列印操作---把傳遞給draw的兩個引數打印出來。
7、函式修改或包裝
同樣的,我們也可以使用關鍵字$對某一個函式進行修改或者是包裝,下面就
CtClass cc = sloader.get("Point");
CtMethod m1 = cc.getDeclaredMethod("move");
CtMethod m2 = CtNewMethod.copy(m1, cc, null);
m1.setName(m1.getName() + "_orig");
m2.setBody("{ System.out.println("call"); return $proceed($$);
}", "this", m1.getName());
cc.addMethod(m2);
cc.writeFile();
以上程式碼的前四行不難理解,Javassist首先對Point中的move方法做了個拷貝,並建立了一個新的函式。然後,它把存在與Point類中的原move方法更名為“_orig”。接下來,讓我們關注一下程式第五行中的幾個引數:第一個引數指示該函式的在執行的最初部分需要先列印一段資訊,然後執行子函式proceed()並返回結果,這個和move方法差不多,很好理解。第二個引數則只是申明該子函式所在的類的位置。這裡為this即為Point類本身。第三個引數,也就是“m1.getName()”則定義了這個新函式的名字。
Javassist也同樣具有其他的操作和類來幫助你實現諸如修改某一個屬性的值,改變函式的回值,並在某個函式的執行後補上其他操作的功能。您可以瀏覽www.javassist.org以獲得相關的資訊。
三、其他
以下是一個簡單的完整示例:
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class Test {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
//設定目標類的路徑,確保能夠找到需要修改的類,這裡我指向firestorm.jar
//解包後的路徑
pool.insertClassPath("d:/work/firestorm/firestorm") ;
//獲得要修改的類
CtClass cc = pool.get("com.codefutures.if.if");
//設定方法需要的引數
CtClass[] param = new CtClass[3] ;
param[0] = pool.get("java.security.PublicKey") ;
param[1] = pool.get("byte[]") ;
param[2] = pool.get("byte[]") ;
//得到方法
CtMethod m = cc.getDeclaredMethod("a", param);
//插入新的程式碼
m.insertBefore("{return true ;}") ;
//儲存到檔案裡
cc.writeFile() ;
}
}
結束!