Java 编程的动态性,第 6 部分: 利用 Javassist 进行面向方面的更改--转载
本系列的?第 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 translatorConverterTranslator 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 classString[] 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;)Vat 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 translatorTranslator xlat = new DissectionTranslator();ClassPool pool = ClassPool.getDefault(xlat);Loader loader = new Loader(pool);// invoke the "main" method of the application classString[] 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 BeanTestnew 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 Beanwrite 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 translatorEditorTranslator 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 classString[] 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)級別工作的全部細節。
原文:http://www.ibm.com/developerworks/cn/java/j-dyn0302/index.html
轉載于:https://www.cnblogs.com/davidwang456/p/4035805.html
總結
以上是生活随笔為你收集整理的Java 编程的动态性,第 6 部分: 利用 Javassist 进行面向方面的更改--转载的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 编程的动态性,第 5 部分:
- 下一篇: Java 编程的动态性,第 7 部分: