java类的修改三个方面_Java 编程的动态性,第 6 部分: 利用 Javassist 进行面向方面的更改--转载...
本系列的?第 4 部分和?第 5 部分討論了如何用 Javassist 對二進制類進行局部更改。這次您將學(xué)習(xí)以一種更強大的方式使用該框架,從而充分利用 Javassist 對在字節(jié)碼中查找所有特定方法或者字段的支持。對于 Javassist 功能而言,這個功能至少與它以類似源代碼的方式指定字節(jié)碼的能力同樣重要。對選擇替換操作的支持也有助于使 Javasssist 成為一個在標(biāo)準(zhǔn) Java 代碼中增加面向方面的編程功能的絕好工具。
第 5 部分介紹了 Javassist 是如何讓您攔截類加載過程的 ―― 甚至在二進制類表示正在被加載的時候?qū)λ鼈冞M行更改。這篇文章中討論的系統(tǒng)字節(jié)碼轉(zhuǎn)換可以用于靜態(tài)類文件轉(zhuǎn)換,也可以用于運行時攔截,但是在運行時使用尤其有用。
處理字節(jié)碼修改
Javassist 提供了兩種不同的系統(tǒng)字節(jié)碼修改的處理方法。第一種技術(shù)是使用?javassist.CodeConverter?類,使用起來要稍微簡單一些,但是可以完成的任務(wù)有很多限制。第二種技術(shù)使用?javassist.ExprEditor?類的自定義子類,它稍微復(fù)雜一些,但是所增加的靈活性足以抵銷所付出的努力。在本文中我將分析這兩種方法的例子。
代碼轉(zhuǎn)換
系統(tǒng)字節(jié)碼修改的第一種 Javassist 技術(shù)使用?javassist.CodeConverter?類。要利用這種技術(shù),只需要創(chuàng)建?CodeConverter?類的一個實例并用一個或者多個轉(zhuǎn)換操作配置它。每一個轉(zhuǎn)換都是用識別轉(zhuǎn)換類型的方法調(diào)用來配置的。轉(zhuǎn)換類型可分為三類:方法調(diào)用轉(zhuǎn)換、字段訪問轉(zhuǎn)換和新對象轉(zhuǎn)換。
清單 1 給出了使用方法調(diào)用轉(zhuǎn)換的一個例子。在這個例子中,轉(zhuǎn)換只是增加了一個方法正在被調(diào)用的通知。在代碼中,首先得到將要使用的?javassist.ClassPool?實例,將它配置為與一個翻譯器一同工作 (正如在前面?第 5 部分?所看到的)。然后,通過?ClassPool?訪問兩個方法定義。第一個方法定義針對的是要監(jiān)視的“set”類型的方法(類和方法名來自命令行參數(shù)),第二個方法定義針對的是?reportSet()?方法?,它位于TranslateConvert?類中,并會報告對第一個方法的調(diào)用。
有了方法信息后,就可以用?CodeConverterinsertBeforeMethod()?配置一個轉(zhuǎn)換,以在每次調(diào)用這個 set 方法之前增加一個對報告方法的調(diào)用。然后所要做的就是將這個轉(zhuǎn)換器應(yīng)用到一個或者多個類上。在清單 1 的代碼中,我是通過調(diào)用類對象的?instrument()?方法,在ConverterTranslator?內(nèi)部類的?onWrite()?方法中完成這項工作的。這將自動對從?ClassPool?實例中加載的每一個類應(yīng)用這個轉(zhuǎn)換。
清單 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);
}
}
}
配置轉(zhuǎn)換是一個相當(dāng)復(fù)雜的操作,但是設(shè)置好以后,在它工作時就不用費什么心了。清單 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?程序運行它并指定監(jiān)視其中的一個 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
每項工作都與以前一樣,但是現(xiàn)在在執(zhí)行這個程序時,所選的方法被調(diào)用時會有一個通知。
在這個例子中,可以用其他的方法容易地實現(xiàn)同樣的效果,例如通過使用?第 4 部分?中的技術(shù)在實際的 set 方法體中增加代碼。這里的區(qū)別是,在使用位置增加代碼讓我有了靈活性。例如,可以容易地修改?TranslateConvert.ConverterTranslatoronWrite()?方法來檢查正在加載的類名,并只轉(zhuǎn)換在我想要監(jiān)視的類的清單中列出的類。直接在 set 方法體中添加代碼無法進行這種有選擇的監(jiān)視。
系統(tǒng)字節(jié)碼轉(zhuǎn)換由于提供了靈活性而使其成為為標(biāo)準(zhǔn) Java 代碼實現(xiàn)面向方面的擴展的強大工具。在本文后面您會看到更多這方面的內(nèi)容。
轉(zhuǎn)換限制
由?CodeConverter?處理的轉(zhuǎn)換很有用,但是有局限性。例如,如果希望在調(diào)用目標(biāo)方法之前或者之后調(diào)用一個監(jiān)視方法,那么這個監(jiān)視方法必須定義為?static void?并且必須先接受一個目標(biāo)方法的類的參數(shù),然后是與目標(biāo)方法所要求的同樣數(shù)量和類型的參數(shù)。
這種嚴(yán)格的結(jié)構(gòu)意味著監(jiān)視方法需要與目標(biāo)類和方法完全匹配。舉一個例子,假設(shè)我改變了?清單 1?中?reportSet()?方法的定義,讓它接受一個一般性的?java.lang.Object?參數(shù),想使它可以用于不同的目標(biāo)類:
public static void reportSet(Object target, String value) {
System.out.println("Call to set value " + value);
}
編譯沒有問題,但是當(dāng)我運行它時它就會中斷:
[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 ...
有辦法繞過這種限制。一種解決方案是在運行時實際生成與目標(biāo)方法相匹配的自定義監(jiān)視方法。不過這要做很多工作,在本文中我不打算試驗這種方法。幸運的是,Javassist 還提供了另一種處理系統(tǒng)字節(jié)碼轉(zhuǎn)換的方法。這種方法使用?javassist.ExprEditor?,與?CodeConverter?相比,它更靈活、也更強大。
容易的類剖析
用?CodeConverter?進行字節(jié)碼轉(zhuǎn)換與用?javassist.ExprEditor?的原理一樣。不過,?ExprEditor?方式也許更難理解一些,所以我首先展示基本原理,然后再加入實際的轉(zhuǎn)換。
清單 3 顯示了如何用?ExprEditor?來報告面向方面的轉(zhuǎn)換的可能目標(biāo)的基本項目。這里我在自己的?VerboseEditor?中派生了?ExprEditor?子類,重寫了三個基本的類方法 ―― 它們的名字都是?edit()?,但是有不同的參數(shù)類型。如?清單 1?中的代碼,我實際上是在DissectionTranslator?內(nèi)部類的?onWrite()?方法中使用這個子類,對從?ClassPool?實例中加載的每一個類,在對類對象的?instrument()?方法的調(diào)用中傳遞一個實例。
清單 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?程序所產(chǎn)生的輸出。它給出了加載的每一個類的每一個方法中所做的工作的詳細分析,列出了所有方法調(diào)用、字段訪問和新對象創(chuàng)建。
清單 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?中實現(xiàn)適當(dāng)?shù)姆椒?#xff0c;可以容易地增加對報告強制類型轉(zhuǎn)換、?instanceof?檢查和?catch?塊的支持。但是只列出有關(guān)這些組件項的信息有些乏味,所以讓我們來實際修改項目吧。
進行剖析
清單 4對類的剖析列出了基本組件操作。容易看出在實現(xiàn)面向方面的功能時使用這些操作會多么有用。例如,報告對所選字段的所有寫訪問的記錄器(logger)在許多應(yīng)用程序中都會發(fā)揮作用。無論如何,我已經(jīng)承諾要為您介紹如何完成?這類工作。
幸運的是,就本文討論的主題來說,?ExprEditor?不但讓我知道代碼中有什么操作,它還讓我可以修改所報告的操作。在不同的ExprEditor.edit()?方法調(diào)用中傳遞的參數(shù)類型分別定義一種?replace()?方法。如果向這個方法傳遞一個普通 Javassist 源代碼格式的語句(在?第 4 部分中介紹),那么這個語句將編譯為字節(jié)碼,并且用來替換原來的操作。這使對字節(jié)碼的切片和切塊變得容易。
清單 5 顯示了一個代碼替換的應(yīng)用程序。在這里我不是記錄操作,而是選擇實際修改存儲在所選字段中的?String?值。在?FieldSetEditor中,我實現(xiàn)了匹配字段訪問的方法簽名。在這個方法中,我只檢查兩樣?xùn)|西:字段名是否是我所查找的,操作是否是一個存儲過程。找到匹配后,就用使用實際的?TranslateEditor?應(yīng)用程序類中?reverse()?方法調(diào)用的結(jié)果來替換原來的存儲。?reverse()?方法就是將原來字符串中的字母順序顛倒并輸出一條消息表明它已經(jīng)使用過了。
清單 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?程序,結(jié)果如下:
[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?字段時,加入了一個對添加的代碼的調(diào)用(一次是在構(gòu)造函數(shù)中,一次是在 set 方法中)。我可以通過對從字段的加載實現(xiàn)類似的修改而得到反向的效果,不過我個人認為顛倒值比開始使用的值有意思得多,所以我選擇使用它們。
包裝 Javassist
本文介紹了用 Javassist 可以容易地完成系統(tǒng)字節(jié)碼轉(zhuǎn)換。將本文與上兩期文章結(jié)合在一起,您應(yīng)該有了在 Java 應(yīng)用程序中實現(xiàn)自己面向方面的轉(zhuǎn)換的堅實基礎(chǔ),這個轉(zhuǎn)換過程可以作為單獨的編譯步驟,也可以在運行時完成。
要想對這種方法的強大之處有更好的了解,還可以分析用 Javassis 建立的 JBoss Aspect Oriented Programming Project (JBossAOP)。JBossAOP 使用一個 XML 配置文件來定義在應(yīng)用程序類中完成的所有不同的操作。其中包括對字段訪問或者方法調(diào)用使用攔截器,在現(xiàn)有類中添加 mix-in 接口實現(xiàn)等。JBossAOP 將被加入正在開發(fā)的 JBoss 應(yīng)用程序服務(wù)器版本中,但是也可以在 JBoss 以外作為單獨的工具提供給應(yīng)用程序使用。
本系列的下一步將介紹 Byte Code Engineering Library (BCEL),這是 Apache Software Foundation 的 Jakarta 項目的一部分。BCEL 是 Java classworking 最廣泛使用的一種框架。它使用與我們在最近這三篇文章中看到的 Javassist 方法的不同方法處理字節(jié)碼,注重個別的字節(jié)碼指令而不是 Javassist 所強調(diào)的源代碼級別的工作。下個月將分析在字節(jié)碼匯編器(assembler)級別工作的全部細節(jié)。
原文:http://www.ibm.com/developerworks/cn/java/j-dyn0302/index.html
總結(jié)
以上是生活随笔為你收集整理的java类的修改三个方面_Java 编程的动态性,第 6 部分: 利用 Javassist 进行面向方面的更改--转载...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 年营收实现1000亿元!TCL李东生新目
- 下一篇: java linux 字体设置_Linu