在旧版 JVM 上使用 J2SE 5.0 特性

来源:百度文库 编辑:神马文学网 时间:2024/03/29 16:02:32
如果试图在早期的 JVM 上运行J2SE 5.0 生成的代码,将会得到 java.lang.UnsupportedClassVersionError 错误。开放源代码的 Retroweaver 项目可以消除 JDK 5.0 (Set JDK Compliance = 5.0) 编译器限制.Retroweaver 使用 classworking 技术来修改由 JDK 5.0 编译器生成的二进制类表示,以便这些类可以与早期的 JVM (e.g j2sdk1.4.2) 一起使用。
Retroweaver 包含两个逻辑组件:一个字节码增强器和一个运行时库。字节码增强器使用 classworking 技术来修改由 JDK 5.0 编译器生成的类文件,使得这些类可以用于旧版 JVM。作为类文件修改的一部分,Retroweaver 可能需要替换对添加到 J2SE 5.0 中的标准类的引用。实际的替换类包含在运行时库中,以便在您执行修改过的代码时它们是可用的。
按照标准开发周期来说,字节码增强器需要在 Java 代码编译之后、类文件为部署而打包之前运行。在您使用一个 IDE 时,该更改是一个问题 ——“集成”一个类转换工具到“开发环境”是很痛苦的事情,因为 IDE 一般假设它们拥有类文件。限制这一痛苦的一种方式是,只对 IDE 中的大多数测试使用 JDK 5.0。这样,您只需要在想要为部署打包文件或者想要测试实际的部署 JVM 时转换类文件。如果使用 Ant 风格的构建过程,就没有问题;只添加 Retroweaver 字节码增强器作为编译之后的一个步骤。
Retroweaver 具有一个小小的限制:它并不支持也包含在 J2SE 5.0 中的所有添加到标准 Java 类的特性。如果您的代码使用任何添加到 J2SE 5.0 中的类或方法,那么就将在试图加载旧版 JVM 中的代码时得到错误,哪怕是在 Retroweaver 处理完成之后也如此。
J2SE 5.0 的更改既发生在 JVM 中,也发生在实际的 Java 语言,但是 JVM 更改相当小。有一个新的字符可以用于字节码中的标识符中 ("+"),一些处理类引用的指令发生了修改,还有一个不同的方法用于处理合成组件。 Retroweaver 在字节码增强步骤中处理这些 JVM 更改,方法是把这些更改返回原样,即替换成用于 J2SE 5.0 之前相同目的的方法(比如标识符中的 + 字符,就是用 $ 取代它)。
包含在 J2SE 5.0 中的语言更改要稍微复杂一点。一些最有趣的更改,比如增强的 for 循环,基本上只是语法更改,即为表示编程操作提供快捷方式。比如泛型更改 —— 泛型类型信息 —— 由编译器用于实施编译时安全,但是生成的字节码仍然到处使用强制转换。但是大多数更改使用了添加到核心 Java API 中的类或方法,所以您不能直接使用为 JDK 5.0 生成的字节码并将它直接运行在早期的 JVM 上。Retroweaver 为支持 J2SE 5.0 语言更改所需的新类提供其自己的等价物,并且用对其自己的类的引用替换对标准类的引用,这是字节码增强步骤的一部分。
Retroweaver 字节码增强不能对所有的 J2SE 5.0 语言更改提供完全支持。例如,没有对处理注释的运行时支持,因为运行时支持涉及到对基本 JVM 类加载器实现的更改。但是一般来说,只是不支持那些不会影响普通用户的小特性。
使用 Retroweaver 简直是太容易了。可以使用一个简单的 GUI 界面或者控制台应用程序来在应用程序类文件上运行字节码增强。两种方式都只要在将要转换的类文件树的根目录指出 Retroweaver 即可。在运行时,如果使用任何需要运行时支持的特性(比如 enums),那么就需要在类路径中包含 Retroweaver 运行时 jar。
package com.sosnoski.dwct;public enum Primitive{ BOOLEAN, BYTE, CHARACTER, DOUBLE, FLOAT, INT, LONG, SHORT; public static void main(String[] args) { for (Primitive p : Primitive.values()) { int size = -1; switch (p) { case BOOLEAN: case BYTE: size = 1; break; case CHARACTER: case SHORT: size = 2; break; case FLOAT: case INT: size = 4; break; case DOUBLE: case LONG: size = 8; break; } System.out.println(p + " is size " + size); } }}
使用 JDK 5.0 编译并运行清单 1 代码会给出清单 2 中的输出。但是不能在早期的 JDK 下编译或运行清单 1 代码;由于特定于 J2SE 5.0 的特性会导致编译失败,而运行失败会抛出 java.lang.UnsupportedClassVersionError 异常。
[dennis@notebook code]$ java -cp classes com.sosnoski.dwct.PrimitiveBOOLEAN is size 1BYTE is size 1CHARACTER is size 2DOUBLE is size 8FLOAT is size 4INT is size 4LONG is size 8SHORT is size 2
清单 3 展示了在 Primitive 类上运行 Retroweaver。这个类实际上编译为两个类文件,一个用于 enum 类,另一个支持在 switch 语句中使用 enum。(注意,清单代码换行是为了适应页面宽度。)
[dennis@notebook code]$ java -cp retro/release/retroweaver.jar:retro/lib/bcel-5.1.jar:retro/lib/ jace.jar:retro/lib/Regex.jar com.rc.retroweaver.Weaver -source classes[RetroWeaver] Weaving /home/dennis/writing/articles/devworks/series/may05/code/ classes/com/sosnoski/dwct/Primitive$1.class[RetroWeaver] Weaving /home/dennis/writing/articles/devworks/series/may05/code/ classes/com/sosnoski/dwct/Primitive.class
在运行 Retroweaver 之后,这些类就可以用于 JDK 5.0 和 JDK 1.4 JVM 上了。当使用 1.4 JVM 运行修改后的类时,输出与清单 2 中的相同。Retroweaver 提供命令行选项来指定旧的 1.3 和 1.2 JVM 以取代默认的 1.4 目标,但是我下载的运行时 jar 版本需要 1.4,我不想重新构建它以检查对早期 JVM 的支持。
我用于对 JDK 5.0 实现 toString() 方法生成的 com.sosnoski.asm.ToStringAgent 类对于旧版 JVM 有一个小小的问题:它使用 J2SE 5.0 中新增的 instrumentation API 来在运行时截取类加载和修改类。在早期 JVM 中截取类加载不太灵活,但是并不是不可能 —— 只需要用您自己的版本来取代用于应用程序的类加载器就可以了。由于所有的应用程序类都是通过您的自定义类加载器加载的,所以在它们被实际提供给 JVM 之前,您可以自由地修改类表示。
package com.sosnoski.asm;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;import org.objectweb.asm.ClassReader;import org.objectweb.asm.ClassWriter;public class ToStringLoader extends URLClassLoader{ private ToStringLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } // override of ClassLoader method protected Class findClass(String name) throws ClassNotFoundException { String resname = name.replace(‘.‘, ‘/‘) + ".class"; InputStream is = getResourceAsStream(resname); if (is == null) { System.err.println("Unable to load class " + name + " for annotation checking"); return super.findClass(name); } else { System.out.println("Processing class " + name); try { // read the entire content into byte array ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buff = new byte[1024]; int length; while ((length = is.read(buff)) >= 0) { bos.write(buff, 0, length); } byte[] bytes = bos.toByteArray(); // scan class binary format to find fields for toString() method ClassReader creader = new ClassReader(bytes); FieldCollector visitor = new FieldCollector(); creader.accept(visitor, true); FieldInfo[] fields = visitor.getFields(); if (fields.length > 0) { // annotated fields present, generate the toString() method System.out.println("Modifying " + name); ClassWriter writer = new ClassWriter(false); ToStringGenerator gen = new ToStringGenerator(writer, name.replace(‘.‘, ‘/‘), fields); creader.accept(gen, false); bytes = writer.toByteArray(); } // return the (possibly modified) class return defineClass(bytes, 0, bytes.length); } catch (IOException e) { throw new ClassNotFoundException("Error reading class " + name); } } } 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() }; } // load the target class using custom class loader ToStringLoader loader = new ToStringLoader(urls, base.getParent()); Class clas = loader.loadClass(args[0]); // invoke the "main" method of the application 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 (Exception e) { e.printStackTrace(); } } else { System.out.println("Usage: com.sosnoski.asm.ToStringLoader " + "report-class main-class args..."); } }}
为了使用清单 4 代码,我仍然需要使用 JDK 5.0 编译与注释相关的代码,然后在产生的类集合上运行 Retroweaver。我也需要在类路径中包含 retroweaver.jar 运行时代码(因为 Retroweaver 对已转换的注释使用它自己的类)。
[dennis@notebook code]$ java -cp classes:retro/release/retroweaver-rt.jar:lib/ asm-2.0.RC1.jar:lib/asm-commons-2.0.RC1.jar com.sosnoski.asm.ToStringLoader com.sosnoski.dwct.RunProcessing class com.sosnoski.dwct.RunProcessing class com.sosnoski.dwct.NameModifying com.sosnoski.dwct.NameProcessing class com.sosnoski.dwct.AddressModifying com.sosnoski.dwct.AddressProcessing class com.sosnoski.dwct.CustomerModifying com.sosnoski.dwct.CustomerCustomer: #=12345 Name: Dennis Michael Sosnoski Address: street=1234 5th St. city=Redmond state=WA zip=98052 homePhone=425 555-1212 dayPhone=425 555-1213
清单 5 显示了生成的 toString() 方法的输出. 用于 JDK 1.4 的自定义类加载器方法不提供 JDK 5.0 instrumentation API 的完全灵活性,但是它适用于所有最近的 JVM,并允许您修改任何应用程序类。