即时相册

    技术2024-05-06  106

    在第4部分“ 使用Javassist进行类转换 ”中,您学习了如何使用Javassist框架转换由编译器生成的Java类文件,并回写修改后的类文件。 这种类型的类文件转换步骤非常适合进行持久更改,但在每次执行应用程序时要进行不同更改时,不一定方便。 对于此类瞬态更改,一种在实际启动应用程序时有效的方法会更好。

    JVM体系结构为我们提供了一种便捷的方式-通过与类加载器实现一起工作。 使用类加载器挂钩,您可以拦截将类加载到JVM的过程,并在实际加载类之前转换类表示形式。 为了说明它是如何工作的,我首先将演示直接拦截类加载,然后展示Javassist如何提供可在应用程序中使用的便捷快捷方式。 在此过程中,我将利用本系列前几篇文章中的文章。

    不要错过本系列的其余部分

    第1部分,“ 类和类加载 ”(2003年4月)

    第2部分,“ 介绍反射 ”(2003年6月)

    第3部分,“ 应用反射 ”(2003年7月)

    第4部分,“ 使用Javassist进行类转换 ”(2003年9月)

    第6部分,“ 使用Javassist进行面向方面的更改 ”(2004年3月)

    第7部分,“ 使用BCEL进行字节码工程 ”(2004年4月)

    第8部分,“ 用代码生成替换反射 ”(2004年6月)

    装罐区

    通常,通过将主类指定为JVM的参数来运行Java应用程序。 这对于标准操作而言效果很好,但是并不能给您任何及时进入类加载过程的方式,以对大多数应用程序有用。 正如我在第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类本身并不是很有用。 为了实现拦截类加载过程的目标,我们需要进一步走一步,为应用程序类定义并使用我们自己的类加载器。

    咨询专家:Dennis Sosnoski关于JVM和字节码的问题

    如果您对本系列文章中涉及的材料以及与Java字节码,Java二进制类格式或一般JVM问题有关的任何其他内容有任何意见或疑问,请访问由Dennis Sosnoski主持的JVM和Bytecode讨论论坛。

    正如我们在第1部分中讨论的那样,类加载器使用树结构层次结构。 每个类加载器(JVM用于核心Java类的根类加载器除外)都有一个父类加载器。 为了避免在一个层次结构中有多个类加载器加载同一类时发生冲突,类加载器应在自己加载类之前先与其父类加载器进行检查。 首先与父级进行检查的过程称为委托 -类加载器将负责将类加载到最接近有权访问该类信息的根的类加载器的责任。

    当清单1中的Run程序开始执行时,它已经被JVM的默认System类加载器加载了(该类加载器可以根据您定义的类路径运行)。 为了遵守类加载的委托规则,我们需要使用所有相同的类路径信息并委派给同一父类,使类加载器真正替代System类加载器。 幸运的是,当前JVM用于System类加载器实现的java.net.URLClassLoader类提供了一种使用getURLs()方法检索类路径信息的简便方法。 要编写我们的类加载器,我们可以子类化java.net.URLClassLoader ,并初始化基类以使用与加载主类的系统类加载器相同的类路径和父类加载器。 清单2给出了这种方法的实际实现:

    清单2.详细的类加载器
    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..."); } } }

    我们使用自己的VerboseLoader类对java.net.URLClassLoader进行了子类化,该类列出了所有要加载的类,并指出该加载器实例(而不是委托父类加载器)加载了哪些类。 在这里,我再次省略了导入和异常以保持代码简洁。

    VerboseLoader类的前两个方法loadClass()和findClass()是标准类加载器方法的替代。 对于从类加载器请求的每个类,都会调用loadClass()方法。 在这种情况下,我们只是将消息打印到控制台,然后调用基类版本进行实际处理。 基类方法实现了标准的类加载器委托行为,首先检查父类加载器是否可以加载请求的类,并且仅在父类加载器失败时才尝试使用受保护的findClass()方法直接加载类。 对于findClass()的VerboseLoader实现,我们首先调用重写的基类实现,然后在调用成功的情况下打印一条消息(返回而不会引发异常)。

    VerboseLoader的main()方法或者从用于包含类的加载器中获取类路径URL列表,或者如果与不是URLClassLoader实例的加载器一起使用,则仅使用当前目录作为唯一的类路径条目。 无论哪种方式,它都会列出实际使用的路径,然后创建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类,它们通过委托通过父类加载器加载。 这些核心Java类中的大多数(如果不是全部)实际上将在VerboseLoader应用程序本身的启动过程中加载,因此父类加载器将仅返回对先前创建的java.lang.Class实例的引用。

    Javassist拦截

    清单2中的VerboseClassloader显示了拦截类加载的基础。 要在加载类时对其进行修改,我们可以更进一步,将代码添加到findClass()方法中以作为资源访问二进制类文件,然后使用二进制数据。 Javassist实际上包括直接执行这种类型的拦截的代码,因此,我们将进一步介绍如何使用Javassist实现,而不是进一步介绍该示例。

    用Javassist拦截类加载是基于我们在第4部分中使用过的同一javassist.ClassPool类构建的。 在那篇文章中,我们直接从ClassPool请求一个按名称命名的类,以javassist.CtClass实例的形式获取ClassPool的Javasist表示形式。 但是,这并不是使用ClassPool的唯一方法-Javassist还提供了一个类加载器, ClassPool加载器以javassist.Loader类的形式使用ClassPool作为其类数据源。

    为了让您在加载类时使用它们, ClassPool使用了一个Observer模式。 您可以将预期的观察者接口的实例javassist.Translator传递给ClassPool的构造函数。 每次从ClassPool请求一个新类时,它都会调用观察者的onWrite()方法,该方法可以在ClassPool传递类之前修改类的表示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包中的正则表达式匹配支持来实现此目的,然后意识到这并没有真正给我我想要的灵活性。 问题在于,对于我来说对于选择要修改的类和方法有意义的模式不适用于正则表达式模型。

    那么什么样的模式是有意义的选择类和方法? 我想要的是能够在模式中使用类和方法的几种特征中的任何一种,包括实际的类和方法名称,返回类型和调用参数类型。 另一方面,我不需要真正灵活的名称和类型比较-简单的equals比较可以处理我感兴趣的大多数情况,并且在比较中添加基本通配符即可解决其余问题。 处理此问题的最简单方法是使模式看起来像标准Java方法声明,并带有一些扩展。

    对于此方法的一些示例,以下是几种与test.StringBuilder类的String buildString(int)方法匹配的模式:

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

    这些模式的一般模式首先是一个可选的返回类型(带有精确文本),然后是组合的类和方法名称模式(带有“ *”通配符),最后是参数类型的列表(带有精确文本) 。 如果存在返回类型,则它必须与方法名称匹配之间用空格隔开,而参数列表位于方法名称匹配之后。 为了使参数匹配变得灵活,我将其设置为以两种方式工作。 如果参数以括号括起来的列表形式给出,则它们必须与方法参数完全匹配。 如果它们被方括号(“ []”)包围,则列出的类型必须全部作为匹配方法的参数存在,但是该方法可以按任何顺序使用它们,也可以使用其他参数。 因此, *buildString(int, java.lang.String)匹配名称以“ buildString”结尾并按顺序恰好采用两个参数int和String任何方法。 *buildString[int,java.lang.String]匹配具有相同名称但使用两个或多个参数的方法,其中一个是int ,另一个是java.lang.String 。

    清单6给出了我为处理这些模式而编写的javassist.Translator子类的简化版本。 实际的匹配代码与本文并没有真正的关系,但是如果您想查看一下或自己使用,则包含在下载文件中(请参阅参考资料 )。 使用此TimingTranslator主程序类是BatchTiming , 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来解锁应用程序中的各个方面。


    翻译自: https://www.ibm.com/developerworks/java/library/j-dyn0203/index.html

    相关资源:JAVA上百实例源码以及开源项目
    Processed: 0.010, SQL: 9