分享

在旧版 JVM 上使用 J2SE 5.0 特性

 SportyBabe 2006-01-19

如果试图在早期的 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,并允许您修改任何应用程序类。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多