首頁  >  文章  >  Java  >  Java 類別載入器的深入探討

Java 類別載入器的深入探討

Y2J
Y2J原創
2017-04-17 14:27:101294瀏覽

類別載入器(class loader)是 Java™中的一個很重要的概念。類別載入器負責載入 Java 類別的位元組程式碼到 Java 虛擬機器中。本文首先詳細介紹了Java 類別載入器的基本概念,包括代理模式、載入類別的具體過程和線程上下文類別載入器等,接著介紹如何開發自己的類別加載器,最後介紹了類別載入器在Web 容器和OSGi™中的應用。

在 IBM Bluemix 雲端平台上開發並部署您的下一個應用程式。
開始您的試用
類別載入器是 Java 語言的創新,也是 Java 語言流行的重要原因之一。它使得 Java 類別可以被動態載入到 Java 虛擬機器中並執行。類別載入器從 JDK 1.0 就出現了,最初是為了滿足 Java Applet 的需要而開發出來的。 Java Applet 需要從遠端下載 Java 類別檔案到瀏覽器中並執行。現在類別載入器在 Web 容器和 OSGi 中得到了廣泛的使用。一般來說,Java 應用程式的開發人員不需要直接同類載入器進行互動。 Java 虛擬機器預設的行為就已經足夠滿足大多數情況的需求了。不過如果遇到了需要與類別載入器互動的情況,而對類別載入器的機制又不是很了解的話,就很容易花大量的時間去調試 ClassNotFoundException
#和NoClassDefFoundError等異常。本文將詳細介紹 Java 的類別載入器,幫助讀者深刻理解 Java 語言中的這個重要概念。以下先介紹一些相關的基本概念。
類別載入器基本概念顧名思義,類別載入器(class loader)用來載入 Java 類別到 Java 虛擬機器中。一般來說,Java 虛擬機器使用 Java 類別的方式如下:Java 原始程式(.java 檔案)在經過 Java 編譯器編譯之後就轉換成 Java 位元組程式碼(.class 檔案)。類別載入器負責讀取 Java 位元組程式碼,並轉換成java.lang.Class類別的一個實例。每個這樣的實例用來表示一個 Java 類別。透過此實例的 newInstance()方法就可以建立出該類別的一個
物件
。實際的情況可能更加複雜,例如 Java 位元組程式碼可能是透過工具動態產生的,也可能是透過網路下載的。 基本上所有的類別載入器都是 java.lang.ClassLoader類別的一個實例。下面詳細介紹這個 Java 類別。
java.lang.ClassLoader**類別介紹**java.lang.ClassLoader類別的基本職責就是根據一個指定的類別的名稱,找到或產生其對應的位元組程式碼,然後從這些位元組程式碼定義出一個Java 類,即java.lang.Class類的一個實例。除此之外,ClassLoader也負責載入 Java 應用程式所需的資源,如映像檔和

設定檔

等。不過本文只討論其載入類別的功能。為了完成載入類別的這個職責,ClassLoader提供了一系列的方法,比較重要的方法如 表 1所示。關於這些方法的細節會在下面介紹。

表1. ClassLoader 中與載入類別相關的方法

#方法說明

##getParent()

#傳回該類別載入器的父類別載入器。

loadClass(String

name)

載入名稱為 name的類,傳回的結果是 java.lang.Class類別的實例。

findClass(String name)

尋找名稱為 name的類,傳回的結果是 java.lang.Class類的實例。

findLoadedClass(String name)

尋找名稱為 name的已經載入過的類,傳回的結果是 java.lang.Class類的實例。

defineClass(String name, byte[] b, int off, int len)

把位元組陣列 b中的內容轉換成Java 類,傳回的結果是java.lang.Class類別的實例。這個方法被宣告為 final

的。

resolveClass(Class6b3d0130bba23ae47fe2b8e8cddf0195 c)

連結指定的 Java 類別。

對於 表 1中給出的方法,表示類別名稱的 name參數的值是類別的二進位名稱。要注意的是內部類別的表示,如 com.example.Sample$1和com.example.Sample$Inner等表示方式。這些方法會在下面介紹類別載入器的工作機制時,做進一步的說明。下面介紹類別載入器的樹狀組織結構。

類別載入器的樹狀組織結構

####

Java 中的類別載入器大致可以分成兩類,一類是系統提供的,另一類則是由 Java 應用開發人員編寫的。系統提供的類別載入器主要有以下三個:
引導類別載入器(bootstrap class loader):它用來載入Java 的核心函式庫,是用原生程式碼來實現的,並不繼承自java.lang.ClassLoader。
擴充類別載入器(extensions class loader):它用來載入 Java 的擴充程式庫。 Java 虛擬機器的實作會提供一個擴充庫目錄。該類別載入器在此目錄裡面尋找並載入 Java 類別。
系統類別載入器(system class loader):它根據 Java 應用的類別路徑(CLASSPATH)來載入 Java 類別。一般來說,Java 應用的類別都是由它來完成載入的。可以透過 ClassLoader.getSystemClassLoader()來取得它。

除了系統提供的類別載入器以外,開發人員可以透過繼承 java.lang.ClassLoader類別的方式實作自己的類別載入器,以滿足一些特殊的需求。
除了引導類別載入器之外,所有的類別載入器都有一個父類別載入器。透過 表 1中給出的 getParent()方法可以得到。對於系統提供的類別載入器來說,系統類別載入器的父類別載入器是擴充類別載入器,而擴充類別載入器的父類別載入器是引導類別載入器;對於開發人員編寫的類別載入器來說,其父類別載入器是載入此類載入器Java 類別的類別載入器。因為類別載入器 Java 類別如同其它的 Java 類別一樣,也是要由類別載入器來載入的。一般來說,開發人員編寫的類別載入器的父類別載入器是系統類別載入器。類別載入器透過這種方式組織起來,形成樹狀結構。樹的根節點就是引導類別載入器。圖 1中給出了一個典型的類別載入器樹狀組織結構示意圖,其中的箭頭指向的是父類別載入器。
圖1. 類別載入器樹狀組織結構示意圖

Java 類別載入器的深入探討

#classload_tree.png


程式碼清單1演示了類別載入器的樹狀組織結構。
清單1. 示範類別載入器的樹狀組織結構

public class ClassLoaderTree {
    public static void main(String[] args) { 
    ClassLoader loader = ClassLoaderTree.class.getClassLoader(); 
    while (loader != null) { 
        System.out.println(loader.toString()); 
        loader = loader.getParent(); 
    } 
    }
}

每個Java 類別都維護著一個指向定義它的類別載入器的引用,透過getClassLoader()方法就可以取得到此引用。程式碼清單 1中透過遞歸呼叫getParent()方法來輸出全部的父類別載入器。程式碼清單 1的運行結果如 程式碼清單 2所示。
清單2. 示範類別載入器的樹狀組織結構的運作結果
sun.misc.Launcher$AppClassLoader@9304b1
sun.misc.Launcher$ExtClassLoader@190d11
#如程式碼清單2所示,第一個輸出的是ClassLoaderTree類別的類別載入器,也就是係統類別載入器。它是 sun.misc.Launcher$AppClassLoader類別的實例;第二個輸出的是擴充類別載入器,是 sun.misc.Launcher$ExtClassLoader類別的實例。要注意的是這裡並沒有輸出引導類別載入器,這是由於有些 JDK 的實作對於父類別載入器是引導類別載入器的情況,getParent()方法傳回 null。
在了解了類別載入器的樹狀組織結構之後,以下介紹類別載入器的代理模式。
類別載入器的代理模式
類別載入器在嘗試自己去尋找某個類別的位元組程式碼並定義它時,會先代理給其父類別載入器,由父類別載入器先去嘗試載入這個類,依序類推。在介紹代理模式背後的動機之前,首先需要先說明一下 Java 虛擬機器是如何判定兩個 Java 類別是相同的。 Java 虛擬機器不僅要看類別的全名是否相同,還要看載入此類的類別載入器是否一樣。只有兩者都相同的情況,才認為兩個類別是相同的。即便是同樣的位元組程式碼,被不同的類別載入器載入之後所得到的類,也是不同的。例如一個 Java 類別 com.example.Sample,編譯之後就產生了位元組程式碼檔案 Sample.class。兩個不同的類別載入器ClassLoaderA和 ClassLoaderB分別讀取了這個 Sample.class文件,並定義出兩個 java.lang.Class類別的實例來表示這個類別。這兩個實例是不相同的。對於 Java 虛擬機器來說,它們是不同的類別。試著對這兩個類別的物件進行相互賦值,會拋出執行時期異常 ClassCastException。下面透過範例來具體說明。程式碼清單 3中給出了 Java 類別 com.example.Sample。
清單 3. com.example.Sample 類別

#
package com.example;
public class Sample {    
    private Sample instance;
    public void setSample(Object instance) { 
    this.instance = (Sample) instance; 
    }
}

如 代码清单 3所示,com.example.Sample类的方法 setSample接受一个 java.lang.Object类型的参数,并且会把该参数强制转换成com.example.Sample类型。测试 Java 类是否相同的代码如 代码清单 4所示。
清单 4. 测试 Java 类是否相同

public void testClassIdentity() {
    String classDataRootPath = "C:\workspace\Classloader\classData";
    
File
SystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
    FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
    String className = "com.example.Sample";
    try {
        Class<?> class1 = fscl1.loadClass(className);
        Object obj1 = class1.newInstance();
        Class<?> class2 = fscl2.loadClass(className);
        Object obj2 = class2.newInstance();
        Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
        setSampleMethod.invoke(obj1, obj2);
    } catch (Exception e) {
        e.printStackTrace();
    }
 }

代码清单 4中使用了类 FileSystemClassLoader的两个不同实例来分别加载类 com.example.Sample,得到了两个不同的java.lang.Class的实例,接着通过 newInstance()方法分别生成了两个类的对象 obj1和 obj2,最后通过 Java 的反射 API 在对象 obj1上调用方法 setSample,试图把对象 obj2赋值给 obj1内部的 instance对象。代码清单 4的运行结果如 代码清单 5所示。
清单 5. 测试 Java 类是否相同的运行结果
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more
从 代码清单 5给出的运行结果可以看到,运行时抛出了 java.lang.ClassCastException异常。虽然两个对象 obj1和 obj2的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被 Java 虚拟机认为是相同的。
了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到,后面会详细介绍。
下面具体介绍类加载器加载类的详细过程。
加载类的过程
在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。
方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是java.lang.NoClassDefFoundError异常。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。
下面讨论另外一种类加载器:线程上下文类加载器。
线程上下文类加载器
線程上下文類別載入器(context class loader)是從 JDK 1.2 開始引入的。類別 java.lang.Thread中的方法 getContextClassLoader()和setContextClassLoader(ClassLoader cl)用來取得和設定執行緒的上下文類別載入器。如果沒有透過setContextClassLoader(ClassLoader cl)方法進行設定的話,則執行緒將繼承其父執行緒的上下文類別載入器。 Java 應用程式運行的初始執行緒的上下文類別載入器是系統類別載入器。在執行緒中運行的程式碼可以透過此類載入器來載入類別和資源。
前面提到的類別載入器的代理模式並不能解決 Java 應用開發中會遇到的類別載入器的全部問題。 Java 提供了許多服務提供者介面(Service Provider Interface,SPI),允許第三方為這些介面提供實作。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的介面由 Java 核心庫來提供,如 JAXP 的 SPI 介面定義包含在 javax.xml.parsers套件中。這些 SPI 的實作程式碼很可能是作為 Java 應用所依賴的 jar 套件被包含進來,可以透過類別路徑(CLASSPATH)來找到,如實作了 JAXP SPI 的 Apache Xerces所包含的 jar 套件。 SPI 介面中的程式碼經常需要載入具體的實作類別。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory類別中的 newInstance()方法是用來產生一個新的DocumentBuilderFactory的實例。這裡的實例的真正的類別是繼承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的實作所提供的。如在 Apache Xerces 中,實作的類別是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而問題在於,SPI 的介面是 Java 核心庫的一部分,是由引導類別載入器來載入的;SPI 實作的 Java 類別一般是由系統類別載入器來載入的。引導類別載入器是無法找到 SPI 的實作類別的,因為它只會載入 Java 的核心函式庫。它也不能代理給系統類別載入器,因為它是系統類別載入器的祖先類別載入器。也就是說,類別載入器的代理模式無法解決這個問題。
線程上下文類別載入器正好解決了這個問題。如果不做任何的設置,Java 應用程式的執行緒的上下文類別載入器預設就是系統上下文類別載入器。在 SPI 介面的程式碼中使用執行緒上下文類別載入器,就可以成功的載入到 SPI 實作的類別。線程上下文類別載入器在很多 SPI 的實作中都會用到。
下面介紹另一個載入類別的方法:Class.forName。
Class.forName
Class.forName是一個靜態方法,同樣可以用來載入類別。此方法有兩種形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。第一種形式的參數 name表示的是類別的全名;initialize表示是否初始化類別;loader表示載入時使用的類別載入器。第二種形式則相當於設定了參數 initialize的值為 true,loader的值為目前類別的類別載入器。 Class.forName的一個很常見的用法是在載入資料庫驅動的時候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用來載入 Apache Derby 資料庫的驅動程式。
在介紹完類別載入器相關的基本概念之後,以下介紹如何開發自己的類別載入器。

開發自己的類別載入器
雖然在絕大多數情況下,系統預設提供的類別載入器實作已經可以滿足需求。但是在某些情況下,您還是需要為應用程式開發自己的類別載入器。例如您的應用程式透過網路來傳輸 Java 類別的位元組程式碼,為了確保安全性,這些位元組程式碼經過了加密處理。這時候您就需要自己的類別載入器來從某個網路位址上讀取加密後的位元組程式碼,接著進行解密和驗證,最後定義出要在 Java 虛擬機器中執行的類別來。下面將透過兩個具體的實例來說明類別載入器的開發。

檔案系統類別載入器
第一個類別載入器用來載入儲存在檔案系統上的 Java 位元組程式碼。完整的實作如 程式碼清單 6所示。

清單 6. 檔案系統類別載入器

public class FileSystemClassLoader extends ClassLoader {
    private String rootDir; 
    public FileSystemClassLoader(String rootDir) { 
    this.rootDir = rootDir; 
} 
    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); 
    } 
} 
    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); 
        } 
        return baos.toByteArray(); 
    } catch (IOException e) { 
        e.printStackTrace(); 
    } 
    return null; 
} 
    private String classNameToPath(String className) { 
    return rootDir + File.separatorChar 
            + className.replace(&#39;.&#39;, 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等異常的時候,應該檢查拋出異常的類別的類別載入器和目前執行緒的上下文類別載入器,從中可以發現問題的所在。在開發自己的類別載入器的時候,需要注意與現有的類別載入器組織結構的協調。

以上是Java 類別載入器的深入探討的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn