Java编程的动态性,第6部分:利用Javassist进行面向方面的更改

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 . BeanTestBean values are originalA and originalBBean values are newA and newB

如果用 清单 1 中的 TranslateConvert 程序运行它并指定监视其中的一个 set 方法, 那么输出将如下所示:

[dennis]$ java -cp .:javassist.jar TranslateConvert Bean setA  BeanTestBean values are originalA and originalBCall to set value newABean 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  BeanTestBean values are A and Bjava.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 BeanTestDissecting 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 originalBBean 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  BeanTestTranslateEdiTor.reverse returning AlanigiroBean values are Alanigiro and originalBTranslateEdiTor.reverse returning AwenBean 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)级别工作的全部细节。

失败是什么?没有什么.只是更走近成功一步,

Java编程的动态性,第6部分:利用Javassist进行面向方面的更改

相关文章:

你感兴趣的文章:

标签云: