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

在第 4 部分“ 用 Javassist 进行类转换”中,您学习了如何使用 Javassist 框架来转换编译器生成的 Java 类文件,同时写回修改过的类文件。这种类文件转换步骤对于做出持久变更是很理想的,但是如果想要在每次执行应用程序时做出不同的变更,这种方法就不一定很方便。对于这种暂时的变更,采用在您实际启动应用程序时起作用的方法要好得多。

JVM 体系结构为我们提供了这样做的便利途径――通过使用 classloader 实现。通过使用 classloader 挂钩(hook),您可以拦截将类加载到 JVM 中的过程,并在实际加载这些类之前转换它们。为了说明这个过程是如何工作的,我将首先展示类加载过程的直接拦截,然后展示 Javassist 如何提供了一种可在您的应用程序中使用的便利捷径。在整个过程中,我将利用取自本系列以前文章中的代码片断。

加载区域

运行 Java 应用程序的通常方式是作为参数向 JVM 指定主类。这对于标准操作没有什么问题,但是它没有提供及时拦截类加载过程的任何途径,而这种拦截对大多数程序来说是很有用的。正如我在第 1 部分“ 类和类装入”中所讨论的,许多类甚至在主类还没有开始执行之前就已经加载了。要拦截这些类的加载,您需要在程序的执行过程中进行某种程度的重定向。

幸运的是,模拟 JVM 在运行应用程序的主类时所做的工作是相当容易的。您所需做的就是使用反射(这是在不得 第 2 部分 中介绍的)来首先找到指定类中的静态 main() 方法,然后使用预期的命令行参数来调用它。清单 1 提供了完成这个任务的示例代码(为简单起见,我省略了导入和异常处理语句):

清单 1. Java 应用程序运行器
public class Run
{
    public static void main(String[] args) {
        if (args.length >= 1) {
            try {

                // load the target class to be run
                Class clas = Run.class.getClassLoader().
                    loadClass(args[0]);

                // invoke "main" method of target class
                Class[] ptypes =
                    new Class[] { args.getClass() };
                Method main =
                    clas.getDeclaredMethod("main", ptypes);
                String[] pargs = new String[args.length-1];
                System.arraycopy(args, 1, pargs, 0, pargs.length);
                main.invoke(null, new Object[] { pargs });

            } catch ...
            }

        } else {
            System.out.println
                ("Usage: Run main-class args...");
        }
    }
}

要使用这个类来运行 Java 应用程序,只需将它指定为 java 命令的目标类,后面跟着应用程序的主类和想要传递给应用程序的其他任何参数。换句话说,如果用于运行 Java 应用程序的命令为:

java test.Test arg1 arg2 arg3

您相应地要通过如下命令使用 Run 类来运行应用程序:

java Run test.Test arg1 arg2 arg3

拦截类加载

就其本身而言,清单 1 中短小的 Run 类不是非常有用。为了实现拦截类加载过程的目标,我们需要采取进一步的动作,对应用程序类定义和使用我们自己的 classloader。

正如我们在第 1 部分中讨论的,classloader 使用一个树状层次结构。每个 classloader(JVM 用于核心 Java 类的根 classloader 除外)都具有一个父 classloader。Classloader 应该在独自加载类之前检查它们的父 classloader,以防止当某个层次结构中的多个 classloader 加载同一个类时可能引发的冲突。首先检查父 classloader 的过程称为 委托――classloader 将加载类的责任委托给最接近根的 classloader,后者能够访问要加载类的信息。

当 清单 1 中的 Run 程序开始执行时,它已经被 JVM 默认的 System classloader(您定义的 classpath 所指定的那一个)加载了。为了符合类加载的委托规则,我们需要对相同的父 classloader 使用完全相同的 classpath 信息和委托,从而使我们的 classloader 成为 System classloader 的真正替代者。幸运的是,JVM 当前用于 System classloader 实现的 java.net.URLClassLoader 类提供了一种检索 classpath 信息的容易途径,它使用了 getURLs() 方法。为了编写 classloader,我们只需从 java.net.URLClassLoader 派生子类,并初始化基类以使用相同的 classpath 和父 classloader 作为加载主类的 System classloader。清单 2 提供了这种方法的具体实现:

清单 2. 一个详细的 classloader
public class VerboseLoader extends URLClassLoader
{
    protected VerboseLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public Class loadClass(String name)
        throws ClassNotFoundException {
        System.out.println("loadClass: " + name);
        return super.loadClass(name);
    }
    protected Class findClass(String name)
        throws ClassNotFoundException {
        Class clas = super.findClass(name);
        System.out.println("findclass: loaded " + name +
            " from this loader");
        return clas;
    }
    public static void main(String[] args) {
        if (args.length >= 1) {
            try {

                // get paths to be used for loading
                ClassLoader base =
                    ClassLoader.getSystemClassLoader();
                URL[] urls;
                if (base instanceof URLClassLoader) {
                    urls = ((URLClassLoader)base).getURLs();
                } else {
                    urls = new URL[]
                        { new File(".").toURI().toURL() };
                }

                // list the paths actually being used
                System.out.println("Loading from paths:");
                for (int i = 0; i < urls.length; i++) {
                    System.out.println(" " + urls[i]);
                }

                // load target class using custom class loader
                VerboseLoader loader =
                    new VerboseLoader(urls, base.getParent());
                Class clas = loader.loadClass(args[0]);

                // invoke "main" method of target class
                Class[] ptypes =
                    new Class[] { args.getClass() };
                Method main =
                    clas.getDeclaredMethod("main", ptypes);
                String[] pargs = new String[args.length-1];
                System.arraycopy(args, 1, pargs, 0, pargs.length);
                Thread.currentThread().
                    setContextClassLoader(loader);
                main.invoke(null, new Object[] { pargs });

            } catch ...
            }

        } else {
            System.out.println
                ("Usage: VerboseLoader main-class args...");
        }
    }
}

我们已从 java.net.URLClassLoader 派生了我们自己的 VerboseLoader 类,它列出正在被加载的所有类,同时指出哪些类是由这个 loader 实例(而不是委托父 classloader)加载的。这里同样为简洁起见而省略了导入和异常处理语句。

VerboseLoader 类中的前两个方法 loadClass() 和 findClass() 重载了标准的 classloader 方法。 loadClass() 方法分别针对 classloader 请求的每个类作了调用。在此例中,我们仅让它向控制台打印一条消息,然后调用它的基类版本来执行实际的处理。基类方法实现了标准 classloader 委托行为,即首先检查父 classloader 是否能够加载所请求的类,并且仅在父 classloader 无法加载该类时,才尝试使用受保护的findClass() 方法来直接加载该类。对于 findClass() 的 VerboseLoader 实现,我们首先调用重载的基类实现,然后在调用成功(在没有抛出异常的情况下返回)时打印一条消息。

VerboseLoader 的 main() 方法或者从用于包含类的 loader 中获得 classpath URL 的列表,或者在与不属于 URLClassLoader 的实例的 loader 一起使用的情况下,简单地使用当前目录作为唯一的 classpath 条目。不管采用哪种方式,它都会列出实际正在使用的路径,然后创建VerboseLoader 类的一个实例,并使用该实例来加载命令行上指定的目标类。该逻辑的其余部分(即查找和调用目标类的 main() 方法)与 清单 1 中的 Run 代码相同。

清单 3 显示了 VerboseLoader 命令行和输出的一个例子,它用于调用清单 1 中的 Run 应用程序:

清单 3. 清单 2 中的程序的例子输出
[dennis]$ java VerboseLoader Run
Loading from paths:
 file:/home/dennis/writing/articles/devworks/dynamic/code5/
loadClass: Run
loadClass: java.lang.Object
findclass: loaded Run from this loader
loadClass: java.lang.Throwable
loadClass: java.lang.reflect.InvocationTargetException
loadClass: java.lang.IllegalAccessException
loadClass: java.lang.IllegalArgumentException
loadClass: java.lang.NoSuchMethodException
loadClass: java.lang.ClassNotFoundException
loadClass: java.lang.NoClassDefFoundError
loadClass: java.lang.Class
loadClass: java.lang.String
loadClass: java.lang.System
loadClass: java.io.PrintStream
Usage: Run main-class args...

在此例中,唯一直接由 VerboseLoader 加载的类是 Run 类。 Run 使用的其他所有类都是核心 Java 类,它们是通过父 classloader 使用委托来加载的。这其中的大多数(如果不是全部的话)核心类实际上都会在 VerboseLoader 应用程序本身的启动期间加载,因此父 classloader 将只返回一个指向先前创建的 java.lang.Class 实例的引用。

Javassist 拦截

清单 2 中的 VerboseClassloader 展示了拦截类加载的基本过程。为了在加载时修改类,我们可以更进一步,向 findClass() 方法添加代码,把二进制类文件当作资源来访问,然后使用该二进制数据。Javassist 实际上包括了直接完成此类拦截的代码,因此与其进一步扩充这个例子,我们不如看看如何使用 Javassist 实现。

使用 Javassist 来拦截类加载的过程要依赖我们在 第 4 部分 中使用的相同 javassist.ClassPool 类。在该文中,我们通过名称直接从ClassPool 请求类,以 javassist.CtClass 实例的形式取回该类的 Javassist 表示。然而,那并不是使用 ClassPool 的唯一方式――Javassist 还以 javassist.Loader 类的形式,提供一个使用 ClassPool 作为其类数据源的 classloader。

为了允许您在加载类时操作它们, ClassPool 使用了一个 Observer 模式。您可以向 ClassPool 的构造函数传递预期的观察者接口(observer interface)的一个实例 javassist.Translator 。每当从 ClassPool 请求一个新的类,它都调用观察者的 onWrite() 方法,这个方法能够在ClassPool 交付类之前修改该类的表示。

javassist.Loader 类包括一个便利的 run() 方法,它加载目标类,并且使用所提供的参数数组来调用该类的 main() 方法(就像在 清单 1 中一样)。清单 4 展示了如何使用 Javassist 类和这个方法来加载和运行目标应用程序类。这个例子中简单的 javassist.Translator 观察者实现仅只是打印一条关于正在被请求的类的消息。

清单 4. Javassist 应用程序运行器
public class JavassistRun
{
    public static void main(String[] args) {
        if (args.length >= 1) {
            try {

                // set up class loader with translator
                Translator xlat = new VerboseTranslator();
                ClassPool pool = ClassPool.getDefault(xlat);
                Loader loader = new Loader(pool);

                // invoke "main" method of target class
                String[] pargs = new String[args.length-1];
                System.arraycopy(args, 1, pargs, 0, pargs.length);
                loader.run(args[0], pargs);

            } catch ...
            }

        } else {
            System.out.println
                ("Usage: JavassistRun main-class args...");
        }
    }

    public static class VerboseTranslator implements Translator
    {
        public void start(ClassPool pool) {}

        public void onWrite(ClassPool pool, String cname) {
            System.out.println("onWrite called for " + cname);
        }
    }
}

下面是 JavassistRun 命令行和输出的一个例子,其中使用它来调用 清单 1 中的 Run 应用程序。

[dennis]$java -cp .:javassist.jar JavassistRun Run
onWrite called for Run
Usage: Run main-class args...

回页首

运行时定时

我们在 第 4 部分中分析过的方法定时修改对于隔离性能问题来说可能一个很有用的工具,但它的确需要一个更灵活的接口。在该文中,我们只是将类和方法名称作为参数传递给程序,程序加载二进制类文件,添加定时代码,然后写回该类。对于本文,我们将把代码转换为使用加载时修改方法,并将它转换为可支持模式匹配,用以指定要定时的类和方法。

在加载类时更改代码以处理这种修改是很容易的。在清单 4 中的 javassist.Translator 代码的基础上,当正在写出的类名称与目标类名称匹配时,我们可以仅从 onWrite() 调用用于添加定时信息的方法。清单 5 展示了这一点(没有包含 addTiming() 的全部细节――请参阅第 4 部分以了解这些细节)。

清单 5. 在加载时添加定时代码
public class TranslateTiming
{
    private static void addTiming(CtClass clas, String mname)
        throws NotFoundException, CannotCompileException {
        ...
    }

    public static void main(String[] args) {
        if (args.length >= 3) {
            try {

                // set up class loader with translator
                Translator xlat =
                    new SimpleTranslator(args[0], args[1]);
                ClassPool pool = ClassPool.getDefault(xlat);
                Loader loader = new Loader(pool);

                // invoke "main" method of target 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: TranslateTiming" +
                " class-name method-mname main-class args...");
        }
    }

    public static class SimpleTranslator implements Translator
    {
        private String m_className;
        private String m_methodName;

        public SimpleTranslator(String cname, String mname) {
            m_className = cname;
            m_methodName = mname;
        }

        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);
                addTiming(clas, m_methodName);
            }
        }
    }
}

模式方法

如清单 5 所示,除了使方法定时代码在加载时工作外,在指定要定时的方法时增加灵活性也是很理想的。我最初使用 Java 1.4java.util.regex 包中的正则表达式匹配支持来实现这点,然后意识到它并没有真正带来我想要的那种灵活性。问题在于,用于选择要修改的类和方法的有意义的模式种类无法很好地适应正则表达式模型。

那么哪种模式对于选择类和方法 意义呢?我想要的是在模式中使用类和方法的任何几个特征的能力,包括实际的类和方法名称、返回类型,以及调用参数类型。另一方面,我不需要对名称和类型进行真正灵活的比较――简单的相等比较就能处理我感兴趣的大多数情况,而对该比较添加基本的通配符就能处理其余的所有情况了。处理这种情况的最容易方法是使模式看起来像标准的 Java 方法声明,另外再进行一些扩展。

关于这种方法的例子,下面是几个与 test.StringBuilder 类的 String buildString(int) 方法相匹配的模式:

java.lang.String test.StringBuilder.buildString(int)
test.StringBuilder.buildString(int)
*buildString(int)
*buildString

这些模式的通用模式首先是一个可选的返回类型(具有精确的文本),然后是组合起来的类和方法名称模式(具有“*”通配字符),最后是参数类型列表(具有精确的文本)。如果提供了返回类型,必须使用一个空格将它与方法名称匹配相隔离,而参数列表则跟在方法名称匹配后面。为了使参数匹配更灵活,我通过两种方式来设置它。如果所给的参数是圆括号括起的列表,它们必须精确匹配方法参数。如果它们是使用方括号(&ldquo;[]&rdquo;)来括起的,所列出的类型全都必须作为匹配方法的参数来提供,不过该方法可以按任何顺序使用它们,并且还可以使用附加的参数。因此 *buildString(int, java.lang.String) 将匹配其名称以“buildString”结尾的任何方法,并且这些方法精确地按顺序接受一个 int 类型和一个 String 类型的参数。 *buildString[int,java.lang.String] 将匹配具有相同名称的方法,但是这些方法接受两个 或更多的 参数,其中一个是 int 类型,另一个是 java.lang.String 类型。

清单 6 给出了我编写来处理这些模式的 javassist.Translator 子类的简略版本。实际的匹配代码与本文并不真正相关,不过如果您想要查看它或亲自使用它,我已将它包括在了下载文件中(请参阅 参考资料)。使用这个 TimingTranslator 的主程序类是 BatchTiming ,它也包括在下载文件中。

清单 6. 模式匹配转换程序
public class TimingTranslator implements Translator
{
    public TimingTranslator(String pattern) {
        // build matching structures for supplied pattern
        ...
    }

    private boolean matchType(CtMethod meth) {
        ...
    }

    private boolean matchParameters(CtMethod meth) {
        ...
    }

    private boolean matchName(CtMethod meth) {
        ...
    }

    private void addTiming(CtMethod meth) {
        ...
    }

    public void start(ClassPool pool) {}
    public void onWrite(ClassPool pool, String cname)
        throws NotFoundException, CannotCompileException {

        // loop through all methods declared in class
        CtClass clas = pool.get(cname);
        CtMethod[] meths = clas.getDeclaredMethods();
        for (int i = 0; i < meths.length; i++) {

            // check if method matches full pattern
            CtMethod meth = meths[i];
            if (matchType(meth) &&
                matchParameters(meth) && matchName(meth)) {

                // handle the actual timing modification
                addTiming(meth);
            }
        }
    }
}

回页首

后续内容

在上两篇文章中,您已经看到了如何使用 Javassist 来处理基本的转换。对于下一篇文章,我们将探讨这个框架的高级特性,这些特性提供用于编辑字节代码的查找和替换技术。这些特性使得对程序行为做出系统性的变更很容易,其中包括诸如拦截所有方法调用或所有字段访问这样的变更。它们是理解为什么 Javassist 是 Java 程序中提供面向方面支持的卓越框架的关键。请下个月再回来看看如何能够使用 Javassist 来揭示应用程序中的方面(aspect)

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

时间: 2024-10-13 22:25:31

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

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

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

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

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

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中的静态代理、通用动态代理类以及原理剖析

代理模式和静态代理 在开发中,代理模式是常用的模式之一,一般来说我们使用的代理模式基本上都是静态代理,实现模式大致如下 : 我们以网络代理为例,简单演示一下静态代理的实现 : // 网络接口 interface Network { public void surfTheInternet(); public void gotoFacebook(); } // 普通网络 class CommonNetwork implements Network { @Override public void su

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

本系列的 第 4 部分和 第 5 部分讨论了如何用 Javassist 对二进制类进行局部更改.这次您将学习以一种更强大的方式使用该框架,从而充分利用 Javassist 对在字节码中查找所有特定方法或者字段的支持.对于 Javassist 功能而言,这个功能至少与它以类似源代码的方式指定字节码的能力同样重要.对选择替换操作的支持也有助于使 Javasssist 成为一个在标准 Java 代码中增加面向方面的编程功能的绝好工具. 第 5 部分介绍了 Javassist 是如何让您拦截类加载过程的

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

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

java 编程思想之7.2.1初始化基类

继承是所有OOP语言不可缺少的能成部分.但继承并不只是复制基类的接口.当创建一个导出类的对象时,该对象包含了一个基类的子对象.这个子对象与你用基类直接创建的对象是一样的.二者的区别在于,后者来自于外部,而基类的子对象被包装在导出类对象内部. 对基类子对象的正确初始化顺序之前不能说不知道,好像所有知道点都一样,只是去看,就觉得自己掌握了,其实不是.那只是当时的自以为.呵呵,眼高手低.今天看到相关的章节,翻翻又是觉得都是自己知道的呀.也许是对同一个知识点有过太多次这种想,于是今天决定不只是看,不只是