Java類加載器是用戶程序和JVM虛擬機之間的橋梁,在Java程序中起了至關重要的作用,理解它有利於我們寫出更優雅的程序。本文首先介紹了Java虛擬機加載程序的過程,簡述了Java類加載器的加載方式(雙親委派模式),然后介紹了幾種常見的類加載器及其適用場景,最后則一個例子展示了如何自定義類加載器。本文很多地方參考了java官方文檔關於虛擬機加載的教程,點此直達官方參考文檔
基本概念
基本文件類型和概念
常見概念介紹:
-
java源文件(.java):.java是Java的源文件后綴,里面存放程序員編寫的功能代碼,只是一個文本文件,不能被java虛擬機所識別, 但是java語法有其自身的語法規范要求,不符合規范的java程序應該在編譯期間報錯。
-
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.
-
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運行過程中,首先會加載初始類,然后再從初始類鏈接觸發它相關的類的加載。
注意:圖中的“引用”指觸發類加載,一共有以下幾種情況會觸發類加載:
-
創建類的實例 訪問類的靜態變量(注意:當訪問類的靜態並且final修飾的變量時,不會觸發類的初始化。),或者為靜態變量賦值。
-
調用類的靜態方法(注意:調用靜態且final的成員方法時,會觸發類的初始化!一定要和靜態且final修飾的變量區分開!!)
-
使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。如:Class.forName("bacejava.Langx");
-
注意通過類名.class得到Class文件對象並不會觸發類的加載。 初始化某個類的子類
-
直接使用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.
綜上所述:類的生成方式一共有三種:
-
啟動類加載器
-
用戶自定義類加載器
-
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.
用戶自定義類加載器
用戶自定義類加載器可以分為兩種類型:
- java庫中的平台類加載器和應用程序類加載器等
- 用戶自己寫的類加載器,比如通過網絡加載類等機制
數組類加載器
數組的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.
雙親委派的具體過程如下:
- 當一個類加載器接收到類加載任務時,先查緩存里有沒有,如果沒有,將任務委托給它的父加載器去執行。
- 父加載器也做同樣的事情,一層一層往上委托,直到最頂層的啟動類加載器為止。
- 如果啟動類加載器沒有找到所需加載的類,便將此加載任務退回給下一級類加載器去執行,而下一級的類加載器也做同樣的事情。
- 如果最底層類加載器仍然沒有找到所需要的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的步驟順序依次是:
- 通過SPI方式,讀取 META-INF/services 下文件中的類名,使用TCCL加載;
- 通過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
總結
通過上面的案例分析,我們可以總結出線程上下文類加載器的適用場景:
- 當高層提供了統一接口讓低層去實現,同時又要是在高層加載(或實例化)低層的類時,必須通過線程上下文類加載器來幫助高層的ClassLoader找到並加載該類。
- 當使用本類托管類加載,然而加載本類的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服務設計的原則:
服務應該服從單一職責原則,通常設計為接口或抽象類,不推薦設計為具體類(雖然也可以這樣實現)。不同情況下設計的服務的方法不同,但是都應該遵守兩個准則:
-
服務開放盡量多的方法,使服務提供方可以更自由的定制自己的服務實現方式。
-
服務應該表明自身是直接還是間接實現機制(如“代理”或“工廠”)。當某領域特定的對象實例化相對比較復雜時,服務提供者往往采用間接機制如,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:
- 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.
- 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 應用共享的目錄下面。
當出現找不到類的錯誤時,檢查當前類的類加載器和當前線程的上下文類加載器是否正確。
歡迎關注御狐神的微信公眾號
本文最先發布至微信公眾號,版權所有,禁止轉載!