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 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)级别工作的全部细节。

原文:http://www.ibm.com/developerworks/cn/java/j-dyn0302/index.html

时间: 2024-08-03 22:32:16

Java 编程的动态性,第 6 部分: 利用 Javassist 进行面向方面的更改--转载的相关文章

Java 编程的动态性,第 7 部分: 用 BCEL 设计字节码--转载

在本系列的最后三篇文章中,我展示了如何用 Javassist 框架操作类.这次我将用一种很不同的方法操纵字节码——使用 Apache Byte Code Engineering Library (BCEL).与 Javassist 所支持的源代码接口不同,BCEL 在实际的 JVM 指令层次上进行操作.在希望对程序执行的每一步进行控制时,底层方法使 BCEL 很有用,但是当两者都可以胜任时,它也使 BCEL 的使用比 Javassist 要复杂得多. 我将首先讨论 BCEL 基本体系结构,然后本

Java 编程的动态性--转载

原文地址:http://www.ibm.com/developerworks/cn/java/j-dyn0429/ 本文是这个新系列文章的第一篇,该系列文章将讨论我称之为 Java 编程的动态性的一系列主题.这些主题的范围从 Java 二进制类文件格式的基本结构,以及使用反射进行运行时元数据访问,一直到在运行时修改和构造新类.贯穿整篇文章的公共线索是这样一种思想:在 Java 平台上编程要比使用直接编译成本机代码的语言更具动态性.如果您理解了这些动态方面,就可以使用 Java 编程完成那些在任何

Java 编程的动态性,第 8 部分: 用代码生成取代反射--转载

既然您已经看到了如何使用 Javassist 和 BCEL 框架来进行 classworking (请参阅 本系列以前的一组文章), 我将展示一个实际的 classworking 应用程序.这个应用程序用运行时生成的.并立即装载到 JVM 的类来取代反射.在综合讨论的过程中,我将引用本系列的前两篇文章,以及对 Javassist 和 BCEL 的讨论,这样本文就成为了对这个很长的系列文章的一个很好的总结. 反射的性能 在 第 2 部分, 我展示了无论是对于字段访问还是方法调用,反射都比直接代码慢

Java编程 的动态性,第 2部分: 引入反射--转载

在“ Java编程的动态性,第1部分,”我为您介绍了Java编程类和类装入.该篇文章介绍了一些Java二进制类格式的相关信息.这个月我将阐述使用Java反射API来在运行时接入和使用一些相同信息的基础.为了使已经熟知反射基础的开发人员关注本文,我将在文章中包括反射性能如何与直接接入相比较. 使用反射不同于常规的Java编程,其中它与 元数据--描述其它数据的数据协作.Java语言反射接入的特殊类型的原数据是JVM中类和对象的描述.反射使您能够运行时接入广泛的类信息.它甚至使您能够读写字段,调用运

Java 编程的动态性, 第4部分: 用 Javassist 进行类转换--转载

讲过了 Java 类格式和利用反射进行的运行时访问后,本系列到了进入更高级主题的时候了.本月我将开始本系列的第二部分,在这里 Java 类信息只不过是由应用程序操纵的另一种形式的数据结构而已.我将这个主题的整个内容称为 classworking. 我将以 Javassist 字节码操作库作为对 classworking 的讨论的开始.Javassist 不仅是一个处理字节码的库,而且更因为它的另一项功能使得它成为试验 classworking 的很好的起点.这一项功能就是:可以用 Javassi

Java 编程的动态性,第 5 部分: 动态转换类--转载

在第 4 部分“ 用 Javassist 进行类转换”中,您学习了如何使用 Javassist 框架来转换编译器生成的 Java 类文件,同时写回修改过的类文件.这种类文件转换步骤对于做出持久变更是很理想的,但是如果想要在每次执行应用程序时做出不同的变更,这种方法就不一定很方便.对于这种暂时的变更,采用在您实际启动应用程序时起作用的方法要好得多. JVM 体系结构为我们提供了这样做的便利途径――通过使用 classloader 实现.通过使用 classloader 挂钩(hook),您可以拦截

Java 编程的动态性,第3部分: 应用反射--转载

在 上个月的文章中,我介绍了Java Reflection API,并简要地讲述了它的一些基本功能.我还仔细研究了反射的性能,并且在文章的最后给出了一些指导方针,告诉读者在一个应用程序中何时应该使用反射,何时不应该使用反射.在本月这一期的文章中,我将通过查看一个应用程序来更深入地讨论这一问题,这个应用程序是用于命令行参数处理的一个库,它能够很好地体现反射的强项和弱点. 一开始,在真正进入编写实现代码的工作之前,我将首先定义要解决的问题,然后为这个库设计一个接口.不过,在开发这个库的时候,我并不是

java编程思想 第二章

这篇时间较之前篇章时间靠后,是由于,某一天晚上看完Java编程思想文献之后来不及做笔记了. 以下笔记基本为转载,不是原创 第二章   一切都是对象 目录: 2.1 用引用操纵对象 2.2 必须由你创建所有对象 2.3 永远不需要销毁对象 2.4 创建新的数据类型:类 2.5 方法.参数和返回值 2.6 构建一个Java程序 2.7 你的第一个Java程序 2.8 注释和嵌入式文档 2.9 编码风格 2.1 用引用操纵对象 一切都看作对象,操纵的标识符实际上是对象的一个“引用”,遥控器(引用)操纵

利用记事本和cmd进行java编程(从安装IDE--编译--运行)

java简介 最大特点---跨平台 java编译环境的安装http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 选择Windows x64的jdk下载安装即可 了解jdk的目录结构 JAVA_HOME  :C:\Program Files\Java\jdk1.8.0_102 jre---java虚拟机 bin---命令 管理工具 src.zip压缩包---java开发工具包提供的源码