【JAVA】深入探討 Java 類加載器2

深入探討 Java 類加載器2 博客分類: j2SE
Java應用服務器OSGI網絡應用Web.
開發自己的類加載器

  我的天,愛死這作者了!!(轉載者淚流滿面ing)

雖然在絕大多數情況下,系統默認提供的類加載器實現已經可以滿足需求。但是在某些情況下,您還是需要爲應用開發出自己的類加載器。比如您的應用通過網絡來傳輸 Java 類的字節代碼,爲了保證安全性,這些字節代碼經過了加密處理。這個時候您就需要自己的類加載器來從某個網絡地址上讀取加密後的字節代碼,接着進行解密和驗證,最後定義出要在 Java 虛擬機中運行的類來。下面將通過兩個具體的實例來說明類加載器的開發。

文件系統類加載器

第一個類加載器用來加載存儲在文件系統上的 Java 字節代碼。完整的實現如 代碼清單 6 所示。


清單 6. 文件系統類加載器

public class FileSystemClassLoader extends ClassLoader {

    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
    //覆蓋ClassLoader 的方法
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        else {
            return defineClass(name, classData, 0, classData.length); //產生Class
        }
    }

    private byte[] getClassData(String className) {         //得到類的系統路徑
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead); //將從文件裏讀到的字節碼寫入StringBuff裏
            }
            return baos.toByteArray(); //返回字節數組
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }
}

 


如 代碼清單 6 所示,類 FileSystemClassLoader 繼承自類 java.lang.ClassLoader。在 表 1 中列出的 java.lang.ClassLoader 類的常用方法中,一般來說,自己開發的類加載器只需要覆寫 findClass(String name) 方法即可。java.lang.ClassLoader 類的方法 loadClass() 封裝了前面提到的代理模式的實現。該方法會首先調用 findLoadedClass() 方法來檢查該類是否已經被加載過;如果沒有加載過的話,會調用父類加載器的 loadClass() 方法來嘗試加載該類;如果父類加載器無法加載該類的話,就調用 findClass() 方法來查找該類。因此,爲了保證類加載器都正確實現代理模式,在開發自己的類加載器時,最好不要覆寫 loadClass() 方法,而是覆寫 findClass() 方法。

類 FileSystemClassLoader 的 findClass() 方法首先根據類的全名在硬盤上查找類的字節代碼文件(.class 文件),然後讀取該文件內容,最後通過 defineClass() 方法來把這些字節代碼轉換成 java.lang.Class 類的實例。

網絡類加載器

   下面將通過一個網絡類加載器來說明如何通過類加載器來實現組件的動態更新。即基本的場景是:Java 字節代碼(.class)文件存放在服務器上,客戶端通過網絡的方式獲取字節代碼並執行。當有版本更新的時候,只需要替換掉服務器上保存的文件即可。通過類加載器可以比較簡單的實現這種需求。

類 NetworkClassLoader 負責通過網絡下載 Java 類字節代碼並定義出 Java 類。它的實現與 FileSystemClassLoader 類似。在通過 NetworkClassLoader 加載了某個版本的類之後,一般有兩種做法來使用它。第一種做法是使用 Java 反射 API。另外一種做法是使用接口。需要注意的是,並不能直接在客戶端代碼中引用從服務器上下載的類,因爲客戶端代碼的類加載器找不到這些類。使用 Java 反射 API 可以直接調用 Java 類的方法。而使用接口的做法則是把接口的類放在客戶端中,從服務器上加載實現此接口的不同版本的類。在客戶端通過相同的接口來使用這些實現類。網絡類加載器的具體代碼見 下載。

在介紹完如何開發自己的類加載器之後,下面說明類加載器和 Web 容器的關係。

回頁首
類加載器與 Web 容器

對於運行在 Java EE™ 容器中的 Web 應用來說,類加載器的實現方式與一般的 Java 應用有所不同。不同的 Web 容器的實現方式也會有所不同。以 Apache Tomcat 來說,每個 Web 應用都有一個對應的類加載器實例。該類加載器也使用代理模式,所不同的是它是首先嚐試去加載某個類,如果找不到再代理給父類加載器。這與一般類加載器的順序是相反的。這是 Java Servlet 規範中的推薦做法,其目的是使得 Web 應用自己的類的優先級高於 Web 容器提供的類。這種代理模式的一個例外是:Java 核心庫的類是不在查找範圍之內的。這也是爲了保證 Java 核心庫的類型安全。

絕大多數情況下,Web 應用的開發人員不需要考慮與類加載器相關的細節。下面給出幾條簡單的原則:

•每個 Web 應用自己的 Java 類文件和使用的庫的 jar 包,分別放在 WEB-INF/classes 和 WEB-INF/lib 目錄下面。
•多個應用共享的 Java 類文件和 jar 包,分別放在 Web 容器指定的由所有 Web 應用共享的目錄下面。
•當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。

 

在介紹完類加載器與 Web 容器的關係之後,下面介紹它與 OSGi 的關係。

 

類加載器與 OSGi

OSGi™ 是 Java 上的動態模塊系統。它爲開發人員提供了面向服務和基於組件的運行環境,並提供標準的方式用來管理軟件的生命週期。OSGi 已經被實現和部署在很多產品上,在開源社區也得到了廣泛的支持。Eclipse 就是基於 OSGi 技術來構建的。

OSGi 中的每個模塊(bundle)都包含 Java 包和類。模塊可以聲明它所依賴的需要導入(import)的其它模塊的 Java 包和類(通過 Import-Package),也可以聲明導出(export)自己的包和類,供其它模塊使用(通過 Export-Package)。也就是說需要能夠隱藏和共享一個模塊中的某些 Java 包和類。這是通過 OSGi 特有的類加載器機制來實現的。OSGi 中的每個模塊都有對應的一個類加載器。它負責加載模塊自己包含的 Java 包和類。當它需要加載 Java 核心庫的類時(以 java 開頭的包和類),它會代理給父類加載器(通常是啓動類加載器)來完成。當它需要加載所導入的 Java 類時,它會代理給導出此 Java 類的模塊來完成加載。模塊也可以顯式的聲明某些 Java 包和類,必須由父類加載器來加載。只需要設置系統屬性 org.osgi.framework.bootdelegation 的值即可。

假設有兩個模塊 bundleA 和 bundleB,它們都有自己對應的類加載器 classLoaderA 和 classLoaderB。在 bundleA 中包含類com.bundleA.Sample,並且該類被聲明爲導出的,也就是說可以被其它模塊所使用的。bundleB 聲明瞭導入 bundleA 提供的類 com.bundleA.Sample,幷包含一個類 com.bundleB.NewSample 繼承自 com.bundleA.Sample。在 bundleB 啓動的時候,其類加載器 classLoaderB 需要加載類 com.bundleB.NewSample,進而需要加載類 com.bundleA.Sample。由於 bundleB 聲明瞭類com.bundleA.Sample 是導入的,classLoaderB 把加載類 com.bundleA.Sample 的工作代理給導出該類的 bundleA 的類加載器 classLoaderA。classLoaderA 在其模塊內部查找類 com.bundleA.Sample 並定義它,所得到的類 com.bundleA.Sample 實例就可以被所有聲明導入了此類的模塊使用。對於以 java 開頭的類,都是由父類加載器來加載的。如果聲明瞭系統屬性org.osgi.framework.bootdelegation=com.example.core.*,那麼對於包 com.example.core 中的類,都是由父類加載器來完成的。

OSGi 模塊的這種類加載器結構,使得一個類的不同版本可以共存在 Java 虛擬機中,帶來了很大的靈活性。不過它的這種不同,也會給開發人員帶來一些麻煩,尤其當模塊需要使用第三方提供的庫的時候。下面提供幾條比較好的建議:

•如果一個類庫只有一個模塊使用,把該類庫的 jar 包放在模塊中,在 Bundle-ClassPath 中指明即可。
•如果一個類庫被多個模塊共用,可以爲這個類庫單獨的創建一個模塊,把其它模塊需要用到的 Java 包聲明爲導出的。其它模塊聲明導入這些類。
•如果類庫提供了 SPI 接口,並且利用線程上下文類加載器來加載 SPI 實現的 Java 類,有可能會找不到 Java 類。如果出現了 NoClassDefFoundError 異常,首先檢查當前線程的上下文類加載器是否正確。通過Thread.currentThread().getContextClassLoader() 就可以得到該類加載器。該類加載器應該是該模塊對應的類加載器。如果不是的話,可以首先通過 class.getClassLoader() 來得到模塊對應的類加載器,再通過Thread.currentThread().setContextClassLoader() 來設置當前線程的上下文類加載器。
 

回頁首
總結

類加載器是 Java 語言的一個創新。它使得動態安裝和更新軟件組件成爲可能。本文詳細介紹了類加載器的相關話題,包括基本概念、代理模式、線程上下文類加載器、與 Web 容器和 OSGi 的關係等。開發人員在遇到 ClassNotFoundException和 NoClassDefFoundError 等異常的時候,應該檢查拋出異常的類的類加載器和當前線程的上下文類加載器,從中可以發現問題的所在。在開發自己的類加載器的時候,需要注意與已有的類加載器組織結構的協調。

 

分享到:   .Tomcat6 使用 NIO | 通過 Tomcat Advanced I/O 獲得高性能的 A ...

發佈了29 篇原創文章 · 獲贊 9 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章