`
Weich_JavaDeveloper
  • 浏览: 100075 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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

    博客分类:
  • JAVA
阅读更多
在经过一段时间的休息之后,Dennis Sosnoski 又回来推出了他的 Java 编程的动态性系列的第 5 部分。您已在前面的文章中看到了如何编写用于转换 Java 类文件以改变代码行为的程序。在本期中,Dennis将展示如何使用 Javassist 框架,把转换与实际的类加载过程结合起来,用以进行灵活的“即时”面向方面的特性处理。这种方法允许您决定想要在运行时改变的内容,并潜地在每次运行程序时做出不同的修改。在整个过程中,您还将更深入地了解向JVM 中加载类的一般问题。

在第 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 实现,我们首先调用重载的基类实现,然后在调用成功(在没有抛出异常的情况下返回)时打印一条消息。

VerboseLoadermain() 方法或者从用于包含类的 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.4 java.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)。

分享到:
评论

相关推荐

    java编程200例(附:JAVA文档完全解读中文版)

    Java编程语言是面向对象的、跨平台的编程语言,由Sun Microsystems公司于1995年推出,目前广泛应用于各种领域,如Web开发、移动应用、企业级应用、大数据处理等。"Java编程200例(附:JAVA文档完全解读中文版)"是一...

    洪恩JAVA编程之道

    学习如何创建、继承类,以及多态性的运用,是Java编程的重要部分。 3. **异常处理**:Java有完善的异常处理机制,通过try-catch-finally语句块来捕获和处理程序运行时可能出现的问题。随书代码中会有各种异常处理的...

    02-课件:第1部分:Java基础编程.zip

    本课件“课件:第1部分:Java基础编程”将详细讲解以上知识点,结合项目实例和源代码,使学习者能够快速上手,深入理解Java编程的精髓。通过实践,学习者将能熟练运用Java进行程序开发,为后续的进阶学习打下坚实...

    Java EE Web编程(Eclipse 平台)

    5. **JavaBean**:JavaBean是符合一定规范的Java类,常用于封装业务逻辑。资料可能解释如何创建、使用和管理JavaBean。 6. **MVC(Model-View-Controller)模式**:在Web应用中,MVC模式有助于解耦视图、模型和控制...

    Java类动态加载(一)——java源文件动态编译为class文件

    在Java编程中,类动态加载是一项重要的技术,它允许程序在运行时根据需要加载新的类,从而提高了灵活性和可扩展性。这篇博客“Java类动态加载(一)——java源文件动态编译为class文件”可能主要探讨了如何在运行时...

    02-课件:第2部分:Java高级编程.zip

    在本课程"02-课件:第2部分:Java高级编程.zip"中,宋红康老师深入讲解了Java语言的高级编程概念和技术。这个压缩包包含了一系列课件,旨在帮助学习者掌握Java编程的精髓,提升编程技能。以下是根据课程内容提炼出的...

    java编程100例

    17. 反射:在运行时动态获取类的信息并操作类的对象,增强了程序的灵活性。 九、泛型 18. 泛型:用于在编译时检查类型安全,并且可以消除运行时的类型转换。 十、Java标准库 19. Java API:Java提供丰富的标准库,...

    JAVA编程思想第三版类包

    《JAVA编程思想第三版》是Java编程领域里一本经典的著作,由...这个压缩包可能包含上述所有或部分知识点的代码示例,通过对这些示例的分析和实践,读者可以更好地理解和掌握《JAVA编程思想第三版》中的概念和技巧。

    Java编程标准教程

    《Java编程标准教程》是一本面向初学者及进阶者的权威指南,旨在帮助读者全面掌握Java编程语言。这本书深入浅出地介绍了Java的核心概念和技术,包括J2SE(Java Standard Edition)的基础知识,使得读者能够从零开始...

    java动态性.rar

    Java动态性是Java编程语言中的一个重要特性,它使得程序在运行时可以改变其结构,比如添加、删除或修改类和对象的行为。这一特性使得Java在处理复杂和灵活的问题时具有强大的能力,尤其在开发框架、插件系统以及元...

    java编程思想书本示例和习题答案

    12. **反射**:反射机制允许程序在运行时动态获取类的信息并操作类的对象,提供了强大的动态性。 13. **注解(Annotation)**:注解是Java的元数据,可以为编译器和运行时环境提供额外的信息,用于简化配置、实现...

    java 编程思想java初学

    《Java编程思想》是Java初学者的一本经典教材,它以其深入浅出的讲解和丰富的实例,深受读者喜爱。本书全面覆盖了Java的基础知识,包括语法特性、面向对象编程概念、异常处理、集合框架、多线程、网络编程等多个方面...

    Java编程艺术.rar

    8. **反射**:Java反射机制允许我们在运行时检查和操作类、接口、字段和方法,提供了强大的动态性,常用于框架开发和元编程。 9. **泛型**:泛型引入于Java 5,用于提供类型安全的容器,防止在运行时出现类型转换...

    java 编程思想第四版习题答案

    理解如何通过类来组织代码,以及如何利用接口实现多态性是Java编程的基础。 2. **异常处理**:Java中的异常处理机制允许程序优雅地处理错误。习题可能涵盖try-catch-finally结构,自定义异常,以及如何有效地使用...

    Java习题_java编程_

    Java编程是计算机科学领域中最广泛使用的编程语言之一,尤其在企业级应用开发中占据着核心地位。本资源针对Java语言的基础部分提供了习题训练,主要覆盖了大学Java教材第八章的内容,旨在帮助学习者巩固和深化对Java...

    最新Java 编程详解

    Java编程详解是一个深入探讨Java语言及其应用的领域,特别是针对最新的Java版本。在这个最新的Java编程详解中,我们可能涵盖了许多现代Java开发的关键知识点,包括但不限于以下几个方面: 1. **Java语言基础**:从...

    《Java编程思想》第四版答案

    《Java编程思想》是Bruce Eckel的经典之作,第四版更是被广大Java开发者视为学习和进阶的必备书籍。这本书深入浅出地介绍了Java语言的核心概念和技术,包括面向对象编程、异常处理、多线程、集合框架、网络编程等多...

    java编程开源代码

    以上知识点只是Java编程开源代码实例中可能涵盖的一部分。通过深入研究提供的代码,你可以实践这些概念,加深理解,并提升自己的编程技能。这个开源代码集合是一个宝贵的教育资源,对于任何想要提升Java编程能力的人...

    JAVA编程思想习题及答案

    这份资料通过习题的形式帮助学习者深入理解和掌握Java编程思想,同时提供了解答,使学习过程更具针对性和实践性。 在Java编程中,有几个关键知识点是不容忽视的: 1. **基础语法**:Java是一种面向对象的语言,其...

    java核心编程第十版示例代码

    泛型在集合框架中的应用尤为广泛,理解其原理和使用方法是现代Java编程的必备技能。 6. **I/O流**:Java的I/O流系统用于读写数据,包括文件操作、网络通信等。了解各种流的分类(如字节流、字符流、输入流、输出流...

Global site tag (gtag.js) - Google Analytics