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 社區。