Java 顧問 Dennis Sosnoski 在他的關於 Javassist 框架的三期文章中將精華部分留在 了最後。這次他展現了 Javassist 對搜索-替換的支持是如何使對 Java 字節碼的編輯變得 像文本編輯器的“替換所有(Replace All )”命令一樣容易的。想報告所有寫入特定字段 的內容或者對方法調用中參數的更改中的補丁嗎?Javassist 使這變得很容易,Dennis 向您 展示了其做法。
本系列的 第 4 部分和 第 5 部分討論了如何用 Javassist 對二進制類進行局部更改。 這次您將學習以一種更強大的方式使用該框架,從而充分利用 Javassist 對在字節碼中查找 所有特定方法或者字段的支持。對於 Javassist 功能而言,這個功能至少與它以類似源代碼 的方式指定字節碼的能力同樣重要。對選擇替換操作的支持也有助於使 Javasssist 成為一 個在標准 Java 代碼中增加面向方面的編程功能的絕好工具。
第 5 部分介紹了 Javassist 是如何讓您攔截類加載過程的 ―― 甚至在二進制類表示正 在被加載的時候對它們進行更改。這篇文章中討論的系統字節碼轉換可以用於靜態類文件轉 換,也可以用於運行時攔截,但是在運行時使用尤其有用。
處理字節碼修改
Javassist 提供了兩種不同的系統字節碼修改的處理方法。第一種技術是使用 javassist.CodeConverter 類,使用起來要稍微簡單一些,但是可以完成的任務有很多限制 。第二種技術使用 javassist.ExprEditor 類的自定義子類,它稍微復雜一些,但是所增加 的靈活性足以抵銷所付出的努力。在本文中我將分析這兩種方法的例子。
代碼轉換
系統字節碼修改的第一種 Javassist 技術使用 javassist.CodeConverter 類。要利用這 種技術,只需要創建 CodeConverter 類的一個實例並用一個或者多個轉換操作配置它。每一 個轉換都是用識別轉換類型的方法調用來配置的。轉換類型可分為三類:方法調用轉換、字 段訪問轉換和新對象轉換。
清單 1 給出了使用方法調用轉換的一個例子。在這個例子中,轉換只是增加了一個方法 正在被調用的通知。在代碼中,首先得到將要使用的 javassist.ClassPool 實例,將它配置 為與一個翻譯器一同工作 (正如在前面 第 5 部分 所看到的)。然後,通過 ClassPool 訪 問兩個方法定義。第一個方法定義針對的是要監視的“set”類型的方法(類和方法名來自命 令行參數),第二個方法定義針對的是 reportSet() 方法 ,它位於TranslateConvert 類中 ,並會報告對第一個方法的調用。
有了方法信息後,就可以用 CodeConverterinsertBeforeMethod() 配置一個轉換,以在 每次調用這個 set 方法之前增加一個對報告方法的調用。然後所要做的就是將這個轉換器應 用到一個或者多個類上。在清單 1 的代碼中,我是通過調用類對象的 instrument() 方法, 在 ConverterTranslator 內部類的 onWrite() 方法中完成這項工作的。這將自動對從 ClassPool 實例中加載的每一個類應用這個轉換。
清單 1. 使用 CodeConverter
public class TranslateConvert
{
public static void main(String[] args) {
if (args.length >= 3) {
try {
// set up class loader with translator
ConverterTranslator xlat =
new ConverterTranslator();
ClassPool pool = ClassPool.getDefault(xlat);
CodeConverter convert = new CodeConverter();
CtMethod smeth = pool.get(args[0]).
getDeclaredMethod(args[1]);
CtMethod pmeth = pool.get("TranslateConvert").
getDeclaredMethod("reportSet");
convert.insertBeforeMethod(smeth, pmeth);
xlat.setConverter(convert);
Loader loader = new Loader(pool);
// invoke "main" method of application class
String[] pargs = new String[args.length-3];
System.arraycopy(args, 3, pargs, 0, pargs.length);
loader.run(args[2], pargs);
} catch ...
}
} else {
System.out.println("Usage: TranslateConvert " +
"clas-name set-name main-class args...");
}
}
public static void reportSet(Bean target, String value) {
System.out.println("Call to set value " + value);
}
public static class ConverterTranslator implements Translator
{
private CodeConverter m_converter;
private void setConverter(CodeConverter convert) {
m_converter = convert;
}
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {
CtClass clas = pool.get(cname);
clas.instrument(m_converter);
}
}
}
配置轉換是一個相當復雜的操作,但是設置好以後,在它工作時就不用費什麼心了。清單 2 給出了代碼示例,可以作為測試案例。這裡 Bean 提供了具有類似 bean 的 get 和 set 方法的測試對象, BeanTest 程序用這些方法來訪問值。
清單 2. 一個 bean 測試程序
public class Bean
{
private String m_a;
private String m_b;
public Bean() {}
public Bean(String a, String b) {
m_a = a;
m_b = b;
}
public String getA() {
return m_a;
}
public String getB() {
return m_b;
}
public void setA(String string) {
m_a = string;
}
public void setB(String string) {
m_b = string;
}
}
public class BeanTest
{
private Bean m_bean;
private BeanTest() {
m_bean = new Bean("originalA", "originalB");
}
private void print() {
System.out.println("Bean values are " +
m_bean.getA() + " and " + m_bean.getB());
}
private void changeValues(String lead) {
m_bean.setA(lead + "A");
m_bean.setB(lead + "B");
}
public static void main(String[] args) {
BeanTest inst = new BeanTest();
inst.print();
inst.changeValues("new");
inst.print();
}
}
如果直接運行清單 2 中的 中的 BeanTest 程序,則輸出如下:
[dennis]$ java -cp . BeanTest
Bean values are originalA and originalB
Bean values are newA and newB
如果用 清單 1 中的 TranslateConvert 程序運行它並指定監視其中的一個 set 方法, 那麼輸出將如下所示:
[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest
Bean values are originalA and originalB
Call to set value newA
Bean values are newA and newB
每項工作都與以前一樣,但是現在在執行這個程序時,所選的方法被調用時會有一個通知 。
在這個例子中,可以用其他的方法容易地實現同樣的效果,例如通過使用 第 4 部分 中 的技術在實際的 set 方法體中增加代碼。這裡的區別是,在使用位置增加代碼讓我有了靈活 性。例如,可以容易地修改 TranslateConvert.ConverterTranslatoronWrite() 方法來檢查 正在加載的類名,並只轉換在我想要監視的類的清單中列出的類。直接在 set 方法體中添加 代碼無法進行這種有選擇的監視。
系統字節碼轉換由於提供了靈活性而使其成為為標准 Java 代碼實現面向方面的擴展的強 大工具。在本文後面您會看到更多這方面的內容。
轉換限制
由 CodeConverter 處理的轉換很有用,但是有局限性。例如,如果希望在調用目標方法 之前或者之後調用一個監視方法,那麼這個監視方法必須定義為 static void 並且必須先接 受一個目標方法的類的參數,然後是與目標方法所要求的同樣數量和類型的參數。
這種嚴格的結構意味著監視方法需要與目標類和方法完全匹配。舉一個例子,假設我改變 了 清單 1 中 reportSet() 方法的定義,讓它接受一個一般性的 java.lang.Object 參數, 想使它可以用於不同的目標類:
public static void reportSet(Object target, String value) {
System.out.println("Call to set value " + value);
}
編譯沒有問題,但是當我運行它時它就會中斷:
[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA BeanTest
Bean values are A and B
java.lang.NoSuchMethodError: TranslateConvert.reportSet (LBean;Ljava/lang/String;)V
at BeanTest.changeValues(BeanTest.java:17)
at BeanTest.main(BeanTest.java:23)
at ...
有辦法繞過這種限制。一種解決方案是在運行時實際生成與目標方法相匹配的自定義監視 方法。不過這要做很多工作,在本文中我不打算試驗這種方法。幸運的是,Javassist 還提 供了另一種處理系統字節碼轉換的方法。這種方法使用 javassist.ExprEditor ,與 CodeConverter 相比,它更靈活、也更強大。
容易的類剖析
用 CodeConverter 進行字節碼轉換與用 javassist.ExprEditor 的原理一樣。不過, ExprEditor 方式也許更難理解一些,所以我首先展示基本原理,然後再加入實際的轉換。
清單 3 顯示了如何用 ExprEditor 來報告面向方面的轉換的可能目標的基本項目。這裡 我在自己的 VerboseEditor 中派生了 ExprEditor 子類,重寫了三個基本的類方法 ―― 它 們的名字都是 edit() ,但是有不同的參數類型。如 清單 1 中的代碼,我實際上是在 DissectionTranslator 內部類的 onWrite() 方法中使用這個子類,對從 ClassPool 實例中 加載的每一個類,在對類對象的 instrument() 方法的調用中傳遞一個實例。
清單 3. 一個類剖析程序
public class Dissect
{
public static void main(String[] args) {
if (args.length >= 1) {
try {
// set up class loader with translator
Translator xlat = new DissectionTranslator();
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);
// invoke the "main" method of the application class
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
loader.run(args[0], pargs);
} catch (Throwable ex) {
ex.printStackTrace();
}
} else {
System.out.println
("Usage: Dissect main-class args...");
}
}
public static class DissectionTranslator implements Translator
{
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {
System.out.println("Dissecting class " + cname);
CtClass clas = pool.get(cname);
clas.instrument(new VerboseEditor());
}
}
public static class VerboseEditor extends ExprEditor
{
private String from(Expr expr) {
CtBehavior source = expr.where();
return " in " + source.getName() + "(" + expr.getFileName() + ":" +
expr.getLineNumber() + ")";
}
public void edit(FieldAccess arg) {
String dir = arg.isReader() ? "read" : "write";
System.out.println(" " + dir + " of " + arg.getClassName () +
"." + arg.getFieldName() + from(arg));
}
public void edit(MethodCall arg) {
System.out.println(" call to " + arg.getClassName() + "." +
arg.getMethodName() + from(arg));
}
public void edit(NewExpr arg) {
System.out.println(" new " + arg.getClassName() + from (arg));
}
}
}
清單 4 顯示了對 清單 2 中的 BeanTest 程序運行清單 3 中的 Dissect 程序所產生的 輸出。它給出了加載的每一個類的每一個方法中所做的工作的詳細分析,列出了所有方法調用 、字段訪問和新對象創建。
清單 4. 已剖析的 BeanTest
[dennis]$ java -cp .:javassist.jar Dissect BeanTest
Dissecting class BeanTest
new Bean in BeanTest(BeanTest.java:7)
write of BeanTest.m_bean in BeanTest(BeanTest.java:7)
read of java.lang.System.out in print(BeanTest.java:11)
new java.lang.StringBuffer in print(BeanTest.java:11)
call to java.lang.StringBuffer.append in print(BeanTest.java:11)
read of BeanTest.m_bean in print(BeanTest.java:11)
call to Bean.getA in print(BeanTest.java:11)
call to java.lang.StringBuffer.append in print(BeanTest.java:11)
call to java.lang.StringBuffer.append in print(BeanTest.java:11)
read of BeanTest.m_bean in print(BeanTest.java:11)
call to Bean.getB in print(BeanTest.java:11)
call to java.lang.StringBuffer.append in print(BeanTest.java:11)
call to java.lang.StringBuffer.toString in print(BeanTest.java:11)
call to java.io.PrintStream.println in print(BeanTest.java:11)
read of BeanTest.m_bean in changeValues(BeanTest.java:16)
new java.lang.StringBuffer in changeValues(BeanTest.java:16)
call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16)
call to java.lang.StringBuffer.append in changeValues(BeanTest.java:16)
call to java.lang.StringBuffer.toString in changeValues (BeanTest.java:16)
call to Bean.setA in changeValues(BeanTest.java:16)
read of BeanTest.m_bean in changeValues(BeanTest.java:17)
new java.lang.StringBuffer in changeValues(BeanTest.java:17)
call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17)
call to java.lang.StringBuffer.append in changeValues(BeanTest.java:17)
call to java.lang.StringBuffer.toString in changeValues (BeanTest.java:17)
call to Bean.setB in changeValues(BeanTest.java:17)
new BeanTest in main(BeanTest.java:21)
call to BeanTest.print in main(BeanTest.java:22)
call to BeanTest.changeValues in main(BeanTest.java:23)
call to BeanTest.print in main(BeanTest.java:24)
Dissecting class Bean
write of Bean.m_a in Bean(Bean.java:10)
write of Bean.m_b in Bean(Bean.java:11)
read of Bean.m_a in getA(Bean.java:15)
read of Bean.m_b in getB(Bean.java:19)
write of Bean.m_a in setA(Bean.java:23)
write of Bean.m_b in setB(Bean.java:27)
Bean values are originalA and originalB
Bean values are newA and newB
通過在 VerboseEditor 中實現適當的方法,可以容易地增加對報告強制類型轉換、 instanceof 檢查和 catch 塊的支持。但是只列出有關這些組件項的信息有些乏味,所以讓 我們來實際修改項目吧。
進行剖析
清單 4對類的剖析列出了基本組件操作。容易看出在實現面向方面的功能時使用這些操作 會多麼有用。例如,報告對所選字段的所有寫訪問的記錄器(logger)在許多應用程序中都 會發揮作用。無論如何,我已經承諾要為您介紹如何完成 這類工作。
幸運的是,就本文討論的主題來說, ExprEditor 不但讓我知道代碼中有什麼操作,它還 讓我可以修改所報告的操作。在不同的 ExprEditor.edit() 方法調用中傳遞的參數類型分別 定義一種 replace() 方法。如果向這個方法傳遞一個普通 Javassist 源代碼格式的語句( 在 第 4 部分中介紹),那麼這個語句將編譯為字節碼,並且用來替換原來的操作。這使對 字節碼的切片和切塊變得容易。
清單 5 顯示了一個代碼替換的應用程序。在這裡我不是記錄操作,而是選擇實際修改存 儲在所選字段中的 String 值。在 FieldSetEditor 中,我實現了匹配字段訪問的方法簽名 。在這個方法中,我只檢查兩樣東西:字段名是否是我所查找的,操作是否是一個存儲過程 。找到匹配後,就用使用實際的 TranslateEditor 應用程序類中 reverse() 方法調用的結 果來替換原來的存儲。 reverse() 方法就是將原來字符串中的字母順序顛倒並輸出一條消息 表明它已經使用過了。
清單 5. 顛倒字符串集
public class TranslateEditor
{
public static void main(String[] args) {
if (args.length >= 3) {
try {
// set up class loader with translator
EditorTranslator xlat =
new EditorTranslator(args[0], new FieldSetEditor(args [1]));
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);
// invoke the "main" method of the application class
String[] pargs = new String[args.length-3];
System.arraycopy(args, 3, pargs, 0, pargs.length);
loader.run(args[2], pargs);
} catch (Throwable ex) {
ex.printStackTrace();
}
} else {
System.out.println("Usage: TranslateEditor clas-name " +
"field-name main-class args...");
}
}
public static String reverse(String value) {
int length = value.length();
StringBuffer buff = new StringBuffer(length);
for (int i = length-1; i >= 0; i--) {
buff.append(value.charAt(i));
}
System.out.println("TranslateEditor.reverse returning " + buff);
return buff.toString();
}
public static class EditorTranslator implements Translator
{
private String m_className;
private ExprEditor m_editor;
private EditorTranslator(String cname, ExprEditor editor) {
m_className = cname;
m_editor = editor;
}
public void start(ClassPool pool) {}
public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {
if (cname.equals(m_className)) {
CtClass clas = pool.get(cname);
clas.instrument(m_editor);
}
}
}
public static class FieldSetEditor extends ExprEditor
{
private String m_fieldName;
private FieldSetEditor(String fname) {
m_fieldName = fname;
}
public void edit(FieldAccess arg) throws CannotCompileException {
if (arg.getFieldName().equals(m_fieldName) && arg.isWriter()) {
StringBuffer code = new StringBuffer();
code.append("$0.");
code.append(arg.getFieldName());
code.append("=TranslateEditor.reverse($1);");
arg.replace(code.toString());
}
}
}
}
如果對 清單 2 中的 BeanTest 程序運行清單 5 中的 TranslateEditor 程序,結果如下 :
[dennis]$ java -cp .:javassist.jar TranslateEditor Bean m_a BeanTest
TranslateEditor.reverse returning Alanigiro
Bean values are Alanigiro and originalB
TranslateEditor.reverse returning Awen
Bean values are Awen and newB
我成功地在每一次存儲到 Bean.m_a 字段時,加入了一個對添加的代碼的調用(一次是在 構造函數中,一次是在 set 方法中)。我可以通過對從字段的加載實現類似的修改而得到反 向的效果,不過我個人認為顛倒值比開始使用的值有意思得多,所以我選擇使用它們。
包裝 Javassist
本文介紹了用 Javassist 可以容易地完成系統字節碼轉換。將本文與上兩期文章結合在 一起,您應該有了在 Java 應用程序中實現自己面向方面的轉換的堅實基礎,這個轉換過程 可以作為單獨的編譯步驟,也可以在運行時完成。
要想對這種方法的強大之處有更好的了解,還可以分析用 Javassis 建立的 JBoss Aspect Oriented Programming Project (JBossAOP)。JBossAOP 使用一個 XML 配置文件來 定義在應用程序類中完成的所有不同的操作。其中包括對字段訪問或者方法調用使用攔截器 ,在現有類中添加 mix-in 接口實現等。JBossAOP 將被加入正在開發的 JBoss 應用程序服 務器版本中,但是也可以在 JBoss 以外作為單獨的工具提供給應用程序使用。
本系列的下一步將介紹 Byte Code Engineering Library (BCEL),這是 Apache Software Foundation 的 Jakarta 項目的一部分。BCEL 是 Java classworking 最廣泛使用 的一種框架。它使用與我們在最近這三篇文章中看到的 Javassist 方法的不同方法處理字節 碼,注重個別的字節碼指令而不是 Javassist 所強調的源代碼級別的工作。下個月將分析在 字節碼匯編器(assembler)級別工作的全部細節。