- ClassLoader & 雙親委派
- URLClassLoader
- SPI(Service Provider Interface)
ClassLoader & 雙親委派
ClassLoader | 編程語言 | 加載內容 | Parent ClassLoader | |
---|---|---|---|---|
BootstrapClassLoader | C++ | jre/lib;jre/classes | Null | Java虛擬機啓動後初始化 |
ExtClassLoader | Java | jre/lib/ext;java.ext.dirs | BootstrapClassLoader | |
AppClassLoader | Java | classpath指定位置 | ExtClassLoader | ClassLoader.getSystemClassLoader 返回值 |
ClassLoader的基本模型:
URLClassLoader
- ClassLoader.loadClass
- URLClassLoader.findClass
- ExtClassLoader & AppClassLoader 的URLClassPath
ClassLoader.loadClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) { resolveClass(c); }
return c;
}
}
// ClassLoader method
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
其中findClass是關鍵:一來沒有具體實現,二來修飾符是protected,所以 很明顯是爲了“開閉”。
URLClassLoader.findClass
// URLClassLoader overwrite
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
這裏要關注的點是: ucp.getResource
所有繼承至URLClassLoader的子類(不overwrite findClass的情況下),都是 【只能通過ucp來進行資源的加載】。
URLClassPath做到了兩點:
- 指定資源的來源:可以來自於Local,來自於Remote;可以是Jar,也可以是War等等(指定URL即可)。
- 限制資源的來源:當指定了該ucp,那麼該ClassLoader的資源來源也就被【限制】只能是來自於這裏。
ExtClassLoader & AppClassLoader
相比之下ExtClassLoader和AppClassLoader有何區別呢?通過源碼對比一下URLClassPath
- ExtClassLoader
static class ExtClassLoader extends URLClassLoader {
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for(int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException)var2.getException();
}
}
public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for(int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
private static URL[] getExtURLs(File[] var0) throws IOException {
Vector var1 = new Vector();
for(int var2 = 0; var2 < var0.length; ++var2) {
String[] var3 = var0[var2].list();
if (var3 != null) {
for(int var4 = 0; var4 < var3.length; ++var4) {
if (!var3[var4].equals("meta-index")) {
File var5 = new File(var0[var2], var3[var4]);
var1.add(Launcher.getFileURL(var5));
}
}
}
}
URL[] var6 = new URL[var1.size()];
var1.copyInto(var6);
return var6;
}
}
(通過構造函數反推)結論:通過getExtClassLoader來獲取ExtClassLoader時,直接採用環境變量中的“java.ext.dirs”作爲資源
- AppClassLoader
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
AppClassLoader(URL[] var1, ClassLoader var2) {
super(var1, var2, Launcher.factory);
this.ucp.initLookupCache(this);
}
}
(通過構造函數反推)結論:通過getAppClassLoader來獲取AppClassLoader時,直接採用環境變量中的“java.class.path”作爲資源
URLClassLoader 總結
對於自定義ClassLoader 只需要關心兩點
- 一:URLClassPath – 通常是構造函數或參數
- 二:委派過程 – findClass
SPI(Service Provider Interface)
SPI機制簡介
SPI的全名爲Service Provider Interface,主要是應用於廠商自定義組件或插件中。在java.util.ServiceLoader的文檔裏有比較詳細的介紹。簡單的總結下java SPI機制的思想:我們系統裏抽象的各個模塊,往往有很多不同的實現方案,比如日誌模塊、xml解析模塊、jdbc模塊等方案。面向的對象的設計裏,我們一般推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。爲了實現在模塊裝配的時候能不在程序裏動態指明,這就需要一種服務發現機制。 Java SPI就是提供這樣的一個機制:爲某個接口尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。
SPI具體約定
Java SPI的具體約定爲:當服務的提供者提供了服務接口的一種實現之後,在jar包的META-INF/services/目錄裏同時創建一個以服務接口命名的文件。該文件裏就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。基於這樣一個約定就能很好的找到服務接口的實現類,而不需要再代碼裏制定。jdk提供服務實現查找的一個工具類:java.util.ServiceLoader。
載錄:https://blog.csdn.net/sigangjun/article/details/79071850
首先,ServiceLoader的構造函數 是private的,那麼自然就需要定位static方法了。
其中load是c位,它有兩個重載
- public static <S> ServiceLoader<S> load(Class<S> service) 即不提供ClassLoader
- public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) 即提供ClassLoader
其中 1中的邏輯 通過Thread.currentThread().getContextClassLoader()獲取ClassLoader,並調用2。
所以 想要說清楚ServiceLoader,就不得不提到 Thread中的ContextClassLoader 。
問題引出
想象一下:如果讓BootstrapClassLoader(或者ExtClassLoader)去load一個MySQL的Driver,正常情況下會成功嗎?
答案是,不會成功的。
其實,細想也是能理解的,畢竟每一個ClassLoader都是有自己負責和管轄的URLClassPath,而MySQL的Driver並不在 BootstrapClassLoader(或者ExtClassLoader)所管轄的URLClassPath下,自然 也就不能成功load到了。
那麼,該如何是好呢?(其實,其中還隱藏了一些問題:load事件何時發生?發生時候的上下文環境又是如何?這是JVM的範疇,這裏不討論)
通過上面的描述,可以猜到 解決的方法也是簡單的,就是將MySQL的Driver放置到“正確”的URLClassPath下即可。 但是,如果真的是這樣,那豈不是把所有的都放在BootstrapClassLoader下面進行load就萬無一失了?
這裏筆者會想到一個問題:(不考慮說BootstrapClassLoader的壓力,因爲這點筆者也說不清楚),由於純理論 可能會描述不清,所以舉例說明:
如 Kafka Connect中,當集羣啓動之後,會在集羣上啓動不同的任務(A任務,B任務),這時如果多任務之間都依賴於一個小模塊,但是又正好,它們依賴的模塊的版本不同且不兼容,那可如何是好?
這裏就需要考慮到Class Resource的隔離了,這也是雙親委派設計的優點。(細節不與說明)
以上都是題外話(不過 不是廢話,筆者覺得 提出的問題還是挺有趣的),回到剛剛的問題:如果讓BootstrapClassLoader(或者ExtClassLoader)去load一個MySQL的Driver,正常情況下會成功嗎?
其實這個問題吧 本身也是有問題的,問題就是:爲什麼非要讓BootstrapClassLoader(或者ExtClassLoader)去load一個MySQL的Driver,讓對應的ClassLoader去load不就好了嗎?例如AppClassLoader。 – 這個問題的本質 就是SPI(Service Provider Interface)了。
回到SPI
SPI的關鍵字:接口編程(不對實現類進行硬編碼)、可拔插、服務發現機制、動態注入。
簡單通過DriverManager來說明ServiceLoader所提供的功能和使用方式:
首先:裝載DriverManager,並觸發靜態邏輯:通過ServiceLoader裝載所有Driver的實現類
接着:通過DriverManager.getConnection()獲取匹配的Driver實現類
最後:由於是面向接口編程,所以直接使用Connection接口即可操作後續邏輯
以上過程看似簡單,但是仔細看 可以提出一些問題:
- DriverManager 是在哪裏,被哪個ClassLoader加載?
- Driver的實現類 是在哪裏,被哪個ClassLoader加載?
從Coding的角度看,DriverManager總是在AppClassLoader或者更上層被加載(調用)的,那麼自然 DriverManager會按照雙親委派的方式 不斷的向上 委託加載,直到ExtClassLoader(因爲DriverManager是在rt.jar的java.sql包下)。到此,第一個問題就解答完畢了。
接着,第二個問題的第一小問。Driver的實現類 是在哪裏被加載的?
public class DriverManager {
static { loadInitialDrivers(); }
private static void loadInitialDrivers() {
String drivers;
......
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
......
}
});
......
}
}
答案就是這個,Driver的實現類 是在DriverManager的靜態方法中 通過ServiceLoader進行加載的。
然後,第二個問題的第二小問。Driver的實現類 是被哪個ClassLoader加載?
觀察加載邏輯,會發現 Driver的實現類 的加載 是通過 ServiceLoader實現的。而ServiceLoader在加載時,則是通過獲取Thread中的ContextClassLoader進行加載的 – Thread.currentThread().getContextClassLoader()。
那麼 這個ContextClassLoader到底是什麼呢? – 默認情況下(可以set進行修改)是 AppClassLoader。這樣就會形成如下的圖:
所以:如果讓BootstrapClassLoader(或者ExtClassLoader)去load一個MySQL的Driver,會發現 並不能成功的load到這個Driver實現類,但是,完全可以通過這種"破壞"的方式 來成功的回到上流的ClassLoader來load這個Driver實現類。
這就是 所謂的 “對雙親委派的破壞”,這也正是SPI的實現原理。
ServiceLoader使用方式 & SPI使用案例
ServiceLoader的使用需要三步操作
- 創建META-INF/services/service.name文件
- 將service的實現類的全限定名稱寫入該文件中
- 使用ServiceLoader.load(Service.class)來獲取實現類
SPI的使用案例很多,有興趣的可以好好看看:
- org.slf4j.spi.SLF4JServiceProvider
- com.facebook.presto.spi.Plugin
- org.apache.flink.table.factories.TableFactory
- …
後記
1 -- this.getClass().getResource(String name)
2 -- this.getClass().getClassLoader().getResource(String name)
最近正好使用了上面的來個方法,隨便跟蹤了一下源碼,這裏記錄一下:
只需要關注(1)的源碼即可
public java.net.URL getResource(String name) {
name = resolveName(name);
// 從這裏可以看出,上面的(1)實際上還是調用了(2)
ClassLoader cl = getClassLoader0();
// 這裏主要是 向上委派的遞歸過程(源碼就不展示了)
return cl.getResource(name);
}
這裏關注一下resolveName邏輯
private String resolveName(String name) {
if (!name.startsWith("/")) {
// this.getClass().getResource(".......") -- 將定位至this的相對路徑下
Class<?> c = this;
while (c.isArray()) { c = c.getComponentType(); }
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) { name = baseName.substring(0, index).replace('.', '/')+"/"+name; }
} else {
// this.getClass().getResource("/.......") -- 將定位至classpath下
name = name.substring(1);
}
return name;
}
結論:
- this.getClass().getResource("") – 相對於this的路徑
- this.getClass().getResource("/") – classpath絕對路徑
- this.getClass().getClassLoader().getResource("") – classpath絕對路徑
- this.getClass().getClassLoader().getResource("/") – 爆炸?