如果试图在早期的 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 发挥作用 使用 Retroweaver 简直是太容易了。可以使用一个简单的 GUI 界面或者控制台应用程序来在应用程序类文件上运行字节码增强。两种方式都只要在将要转换的类文件树的根目录指出 Retroweaver 即可。在运行时,如果使用任何需要运行时支持的特性(比如 enums),那么就需要在类路径中包含 Retroweaver 运行时 jar。 清单 1. 简单的 J2SE 5.0 enum 示例
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 异常。 清单 2. enum 示例输出
[dennis@notebook code]$ java -cp classes com.sosnoski.dwct.Primitive
BOOLEAN is size 1
BYTE is size 1
CHARACTER is size 2
DOUBLE is size 8
FLOAT is size 4
INT is size 4
LONG is size 8
SHORT is size 2
|
清单 3 展示了在 Primitive 类上运行 Retroweaver。这个类实际上编译为两个类文件,一个用于 enum 类,另一个支持在 switch 语句中使用 enum。(注意,清单代码换行是为了适应页面宽度。) 清单 3. 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 之前,您可以自由地修改类表示。 清单 4. ToStringLoader 代码
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 对已转换的注释使用它自己的类)。 清单 5. JDK 1.4 上的 ToString 注释
[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.Run
Processing class com.sosnoski.dwct.Run
Processing class com.sosnoski.dwct.Name
Modifying com.sosnoski.dwct.Name
Processing class com.sosnoski.dwct.Address
Modifying com.sosnoski.dwct.Address
Processing class com.sosnoski.dwct.Customer
Modifying com.sosnoski.dwct.Customer
Customer: #=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,并允许您修改任何应用程序类。
|