http://www-128.ibm.com/developerworks/cn/java/j-cwt07065/index.html?ca=dwcn-newsletter-java
无法转向 JDK 5.0?学习一款开放源代码工具如何帮助在旧版 JVM 上使用这些特性

级别: 中级

Dennis Sosnoski
主席, Sosnoski Software Solutions, Inc.
2005 年 7 月 25 日

许多 J2SE 5.0 语言特性应该对旧版 JVM 也有用,但是实现这些特性的编译器会生成需要 JDK 5.0 或更高版本的代码。幸运的是,有一个开放源代码项目 Retroweaver 在 J2SE 5.0 与旧版 JVM 之间架起了一座桥梁。Retroweaver 转换您的类文件以消除 JDK 5.0 依赖性,同时添加其自己的支持函数库以使得大多数 5.0 特性在旧版 JVM 上完全有用。如果您喜欢 J2SE 5.0 语言特性,却无法在运行时使用 JDK 5.0,那么 Retroweaver 就是您所需要的。

J2SE 5.0 为 Java 语言带来了巨大的改变,因此即使是经验丰富的 Java 开发人员也需要深入的培训才能利用 5.0 特性。不幸的是,实现这些语言特性的 JDK 5.0 编译器在生成特定于 JDK 5.0 或更高版本的代码时不支持这些特性。如果试图在早期的 JVM 上运行生成的代码,将会得到 java.lang.UnsupportedClassVersionError 错误。

即使生成的类指定 JDK 5.0 和更高的 JVM,但是故事并没有结束。开发人员发现,一些新特性实际上生成与旧版 JVM 完全兼容的代码,而其他特性可以与标准库的少量扩展兼容。有一个名叫 Toby Reyelts 的开发人员决定消除 JDK 5.0 编译器限制。结果就是开放源代码的 Retroweaver 项目(参见 参考资料)。Retroweaver 使用 classworking 技术来修改由 JDK 5.0 编译器生成的二进制类表示,以便这些类可以与早期的 JVM 一起使用。

对于本文来说,我将展示 Retroweaver 的基本使用。Retroweaver 实际上非常容易使用,所以不用花太大的篇幅去介绍它,所以我还将修改 上个月 介绍的 annotations+ASM 运行时代码生成方法以使用 5.0 之前的 JDK,期间使用了 Retroweaver 来回避 JDK 5.0 编译器限制。

向后兼容 J2SE 5.0
Retroweaver 包含两个逻辑组件:一个字节码增强器和一个运行时库。字节码增强器使用 classworking 技术来修改由 JDK 5.0 编译器生成的类文件,使得这些类可以用于旧版 JVM。作为类文件修改的一部分,Retroweaver 可能需要替换对添加到 J2SE 5.0 中的标准类的引用。实际的替换类包含在运行时库中,以便在您执行修改过的代码时它们是可用的。

按照标准开发周期来说,字节码增强器需要在 Java 代码编译之后、类文件为部署而打包之前运行。在您使用一个 IDE 时,该更改是一个问题 ——“集成”一个类转换工具到“开发环境”是很痛苦的事情,因为 IDE 一般假设它们拥有类文件。限制这一痛苦的一种方式是,只对 IDE 中的大多数测试使用 JDK 5.0。这样,您只需要在想要为部署打包文件或者想要测试实际的部署 JVM 时转换类文件。如果使用 Ant 风格的构建过程,就没有问题;只添加 Retroweaver 字节码增强器作为编译之后的一个步骤。

Retroweaver 具有一个小小的限制:尽管 Retroweaver 允许您在运行在旧版 JVM 上的代码中使用 J2SE 5.0 语言特性,但是它并不支持也包含在 J2SE 5.0 中的所有添加到标准 Java 类的特性。如果您的代码使用任何添加到 J2SE 5.0 中的类或方法,那么就将在试图加载旧版 JVM 中的代码时得到错误,哪怕是在 Retroweaver 处理完成之后也如此。避免对标准库的 J2SE 5.0 添加不应该是一个主要问题,但是如果使用 IDE 中的感应弹出特性并偶然挑选了一个仅添加到 J2SE 5.0 中的方法或类,它就有可能让您得到错误。

它做什么
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 特性。com.sosnoski.dwct.Primitive 是一个针对 Java 语言原语类型的 enum 类。main() 方法使用增强的 for 循环来迭代通过不同的原语,并在当前实例上使用一个简单的 switch 语句来设置每个原语的大小值。

清单 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 1.4 上的注释
既然已经看到了 Retroweaver 如何让您运行在早期 JVM 上的同时在源代码中使用 J2SE 5.0 特性,我将返回到 上个月 的代码。以防您没有阅读上一期,我在此做一个总结:我展示了如何使用 ASM 2.0 基于注释实现运行时类转换,并给出一个注释的特定例子,该注释用于指定 toString() 方法中应该包括哪些字段。

上个月的代码只适用于 JDK 5.0 或更高版本。在本文中,我将修改代码以适用于早期 JVM。与 Retroweaver 一起使用,自动化 toString() 生成的好处将会扩展到许多还停留在 J2SE 5.0 之前运行时的 Java 开发人员。

回忆 ToStringAgent
我用于对 JDK 5.0 实现 toString() 方法生成的 com.sosnoski.asm.ToStringAgent 类对于旧版 JVM 有一个小小的问题:它使用 J2SE 5.0 中新增的 instrumentation API 来在运行时截取类加载和修改类。在早期 JVM 中截取类加载不太灵活,但是并不是不可能 —— 只需要用您自己的版本来取代用于应用程序的类加载器就可以了。由于所有的应用程序类都是通过您的自定义类加载器加载的,所以在它们被实际提供给 JVM 之前,您可以自由地修改类表示。

在上一篇文章中,我使用这种代入自定义类加载器的技术来在运行时修改类(参阅 参考资料)。这里我不想重复背景材料,但是如果您感兴趣的话,可参阅上一篇文章。

更新 上个月 的代码以使用自定义类加载器方法是很容易的。清单 4 展示了带有所有修改的类。该类取代了上一期文章中使用的 com.sosnoski.asm.ToStringAgent 类。上一期中使用到的其他类保持不变。

清单 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 展示了运行与 上个月 相同的测试代码所产生的输出,但是这一次使用了 Retroweaver 和清单 4 中的 ToStringLoader 类,其中命令行换行是为了适应页面宽度)。

清单 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 5.0 版本的结果相同。被处理的类列表几乎是相同的,只是用于截取类加载的技术不同。用于 JDK 1.4 的自定义类加载器方法不提供 JDK 5.0 instrumentation API 的完全灵活性,但是它适用于所有最近的 JVM,并允许您修改任何应用程序类。

结束语
在本期文章中,我展示了如何使用 Retroweaver 来使 J2SE 5.0 Java 代码可运行在旧版 JVM 上。如果您喜欢新的 J2SE 5.0 语言特性,并迫不及待想在自己的应用程序中使用这些特性,那么 Retroweaver 提供了完美的解决方案:您可以马上在开发中开始使用这些语言特性,根本不会影响生产平台。作为 Retroweaver 发挥作用的一个例子,我也 backport 了 上个月 的基于注释的 ToString 生成器,以在早期 JVM 上运行。

对于下个月的文章,我将回到在上一期文章中简要提到的一个问题,即注释与外部配置文件之间的权衡。在配置文件疯狂了很多年之后,整个的 Java 扩展集合似乎都一股脑儿转向使用注释了。但是难道注释总是提供配置类型信息的最佳方式吗?我对此表示怀疑,下个月我将提供一些例子,以及一些我个人的最佳实践指导方针。

参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

  • 单击本文顶部或底部的 代码 图标,下载文中讨论的源代码。

  • 想要开始在旧版 JVM 上使用 J2SE 5.0 语言特性?请直接查看 Retroweaver 项目 的开放源代码。

  • 获得 ASM 这个快速而灵活的 Java 字节码操作框架的所有详细资料。

  • 对 J2SE 5.0 与旧版 Java 平台的区别感兴趣?请查看 John Zukowski 撰写的 驯服 Tiger 系列,了解所有的更改。

  • JSR-175: A Metadata Facility for the Java Programming Language 中找到关于 J2SE 注释的所有信息。

  • 关于使用自定义类加载器在运行时进行类转换的深入讨论,请参阅作者的文章“Java 编程的动态性,第 5 部分: 动态转换类”(developerWorks, 2004 年 2 月)。

  • 不要错过 Dennis Sosnoski 撰写的 Classworking 工具箱 系列中的其他文章。

  • 请参阅 Peter Haggar 撰写的“Java bytecode: Understanding bytecode makes you a better programmer”(developerWorks, 2001 年 7 月),了解关于 Java 字节码设计的更多信息。

  • 同样由 Dennis Sosnoski 撰写的 Java 编程的动态性 系列,将带您漫游 Java 类结构、发射和 classworking。

  • Jikes 开放源代码项目提供了 Java 编程语言的非常快速和高兼容性的编译器。可以使用它来老式地生成字节码 —— 从 Java 源代码生成。

  • 要了解更多关于 Java 技术的信息,请访问 developerWorks Java 专区。您将找到技术文档、how-to 文章、教程、下载、产品信息,以及更多内容。

  • 请访问 New to Java technology 站点,找到帮助您开始 Java 编程的最新资源。

  • 通过参与 developerWorks blogs 加入 developerWorks 社区。