Java類載入器是使用者程式和JVM虛擬機器之間的橋樑,在Java程式中起了至關重要的作用,理解它有利於我們寫出更優雅的程式。本文首先介紹了Java虛擬機器載入程式的過程,簡述了Java類載入器的載入方式(雙親委派模式),然後介紹了幾種常見的類載入器及其適用場景,最後則一個例子展示瞭如何自定義類載入器。本文很多地方參考了java官方文件關於虛擬機器載入的教程,點此直達官方參考文件

基本概念

基本檔案型別和概念

常見概念介紹:

  1. java原始檔(.java):.java是Java的原始檔字尾,裡面存放程式設計師編寫的功能程式碼,只是一個文字檔案,不能被java虛擬機器所識別, 但是java語法有其自身的語法規範要求,不符合規範的java程式應該在編譯期間報錯。

  2. java位元組碼檔案(.class):可以由java檔案通過 javac這個命令(jdk本身提供的工具)編譯生成,本質上是一種二進位制檔案,這個檔案可以由java虛擬機器載入(類載入),然後進java解釋執行, 這也就是執行你的程式。

    java位元組碼檔案(.class檔案)看起來有點多餘,為什麼java虛擬機器不能直接執行java原始碼呢?主要是為了實現 多語言支援性:java虛擬機器本身只識別.class檔案,所以任何語言(python、go等)只要有合適的直譯器解釋為.class檔案,就可以在java虛擬機器上執行。下文為java官方對於Class檔案和虛擬機器關係之間的描述原文。

    The Java Virtual Machine knows nothing of the Java programming language, only of a particular binary format, the class file format. A class file contains Java Virtual Machine instructions (or bytecodes) and a symbol table, as well as other ancillary information. For the sake of security, the Java Virtual Machine imposes strong syntactic and structural constraints on the code in a class file. However, any language with functionality that can be expressed in terms of a valid class file can be hosted by the Java Virtual Machine. Attracted by a generally available, machine-independent platform, implementors of other languages can turn to the Java Virtual Machine as a delivery vehicle for their languages.

  3. java虛擬機器:Java Virtual Machine(縮寫為JVM),僅識別.class檔案,可以把.class檔案載入到記憶體中,生成對應的java物件。還有記憶體管理、程式優化、鎖管理等功能。所有的java程式最終都執行在jvm之上。下文為java官方對於JAVA虛擬機器的描述資訊

    The Java Virtual Machine is the cornerstone of the Java platform. It is the component of the technology responsible for its hardware- and operating systemindependence, the small size of its compiled code, and its ability to protect users from malicious programs. The Java Virtual Machine is an abstract computing machine. Like a real computing machine, it has an instruction set and manipulates various memory areas at run time. It is reasonably common to implement a programming language using a virtual machine;

idea程式示例

下文將用idea中的java專案示例對Java 源程式、 Java 位元組碼、類例項分別進行示範:

idea-java原始檔

通常來說,我們在idea中寫的java程式都屬於java源程式,idea會把檔案的[.java]字尾隱藏掉。我們也可以使用任何文字編輯器編寫生成[.java]檔案。下圖展示了一個典型的JAVA檔案

idea-java位元組碼

java檔案是不能被java虛擬機器所識別的,需要翻譯為位元組碼檔案才可以被java虛擬機器接受。idea中可以直接點選build專案按鈕實現原始檔解釋為位元組碼的過程(本質是通過java中的javac工具實現)。

idea-類載入

在idea中新建java的主類,並在主類中觸發測試類的類載入流程(如new一個測試類),通過斷點的方式可以檢視到載入好的類的資訊。

類載入器介紹

類載入器的作用

由上文中的流程圖可以看出,類載入器負責讀取 Java 位元組程式碼(.class 檔案),並轉換成 java.lang.Class 類的一個例項。每個這樣的例項用來表示一個 Java 類。通過此例項的 newInstance() 方法就可以創建出該類的一個物件。實際的情況可能更加複雜,比如 Java 位元組程式碼可能是通過工具動態生成的,也可能是通過網路下載的。

虛擬機器設計團隊把類載入階段中的“通過一個類的全限定名來獲取描述此類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需要的類。實現這個動作的程式碼模組稱為“類載入器”。

類載入的時機

java類載入使用動態類載入機制, 程式在啟動的時候,並不會一次性載入程式所要用的所有class檔案,而是根據程式的需要,通過Java的類載入機(ClassLoader)來動態載入某個class檔案到記憶體當中的,從而只有class檔案被載入到了記憶體之後,才能被其它class所引用。JVM執行過程中,首先會載入初始類,然後再從初始類連結觸發它相關的類的載入。

注意:圖中的“引用”指觸發類載入,一共有以下幾種情況會觸發類載入:

  1. 建立類的例項 訪問類的靜態變數(注意:當訪問類的靜態並且final修飾的變數時,不會觸發類的初始化。),或者為靜態變數賦值。

  2. 呼叫類的靜態方法(注意:呼叫靜態且final的成員方法時,會觸發類的初始化!一定要和靜態且final修飾的變數區分開!!)

  3. 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。如:Class.forName("bacejava.Langx");

  4. 注意通過類名.class得到Class檔案物件並不會觸發類的載入。 初始化某個類的子類

  5. 直接使用java.exe命令來執行某個主類(java.exe執行,本質上就是呼叫main方法,所以必須要有main方法才行)。

    java官方對於類載入的描述:The Java Virtual Machine starts up by creating an initial class or interface using the bootstrap class loader or a user-defined class loader . The Java Virtual Machine then links the initial class or interface, initializes it, and invokes the public static method void main(String[]). The invocation of this method drives all further execution. Execution of the Java Virtual Machine instructions constituting the main method may cause linking (and consequently creation) of additional classes and interfaces, as well as invocation of additional methods.

    The initial class or interface is specified in an implementation-dependent manner. For example, the initial class or interface could be provided as a command line argument. Alternatively, the implementation of the Java Virtual Machine could itself provide an initial class that sets up a class loader which in turn loads an application. Other choices of the initial class or interface are possible so long as they are consistent with the specification given in the previous paragraph.

類載入器的意義

類載入器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態載入到 Java 虛擬機器中並執行。類載入器從 JDK 1.0 就出現了,最初是為了滿足 Java Applet 的需要而開發出來的。Java Applet 需要從遠端下載 Java 類檔案到瀏覽器中並執行。現在類載入器在 Web 容器和 OSGi 中得到了廣泛的使用。一般來說,Java 應用的開發人員不需要直接同類載入器進行互動。Java 虛擬機器預設的行為就已經足夠滿足大多數情況的需求了。不過如果遇到了需要與類載入器進行互動的情況,而對類載入器的機制又不是很瞭解的話,就很容易花大量的時間去除錯 ClassNotFoundException 和 NoClassDefFoundError 等異常。

類載入的基本流程

1.載入:載入是通過類載入器(classLoader)完成的,它既可以是餓漢式eagerly load載入類(預載入),也可以是懶載入lazy load(執行時載入)

2.驗證:確保.class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。 驗證階段是否嚴謹,直接決定了Java虛擬機器是否能承受惡意程式碼的攻擊。 從整體上看,驗證階段大致上會完成下面四個階段的檢驗動作:檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證。

3.準備:準備階段的主要任務是如下兩點:為類變數分配記憶體;設定類變數初始值

4.解析:解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程

5.初始化:初始化階段即虛擬機器執行類構造器<clinit>()方法的過程。

6.使用:正常使用類資訊

7.解除安裝:滿足類解除安裝條件時(比較苛刻),jvm會從記憶體中解除安裝對應的類資訊

oracle官網對於類載入只粗略劃分為了三個階段,載入(包含上圖中的載入、驗證和準備)、連結和初始化,以下為java官方對於類載入的描述資訊

The Java Virtual Machine dynamically loads, links and initializes classes and interfaces. Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation. Linking is the process of taking a class or interface and combining it into the run-time state of the Java Virtual Machine so that it can be executed. Initialization of a class or interface consists of executing the class or interface initialization method <clinit>

類載入器詳細介紹

生成類物件的三種方法

oracle官網把類載入器劃分為兩種型別:啟動類載入器(BootStrapClassloader)和使用者自定義類載入器,使用者自定義載入器都繼承自ClassLoad類。啟動類載入器主要用於載入一些核心java庫,如rt.jar。使用者自定義載入器則可以載入各種來源的class檔案。以下為java官方對於類載入器生成方式的描述資訊。

>There are two kinds of class loaders: the bootstrap class loader supplied by the Java Virtual Machine, and user-defined class loaders.Every user-defined class loader is an instance of a subclass of the abstract class ClassLoader. Applications employ user-defined class loaders in order to extend the manner in which the Java Virtual Machine dynamically loads and thereby creates classes. User-defined class loaders can be used to create classes that originate from user-defined sources. For example, a class could be downloaded across a network, generated on the fly, or extracted from an encrypted file.

陣列本身也是一個物件,但是這個物件對應的類不通過類載入器載入,而是通過JVM生成。以下為java官方對於陣列物件的描述資訊

>Array classes do not have an external binary representation; they are created by the Java Virtual Machine rather than by a class loader.

綜上所述:類的生成方式一共有三種:

  1. 啟動類載入器

  2. 使用者自定義類載入器

  3. JVM生成陣列物件

    The Java Virtual Machine uses one of three procedures to create class or interface C denoted by N:

    • If N denotes a nonarray class or an interface, one of the two following methods is used to load and thereby create C:

    – If D was defined by the bootstrap class loader, then the bootstrap class loader initiates loading of C .

    – If D was defined by a user-defined class loader, then that same user-defined class loader initiates loading of C.

    • Otherwise N denotes an array class. An array class is created directly by the Java Virtual Machine, not by a class loader. However, the defining class loader of D is used in the process of creating array class C.

啟動類載入器

啟動類載入器主要載入的是JVM自身需要的類,這個類載入使用C++語言實現的,是虛擬機器自身的一部分,它負責將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath引數指定的路徑下的jar包載入到記憶體中,注意必由於虛擬機器是按照檔名識別載入jar包的,如rt.jar,如果檔名不被虛擬機器識別,即使把jar包丟到lib目錄下也是沒有作用的(出於安全考慮,Bootstrap啟動類載入器只加載包名為java、javax、sun等開頭的類)。

雙親委派模型中,如果一個類載入器的父類載入器為null,則表示該類載入器的父類載入器是啟動類載入器

Bootstrap class loader. It is the virtual machine's built-in class loader, typically represented as null, and does not have a parent.

The following steps are used to load and thereby create the nonarray class or interface C denoted by N using the bootstrap class loader. First, the Java Virtual Machine determines whether the bootstrap class loader has already been recorded as an initiating loader of a class or interface denoted by N. If so, this class or interface is C, and no class creation is necessary. Otherwise, the Java Virtual Machine passes the argument N to an invocation of a method on the bootstrap class loader to search for a purported representation of C in a platform-dependent manner. Typically, a class or interface will be represented using a file in a hierarchical file system, and the name of the class or interface will be encoded in the pathname of the file. Note that there is no guarantee that a purported representation found is valid or is a representation of C. This phase of loading must detect the following error:

• If no purported representation of C is found, loading throws an instance of

ClassNotFoundException.

使用者自定義類載入器

使用者自定義類載入器可以分為兩種型別:

  1. java庫中的平臺類載入器和應用程式類載入器等
  2. 使用者自己寫的類載入器,比如通過網路載入類等機制

陣列類載入器

陣列的Class類是由jvm生成的,但是陣列類的Class.getClassLoader() 和陣列元素的類載入器保持一致,如果陣列的元素是基本型別,那麼陣列類的類載入器會為空。

Class objects for array classes are not created by class loaders, but are created automatically as required by the Java runtime. The class loader for an array class, as returned by Class.getClassLoader() is the same as the class loader for its element type; if the element type is a primitive type, then the array class has no class loader.

使用者自定義類載入器介紹

本章節會詳細介紹下圖中的各個類載入器:

基本類載入器ClassLoader

參考文件:https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html

ClassLoader 類是所有類載入器的基類。ClassLoader 類基本職責就是根據一個指定的類的名稱,找到或者生成其對應的位元組程式碼,然後從這些位元組程式碼中定義出一個 Java 類,即 java.lang.Class 類的一個例項。除此之外, ClassLoader 還負責載入 Java 應用所需的資源,如影象檔案和配置檔案等。不過本節只討論其載入類的功能。為了完成載入類的這個職責, ClassLoader 提供了一系列的方法,比較重要的方法如 java.lang.ClassLoader 類介紹 所示。關於這些方法的細節會在下面進行介紹。

A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system. Every Class object contains a reference to the ClassLoader that defined it.

ClassLoader預設支援併發載入,可以通過ClassLoader.registerAsParallelCapable方法主動取消併發載入操作,ClassLoader實現併發載入的原理如下:當ClassLoader載入類時,如果該類是第一次載入,則會以該類的完全限定名稱作為Key,一個new Object()物件為Value,存入一個ConcurrentHashMap的中。並以該object物件為鎖進行同步控制。同一時間如果有其它執行緒再次請求載入該類時,則取出map中的物件object,發現該物件已被佔用,則阻塞。也就是說ClassLoader的併發載入通過一個ConcurrentHashMap實現的。

    // java載入類時獲取鎖的流程
protected Object getClassLoadingLock(String className) {
// 不開啟併發載入的情況下,使用ClassLoader物件本身加鎖
Object lock = this;
// 開啟併發載入的情況下,從ConcurrentHashMap中獲取需要載入的類物件進行加鎖。
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}

在某些不是嚴格遵循雙親委派模型的場景下,併發載入可能造成類載入器死鎖:

舉例:A和B兩個類使用不同的類載入器,A類的靜態初始化程式碼塊包含了B類的初始化操作(new B),B類的初始化程式碼塊也包含了A類的初始化操作(new A);併發載入A和B的情況下,就有可能出現死鎖的情況。而且加鎖操作發生在JVM層面,無法用常用的java類載入工具檢視到死鎖情況。

Class loaders that support concurrent loading of classes are known as parallel capable class loaders and are required to register themselves at their class initialization time by invoking the ClassLoader.registerAsParallelCapable method. Note that the ClassLoader class is registered as parallel capable by default. However, its subclasses still need to register themselves if they are parallel capable. In environments in which the delegation model is not strictly hierarchical, class loaders need to be parallel capable, otherwise class loading can lead to deadlocks because the loader lock is held for the duration of the class loading process (see loadClass methods).

方法 說明
getParent() 返回該類載入器的父類載入器(下文介紹的雙親委派模型會用到)。
findClass(String name) 查詢名稱為 name 的類,返回的結果是 java.lang.Class 類的例項()。
loadClass(String name) 載入名稱為 name 的類,返回的結果是 java.lang.Class 類的例項。和findClass的不同之處在於:loadClass添加了雙親委派和判斷
findLoadedClass(String name) 查詢名稱為 name 的已經被載入過的類,返回的結果是 java.lang.Class 類的例項。
defineClass(String name, byte[] b, int off, int len) 把位元組陣列 b 中的內容轉換成 Java 類,返回的結果是 java.lang.Class 類的例項。這個方法被宣告為 final 的
resolveClass(Class<?> c) 連結指定的 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 方法不會被重複呼叫。

許可權管理類載入器SecureClassLoader

在ClassLoader的基礎上添加了程式碼源和安全管理器。

This class extends ClassLoader with additional support for defining classes with an associated code source and permissions which are retrieved by the system policy by default.

內建類載入器BuiltinClassLoader

(建議看看java9 jigsaw模組化特性)BuiltinClassLoader載入器使用的委派模型與常規委派模型不同,該類載入器支援從模組載入類和資源。當請求載入一個類時,這個類載入器首先將類名對映到它的包名。如果有一個模組定義給包含這個包的BuiltinClassLoader,那麼類載入器將直接委託給該類載入器。如果沒有包含包的模組,那麼它將搜尋委託給父類裝入器,如果在父類中找不到,則會搜尋類路徑。這種委託模型與通常的委託模型的主要區別在於,它允許平臺類載入器委託給應用程式類載入器,這一點應該和java9 jigsaw模組化特性有關(破壞了雙親委派模型)。

The delegation model used by this ClassLoader differs to the regular delegation model. When requested to load a class then this ClassLoader first maps the class name to its package name. If there is a module defined to a BuiltinClassLoader containing this package then the class loader delegates directly to that class loader. If there isn't a module containing the package then it delegates the search to the parent class loader and if not found in the parent then it searches the class path. The main difference between this and the usual delegation model is that it allows the platform class loader to delegate to the application class loader, important with upgraded modules defined to the platform class loader.

平臺類載入器PlatformClassLoader

從JDK9開始,擴充套件類載入器被重新命名為平臺類載入器(Platform ClassLoader),部分不需要 AllPermission 的 Java 基礎模組,被降級到平臺類載入器中,相應的許可權也被更精細粒度地限制起來。它用來載入 Java 的擴充套件庫。Java 虛擬機器的實現會提供一個擴充套件庫目錄。該類載入器在此目錄裡面查詢並載入 Java 類。

Platform class loader. All platform classes are visible to the platform class loader that can be used as the parent of a ClassLoader instance. Platform classes include Java SE platform APIs, their implementation classes and JDK-specific run-time classes that are defined by the platform class loader or its ancestors.

To allow for upgrading/overriding of modules defined to the platform class loader, and where upgraded modules read modules defined to class loaders other than the platform class loader and its ancestors, then the platform class loader may have to delegate to other class loaders, the application class loader for example. In other words, classes in named modules defined to class loaders other than the platform class loader and its ancestors may be visible to the platform class loader.

應用程式類載入器AppClassLoader

系統類載入器負責將使用者類路徑(java -classpath或-Djava.class.path變數所指的目錄,即當前類所在路徑及其引用的第三方類庫的路下的類庫載入到記憶體中。如果程式設計師沒有自定義類載入器,預設呼叫該載入器。

System class loader. It is also known as application class loader and is distinct from the platform class loader. The system class loader is typically used to define classes on the application class path, module path, and JDK-specific tools. The platform class loader is a parent or an ancestor of the system class loader that all platform classes are visible to it.

使用者自定義類載入器

一般來說,使用者自定義類載入器以ClassLoader為基類,重寫其中的findClass,使findClass可以從使用者指定的位置讀取位元組碼.class檔案。不建議使用者重寫loadClass方法,因為loadClass包含了雙親委派模型和鎖等相關邏輯。

使用者自定義類載入器的父載入器可以在建構函式中指定,如果建構函式中沒有指定,那麼將會呼叫ClassLoader中的getSystemClassLoader()方法獲取預設類載入器:

    @CallerSensitive
public static ClassLoader getSystemClassLoader() {
switch (VM.initLevel()) {
case 0:
case 1:
case 2:
// the system class loader is the built-in app class loader during startup
return getBuiltinAppClassLoader();
case 3:
String msg = "getSystemClassLoader cannot be called during the system class loader instantiation";
throw new IllegalStateException(msg);
default:
// system fully initialized
asset VM.isBooted() && scl != null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
}

Normally, the Java virtual machine loads classes from the local file system in a platform-dependent manner. However, some classes may not originate from a file; they may originate from other sources, such as the network, or they could be constructed by an application. The method defineClass converts an array of bytes into an instance of class Class. Instances of this newly defined class can be created using Class.newInstance.

The methods and constructors of objects created by a class loader may reference other classes. To determine the class(es) referred to, the Java virtual machine invokes the loadClass method of the class loader that originally created the class.

For example, an application could create a network class loader to download class files from a server. Sample code might look like:

ClassLoader loader = new NetworkClassLoader(host, port);

Object main = loader.loadClass("Main", true).newInstance();

. . .

The network class loader subclass must define the methods findClass and loadClassData to load a class from the network. Once it has downloaded the bytes that make up the class, it should use the method defineClass to create a class instance. A sample implementation is:

class NetworkClassLoader extends ClassLoader {

String host;

int port;

public Class findClass(String name) {

byte[] b = loadClassData(name);

return defineClass(name, b, 0, b.length);

}

private byte[] loadClassData(String name) {

// load the class data from the connection

. . .

}

}

類載入器的特殊邏輯

雙親委派模型

而通常java中的類載入預設是採用雙親委派模型,即載入一個類時,首先判斷自身define載入器有沒有載入過此類,如果載入了直接獲取class物件,如果沒有查到,則交給載入器的父類載入器去重複上面過程。而java中載入器關係如下:

The ClassLoader class uses a delegation model to search for classes and resources. Each instance of ClassLoader has an associated parent class loader. When requested to find a class or resource, a ClassLoader instance will usually delegate the search for the class or resource to its parent class loader before attempting to find the class or resource itself.

雙親委派的具體過程如下:

  1. 當一個類載入器接收到類載入任務時,先查快取裡有沒有,如果沒有,將任務委託給它的父載入器去執行。
  2. 父載入器也做同樣的事情,一層一層往上委託,直到最頂層的啟動類載入器為止。
  3. 如果啟動類載入器沒有找到所需載入的類,便將此載入任務退回給下一級類載入器去執行,而下一級的類載入器也做同樣的事情。
  4. 如果最底層類載入器仍然沒有找到所需要的class檔案,則丟擲異常。

雙親委派模型的意義:確保類的全域性唯一性

如果你自己寫的一個類與核心類庫中的類重名,會發現這個類可以被正常編譯,但永遠無法被載入執行。因為你寫的這個類不會被應用類載入器載入,而是被委託到頂層,被啟動類載入器在核心類庫中找到了。如果沒有雙親委託機制來確保類的全域性唯一性,誰都可以編寫一個java.lang.Object類放在classpath下,那應用程式就亂套了。

從安全的角度講,通過雙親委託機制,Java虛擬機器總是先從最可信的Java核心API查詢型別,可以防止不可信的類假扮被信任的類對系統造成危害。

上下文類載入器

Java 提供了很多服務提供者介面(Service Provider Interface,SPI),允許第三方為這些介面提供實現。常見的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。這些 SPI 的介面由 Java 核心庫來提供,而這些 SPI 的實現程式碼則是作為 Java 應用所依賴的 jar 包被包含進類路徑(CLASSPATH)裡。SPI介面中的程式碼經常需要載入具體的實現類。那麼問題來了,SPI的介面是Java核心庫的一部分,是由啟動類載入器(Bootstrap Classloader)來載入的;SPI的實現類是由系統類載入器(System ClassLoader)來載入的。引導類載入器是無法找到 SPI 的實現類的,因為依照雙親委派模型,BootstrapClassloader無法委派AppClassLoader來載入類。而執行緒上下文類載入器破壞了“雙親委派模型”,可以在執行執行緒中拋棄雙親委派載入鏈模式,使程式可以逆向使用類載入器。

簡單來說:SPI介面類在java核心庫中,本來應該由啟動類載入器載入,但是因為SPI實現類機制,所以由上下文類載入器載入SPI介面類,使SPI介面類和實現類由同一個類載入器載入。

JDBC SPI介紹

只看文字理解有點困難,此處用JDBC案例進行分析(參考部落格):

// 載入Class到AppClassLoader(系統類載入器),然後註冊驅動類
// Class.forName("com.mysql.jdbc.Driver").newInstance();
String url = "jdbc:mysql://localhost:3306/testdb";
// 通過java庫獲取資料庫連線
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");

以上為我們獲取JDBC連結時常用的語句,實驗發現將的Class.forName註釋掉之後,程式但依然可以正常執行,這是為什麼呢?這是因為從Java1.6開始自帶的jdbc4.0版本已支援SPI服務載入機制,只要mysql的jar包在類路徑中,就可以註冊mysql驅動。

那到底是在哪一步自動註冊了mysql driver的呢?重點就在DriverManager.getConnection()中。我們都是知道呼叫類的靜態方法會初始化該類,進而執行其靜態程式碼塊,DriverManager的靜態程式碼塊就是:

static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

初始化方法loadInitialDrivers()的程式碼如下:

private static void loadInitialDrivers() {
String drivers;
try {
// 先讀取系統屬性
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 通過SPI載入驅動類
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// 繼續載入系統屬性中的驅動類
if (drivers == null || drivers.equals("")) {
return;
} String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 使用AppClassloader載入
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}

從上面可以看出JDBC中的DriverManager的載入Driver的步驟順序依次是:

  1. 通過SPI方式,讀取 META-INF/services 下檔案中的類名,使用TCCL載入;
  2. 通過System.getProperty("jdbc.drivers")獲取設定,然後通過系統類載入器載入。

    下面詳細分析SPI載入的那段程式碼。

JDBC中的SPI介紹:

SPI機制簡介

SPI的全名為Service Provider Interface,主要是應用於廠商自定義元件或外掛中。在java.util.ServiceLoader的文件裡有比較詳細的介紹。簡單的總結下java SPI機制的思想:我們系統裡抽象的各個模組,往往有很多不同的實現方案,比如日誌模組、xml解析模組、jdbc模組等方案。面向的物件的設計裡,我們一般推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。一旦程式碼裡涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改程式碼。為了實現在模組裝配的時候能不在程式裡動態指明,這就需要一種服務發現機制。 Java SPI就是提供這樣的一個機制:為某個介面尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程式之外,在模組化設計中這個機制尤其重要。

Java SPI的具體約定為:當服務的提供者提供了服務介面的一種實現之後,在jar包的META-INF/services/目錄裡同時建立一個以服務介面命名的檔案。該檔案裡就是實現該服務介面的具體實現類。而當外部程式裝配這個模組的時候,就能通過該jar包META-INF/services/裡的配置檔案找到具體的實現類名,並裝載例項化,完成模組的注入。基於這樣一個約定就能很好的找到服務介面的實現類,而不需要再程式碼裡制定。jdk提供服務實現查詢的一個工具類:java.util.ServiceLoader。

按照上文中的SPI介紹,我們分析一下JDBC的SPI程式碼:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}

注意driversIterator.next()最終就是呼叫Class.forName(DriverName, false, loader)方法,也就是最開始我們註釋掉的那一句程式碼。好,那句因SPI而省略的程式碼現在解釋清楚了,那我們繼續看給這個方法傳的loader是怎麼來的。

因為這句Class.forName(DriverName, false, loader)程式碼所在的類在java.util.ServiceLoader類中,而ServiceLoader.class又載入在BootrapLoader中,因此傳給 forName 的 loader 必然不能是BootrapLoader,複習雙親委派載入機制請看:java類載入器不完整分析 。這時候只能使用TCCL了,也就是說把自己載入不了的類載入到TCCL中(通過Thread.currentThread()獲取,簡直作弊啊!)。上面那篇文章末尾也講到了TCCL預設使用當前執行的是程式碼所在應用的系統類載入器AppClassLoader。

再看下看ServiceLoader.load(Class)的程式碼,的確如此:

public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

ContextClassLoader預設存放了AppClassLoader的引用,由於它是在執行時被放在了執行緒中,所以不管當前程式處於何處(BootstrapClassLoader或是ExtClassLoader等),在任何需要的時候都可以用Thread.currentThread().getContextClassLoader()取出應用程式類載入器來完成需要的操作。

到這兒差不多把SPI機制解釋清楚了。直白一點說就是,我(JDK)提供了一種幫你(第三方實現者)載入服務(如資料庫驅動、日誌庫)的便捷方式,只要你遵循約定(把類名寫在/META-INF裡),那當我啟動時我會去掃描所有jar包裡符合約定的類名,再呼叫forName載入,但我的ClassLoader是沒法載入的,那就把它載入到當前執行執行緒的TCCL裡,後續你想怎麼操作(驅動實現類的static程式碼塊)就是你的事了。

好,剛才說的驅動實現類就是com.mysql.jdbc.Driver.Class,它的靜態程式碼塊裡頭又寫了什麼呢?是否又用到了TCCL呢?我們繼續看下一個例子。

com.mysql.jdbc.Driver載入後執行的靜態程式碼塊:

static {
try {
// Driver已經載入到TCCL中了,此時可以直接例項化
java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

registerDriver方法將driver例項註冊到系統的java.sql.DriverManager類中,其實就是add到它的一個名為registeredDrivers的靜態成員CopyOnWriteArrayList中 ,到此驅動註冊基本完成.

更多案例參考部落格:https://blog.csdn.net/yangcheng33/article/details/52631940

總結

通過上面的案例分析,我們可以總結出執行緒上下文類載入器的適用場景:

  1. 當高層提供了統一介面讓低層去實現,同時又要是在高層載入(或例項化)低層的類時,必須通過執行緒上下文類載入器來幫助高層的ClassLoader找到並載入該類。
  2. 當使用本類託管類載入,然而載入本類的ClassLoader未知時,為了隔離不同的呼叫者,可以取呼叫者各自的執行緒上下文類載入器代為託管。

3.2.3 ServiceLoader

ServiceLoader是用於載入SPI服務實現類的工具,可以處理0個、1個或者多個服務提供商的情況。

A facility to load implementations of a service.

A service is a well-known interface or class for which zero, one, or many service providers exist. A service provider (or just provider) is a class that implements or subclasses the well-known interface or class. A ServiceLoader is an object that locates and loads service providers deployed in the run time environment at a time of an application's choosing. Application code refers only to the service, not to service providers, and is assumed to be capable of differentiating between multiple service providers as well as handling the possibility that no service providers are located.

應用程式通過ServiceLoader的靜態方法載入給定的服務,如果服務提供者在另外一個模組化的程式中,那麼當前模組必須宣告依賴服務提供方的服務實現類。ServiceLoader可以通過迭代器方法來定位和例項化服務的提供者,可以通過stream方法來獲取一個可以檢查和過濾的提供者流,而無需例項化它們。

An application obtains a service loader for a given service by invoking one of the static load methods of ServiceLoader. If the application is a module, then its module declaration must have a uses directive that specifies the service; this helps to locate providers and ensure they will execute reliably. In addition, if the service is not in the application module, then the module declaration must have a requires directive that specifies the module which exports the service.

A service loader can be used to locate and instantiate providers of the service by means of the iterator method. ServiceLoader also defines the stream method to obtain a stream of providers that can be inspected and filtered without instantiating them.

As an example, suppose the service is com.example.CodecFactory, an interface that defines methods for producing encoders and decoders:

下文舉例說明:CodecFactory為一個SPI服務介面。定義了getEncoder和getDecoder兩個藉口。

 package com.example;
public interface CodecFactory {
Encoder (String encodingName);
Decoder getDecoder(String encodingName);
}

下面的程式通過迭代器的方式獲取CodecFactory的服務提供者:

ServiceLoader<CodecFactory> loader = ServiceLoader.load(CodecFactory.class);
for (CodecFactory factory : loader) {
Encoder enc = factory.getEncoder("PNG");
if (enc != null)
... use enc to encode a PNG file
break;
}

有些時候,我們可能有很多服務提供者,但是隻有其中一些是有用的,這種情況下我們就需要對ServiceLoader獲取到的服務實現類進行過濾,比如案例中,我們只需要PNG格式的CodecFactory,那麼我們就可以對對應的服務實現類新增一個自定義的@PNG註解,然後通過下文過濾得到所需的服務提供者:

 ServiceLoader<CodecFactory> loader = ServiceLoader.load(CodecFactory.class);
Set<CodecFactory> pngFactories = loader
.stream() // Note a below
.filter(p -> p.type().isAnnotationPresent(PNG.class)) // Note b
.map(Provider::get) // Note c
.collect(Collectors.toSet());

SPI服務設計的原則:

服務應該服從單一職責原則,通常設計為介面或抽象類,不推薦設計為具體類(雖然也可以這樣實現)。不同情況下設計的服務的方法不同,但是都應該遵守兩個準則:

  1. 服務開放盡量多的方法,使服務提供方可以更自由的定製自己的服務實現方式。

  2. 服務應該表明自身是直接還是間接實現機制(如“代理”或“工廠”)。當某領域特定的物件例項化相對比較複雜時,服務提供者往往採用間接機制如,CodecFactory服務通過其名稱表示其服務提供商是編解碼器的工廠,而不是編解碼器本身,因為生產某些編解碼器可能很複雜。

    A service is a single type, usually an interface or abstract class. A concrete class can be used, but this is not recommended. The type may have any accessibility. The methods of a service are highly domain-specific, so this API specification cannot give concrete advice about their form or function. However, there are two general guidelines:

    1. A service should declare as many methods as needed to allow service providers to communicate their domain-specific properties and other quality-of-implementation factors. An application which obtains a service loader for the service may then invoke these methods on each instance of a service provider, in order to choose the best provider for the application.
    2. A service should express whether its service providers are intended to be direct implementations of the service or to be an indirection mechanism such as a "proxy" or a "factory". Service providers tend to be indirection mechanisms when domain-specific objects are relatively expensive to instantiate; in this case, the service should be designed so that service providers are abstractions which create the "real" implementation on demand. For example, the CodecFactory service expresses through its name that its service providers are factories for codecs, rather than codecs themselves, because it may be expensive or complicated to produce certain codecs.

有兩種方式可以宣告一個服務實現類:

  • 通過模組化的包宣告:

    provides com.example.CodecFactory with com.example.impl.StandardCodecs;

    provides com.example.CodecFactory with com.example.impl.ExtendedCodecsFactory;

    -通過指定路徑宣告:META-INF/services

    如:META-INF/services/com.example.CodecFactory

    新增一行:com.example.impl.StandardCodecs # Standard codecs

開發自己的類載入器

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

檔案系統類載入器

第一個類載入器用來載入儲存在檔案系統上的 Java 位元組程式碼。完整的實現如清單 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('.', File.separatorChar) + ".class";
}
}

如清單 6 所示,類 FileSystemClassLoader 繼承自類 java.lang.ClassLoader 。在 java.lang.ClassLoader 類介紹 中列出的 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 應用共享的目錄下面。

當出現找不到類的錯誤時,檢查當前類的類載入器和當前執行緒的上下文類載入器是否正確。

歡迎關注御狐神的微信公眾號

本文最先發布至微信公眾號,版權所有,禁止轉載!