http://www-128.ibm.com/developerworks/cn/java/j-cwt07065/index.html?ca=dwcn-newsletter-java
無法轉(zhuǎn)向 JDK 5.0?學(xué)習(xí)一款開放源代碼工具如何幫助在舊版 JVM 上使用這些特性

級別: 中級

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

許多 J2SE 5.0 語言特性應(yīng)該對舊版 JVM 也有用,但是實現(xiàn)這些特性的編譯器會生成需要 JDK 5.0 或更高版本的代碼。幸運(yùn)的是,有一個開放源代碼項目 Retroweaver 在 J2SE 5.0 與舊版 JVM 之間架起了一座橋梁。Retroweaver 轉(zhuǎn)換您的類文件以消除 JDK 5.0 依賴性,同時添加其自己的支持函數(shù)庫以使得大多數(shù) 5.0 特性在舊版 JVM 上完全有用。如果您喜歡 J2SE 5.0 語言特性,卻無法在運(yùn)行時使用 JDK 5.0,那么 Retroweaver 就是您所需要的。

J2SE 5.0 為 Java 語言帶來了巨大的改變,因此即使是經(jīng)驗豐富的 Java 開發(fā)人員也需要深入的培訓(xùn)才能利用 5.0 特性。不幸的是,實現(xiàn)這些語言特性的 JDK 5.0 編譯器在生成特定于 JDK 5.0 或更高版本的代碼時不支持這些特性。如果試圖在早期的 JVM 上運(yùn)行生成的代碼,將會得到 java.lang.UnsupportedClassVersionError 錯誤。

即使生成的類指定 JDK 5.0 和更高的 JVM,但是故事并沒有結(jié)束。開發(fā)人員發(fā)現(xiàn),一些新特性實際上生成與舊版 JVM 完全兼容的代碼,而其他特性可以與標(biāo)準(zhǔn)庫的少量擴(kuò)展兼容。有一個名叫 Toby Reyelts 的開發(fā)人員決定消除 JDK 5.0 編譯器限制。結(jié)果就是開放源代碼的 Retroweaver 項目(參見 參考資料)。Retroweaver 使用 classworking 技術(shù)來修改由 JDK 5.0 編譯器生成的二進(jìn)制類表示,以便這些類可以與早期的 JVM 一起使用。

對于本文來說,我將展示 Retroweaver 的基本使用。Retroweaver 實際上非常容易使用,所以不用花太大的篇幅去介紹它,所以我還將修改 上個月 介紹的 annotations+ASM 運(yùn)行時代碼生成方法以使用 5.0 之前的 JDK,期間使用了 Retroweaver 來回避 JDK 5.0 編譯器限制。

向后兼容 J2SE 5.0
Retroweaver 包含兩個邏輯組件:一個字節(jié)碼增強(qiáng)器和一個運(yùn)行時庫。字節(jié)碼增強(qiáng)器使用 classworking 技術(shù)來修改由 JDK 5.0 編譯器生成的類文件,使得這些類可以用于舊版 JVM。作為類文件修改的一部分,Retroweaver 可能需要替換對添加到 J2SE 5.0 中的標(biāo)準(zhǔn)類的引用。實際的替換類包含在運(yùn)行時庫中,以便在您執(zhí)行修改過的代碼時它們是可用的。

按照標(biāo)準(zhǔn)開發(fā)周期來說,字節(jié)碼增強(qiáng)器需要在 Java 代碼編譯之后、類文件為部署而打包之前運(yùn)行。在您使用一個 IDE 時,該更改是一個問題 ——“集成”一個類轉(zhuǎn)換工具到“開發(fā)環(huán)境”是很痛苦的事情,因為 IDE 一般假設(shè)它們擁有類文件。限制這一痛苦的一種方式是,只對 IDE 中的大多數(shù)測試使用 JDK 5.0。這樣,您只需要在想要為部署打包文件或者想要測試實際的部署 JVM 時轉(zhuǎn)換類文件。如果使用 Ant 風(fēng)格的構(gòu)建過程,就沒有問題;只添加 Retroweaver 字節(jié)碼增強(qiáng)器作為編譯之后的一個步驟。

Retroweaver 具有一個小小的限制:盡管 Retroweaver 允許您在運(yùn)行在舊版 JVM 上的代碼中使用 J2SE 5.0 語言特性,但是它并不支持也包含在 J2SE 5.0 中的所有添加到標(biāo)準(zhǔn) Java 類的特性。如果您的代碼使用任何添加到 J2SE 5.0 中的類或方法,那么就將在試圖加載舊版 JVM 中的代碼時得到錯誤,哪怕是在 Retroweaver 處理完成之后也如此。避免對標(biāo)準(zhǔn)庫的 J2SE 5.0 添加不應(yīng)該是一個主要問題,但是如果使用 IDE 中的感應(yīng)彈出特性并偶然挑選了一個僅添加到 J2SE 5.0 中的方法或類,它就有可能讓您得到錯誤。

它做什么
J2SE 5.0 的更改既發(fā)生在 JVM 中,也發(fā)生在實際的 Java 語言,但是 JVM 更改相當(dāng)小。有一個新的字符可以用于字節(jié)碼中的標(biāo)識符中 ("+"),一些處理類引用的指令發(fā)生了修改,還有一個不同的方法用于處理合成組件。 Retroweaver 在字節(jié)碼增強(qiáng)步驟中處理這些 JVM 更改,方法是把這些更改返回原樣,即替換成用于 J2SE 5.0 之前相同目的的方法(比如標(biāo)識符中的 + 字符,就是用 $ 取代它)。

包含在 J2SE 5.0 中的語言更改要稍微復(fù)雜一點(diǎn)。一些最有趣的更改,比如增強(qiáng)的 for 循環(huán),基本上只是語法更改,即為表示編程操作提供快捷方式。比如泛型更改 —— 泛型類型信息 —— 由編譯器用于實施編譯時安全,但是生成的字節(jié)碼仍然到處使用強(qiáng)制轉(zhuǎn)換。但是大多數(shù)更改使用了添加到核心 Java API 中的類或方法,所以您不能直接使用為 JDK 5.0 生成的字節(jié)碼并將它直接運(yùn)行在早期的 JVM 上。Retroweaver 為支持 J2SE 5.0 語言更改所需的新類提供其自己的等價物,并且用對其自己的類的引用替換對標(biāo)準(zhǔn)類的引用,這是字節(jié)碼增強(qiáng)步驟的一部分。

Retroweaver 字節(jié)碼增強(qiáng)不能對所有的 J2SE 5.0 語言更改提供完全支持。例如,沒有對處理注釋的運(yùn)行時支持,因為運(yùn)行時支持涉及到對基本 JVM 類加載器實現(xiàn)的更改。但是一般來說,只是不支持那些不會影響普通用戶的小特性。

Retroweaver 發(fā)揮作用
使用 Retroweaver 簡直是太容易了。可以使用一個簡單的 GUI 界面或者控制臺應(yīng)用程序來在應(yīng)用程序類文件上運(yùn)行字節(jié)碼增強(qiáng)。兩種方式都只要在將要轉(zhuǎn)換的類文件樹的根目錄指出 Retroweaver 即可。在運(yùn)行時,如果使用任何需要運(yùn)行時支持的特性(比如 enums),那么就需要在類路徑中包含 Retroweaver 運(yùn)行時 jar。

清單 1 給出了一個簡單的示例程序,其中使用了一些 J2SE 5.0 特性。com.sosnoski.dwct.Primitive 是一個針對 Java 語言原語類型的 enum 類。main() 方法使用增強(qiáng)的 for 循環(huán)來迭代通過不同的原語,并在當(dāng)前實例上使用一個簡單的 switch 語句來設(shè)置每個原語的大小值。

清單 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 編譯并運(yùn)行清單 1 代碼會給出清單 2 中的輸出。但是不能在早期的 JDK 下編譯或運(yùn)行清單 1 代碼;由于特定于 J2SE 5.0 的特性會導(dǎo)致編譯失敗,而運(yùn)行失敗會拋出 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 類上運(yùn)行 Retroweaver。這個類實際上編譯為兩個類文件,一個用于 enum 類,另一個支持在 switch 語句中使用 enum。(注意,清單代碼換行是為了適應(yīng)頁面寬度。)

清單 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

在運(yùn)行 Retroweaver 之后,這些類就可以用于 JDK 5.0 和 JDK 1.4 JVM 上了。當(dāng)使用 1.4 JVM 運(yùn)行修改后的類時,輸出與 清單 2 中的相同。Retroweaver 提供命令行選項來指定舊的 1.3 和 1.2 JVM 以取代默認(rèn)的 1.4 目標(biāo),但是我下載的運(yùn)行時 jar 版本需要 1.4,我不想重新構(gòu)建它以檢查對早期 JVM 的支持。

JDK 1.4 上的注釋
既然已經(jīng)看到了 Retroweaver 如何讓您運(yùn)行在早期 JVM 上的同時在源代碼中使用 J2SE 5.0 特性,我將返回到 上個月 的代碼。以防您沒有閱讀上一期,我在此做一個總結(jié):我展示了如何使用 ASM 2.0 基于注釋實現(xiàn)運(yùn)行時類轉(zhuǎn)換,并給出一個注釋的特定例子,該注釋用于指定 toString() 方法中應(yīng)該包括哪些字段。

上個月的代碼只適用于 JDK 5.0 或更高版本。在本文中,我將修改代碼以適用于早期 JVM。與 Retroweaver 一起使用,自動化 toString() 生成的好處將會擴(kuò)展到許多還停留在 J2SE 5.0 之前運(yùn)行時的 Java 開發(fā)人員。

回憶 ToStringAgent
我用于對 JDK 5.0 實現(xiàn) toString() 方法生成的 com.sosnoski.asm.ToStringAgent 類對于舊版 JVM 有一個小小的問題:它使用 J2SE 5.0 中新增的 instrumentation API 來在運(yùn)行時截取類加載和修改類。在早期 JVM 中截取類加載不太靈活,但是并不是不可能 —— 只需要用您自己的版本來取代用于應(yīng)用程序的類加載器就可以了。由于所有的應(yīng)用程序類都是通過您的自定義類加載器加載的,所以在它們被實際提供給 JVM 之前,您可以自由地修改類表示。

在上一篇文章中,我使用這種代入自定義類加載器的技術(shù)來在運(yùn)行時修改類(參閱 參考資料)。這里我不想重復(fù)背景材料,但是如果您感興趣的話,可參閱上一篇文章。

更新 上個月 的代碼以使用自定義類加載器方法是很容易的。清單 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 編譯與注釋相關(guān)的代碼,然后在產(chǎn)生的類集合上運(yùn)行 Retroweaver。我也需要在類路徑中包含 retroweaver.jar 運(yùn)行時代碼(因為 Retroweaver 對已轉(zhuǎn)換的注釋使用它自己的類)。清單 5 展示了運(yùn)行與 上個月 相同的測試代碼所產(chǎn)生的輸出,但是這一次使用了 Retroweaver 和清單 4 中的 ToStringLoader 類,其中命令行換行是為了適應(yīng)頁面寬度)。

清單 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 版本的結(jié)果相同。被處理的類列表幾乎是相同的,只是用于截取類加載的技術(shù)不同。用于 JDK 1.4 的自定義類加載器方法不提供 JDK 5.0 instrumentation API 的完全靈活性,但是它適用于所有最近的 JVM,并允許您修改任何應(yīng)用程序類。

結(jié)束語
在本期文章中,我展示了如何使用 Retroweaver 來使 J2SE 5.0 Java 代碼可運(yùn)行在舊版 JVM 上。如果您喜歡新的 J2SE 5.0 語言特性,并迫不及待想在自己的應(yīng)用程序中使用這些特性,那么 Retroweaver 提供了完美的解決方案:您可以馬上在開發(fā)中開始使用這些語言特性,根本不會影響生產(chǎn)平臺。作為 Retroweaver 發(fā)揮作用的一個例子,我也 backport 了 上個月 的基于注釋的 ToString 生成器,以在早期 JVM 上運(yùn)行。

對于下個月的文章,我將回到在上一期文章中簡要提到的一個問題,即注釋與外部配置文件之間的權(quán)衡。在配置文件瘋狂了很多年之后,整個的 Java 擴(kuò)展集合似乎都一股腦兒轉(zhuǎn)向使用注釋了。但是難道注釋總是提供配置類型信息的最佳方式嗎?我對此表示懷疑,下個月我將提供一些例子,以及一些我個人的最佳實踐指導(dǎo)方針。

參考資料

  • 您可以參閱本文在 developerWorks 全球站點(diǎn)上的 英文原文

  • 單擊本文頂部或底部的 代碼 圖標(biāo),下載文中討論的源代碼。

  • 想要開始在舊版 JVM 上使用 J2SE 5.0 語言特性?請直接查看 Retroweaver 項目 的開放源代碼。

  • 獲得 ASM 這個快速而靈活的 Java 字節(jié)碼操作框架的所有詳細(xì)資料。

  • 對 J2SE 5.0 與舊版 Java 平臺的區(qū)別感興趣?請查看 John Zukowski 撰寫的 馴服 Tiger 系列,了解所有的更改。

  • JSR-175: A Metadata Facility for the Java Programming Language 中找到關(guān)于 J2SE 注釋的所有信息。

  • 關(guān)于使用自定義類加載器在運(yùn)行時進(jìn)行類轉(zhuǎn)換的深入討論,請參閱作者的文章“Java 編程的動態(tài)性,第 5 部分: 動態(tài)轉(zhuǎn)換類”(developerWorks, 2004 年 2 月)。

  • 不要錯過 Dennis Sosnoski 撰寫的 Classworking 工具箱 系列中的其他文章。

  • 請參閱 Peter Haggar 撰寫的“Java bytecode: Understanding bytecode makes you a better programmer”(developerWorks, 2001 年 7 月),了解關(guān)于 Java 字節(jié)碼設(shè)計的更多信息。

  • 同樣由 Dennis Sosnoski 撰寫的 Java 編程的動態(tài)性 系列,將帶您漫游 Java 類結(jié)構(gòu)、發(fā)射和 classworking。

  • Jikes 開放源代碼項目提供了 Java 編程語言的非常快速和高兼容性的編譯器。可以使用它來老式地生成字節(jié)碼 —— 從 Java 源代碼生成。

  • 要了解更多關(guān)于 Java 技術(shù)的信息,請訪問 developerWorks Java 專區(qū)。您將找到技術(shù)文檔、how-to 文章、教程、下載、產(chǎn)品信息,以及更多內(nèi)容。

  • 請訪問 New to Java technology 站點(diǎn),找到幫助您開始 Java 編程的最新資源。

  • 通過參與 developerWorks blogs 加入 developerWorks 社區(qū)。