深入剖析Spring boot自動裝配原理
ServiceLoader是jdk6里面引進的一個特性。它用來實現(xiàn)SPI(Service Provider Interface),一種服務發(fā)現(xiàn)機制,很多框架用它來做來做服務的擴展發(fā)現(xiàn)。
系統(tǒng)里抽象的各個模塊一般會有很多種不同的實現(xiàn),如JDBC、日志等。通常模塊之間我們均是基于接口進行編程,而不是對實現(xiàn)類進行硬編碼。這時候就需要一種動態(tài)替換發(fā)現(xiàn)的機制,即在運行時動態(tài)地給接口添加實現(xiàn),而不需要在程序中指明。
引用自JDK文檔對于java.util.ServiceLoader的描述:
服務是一個熟知的接口和類(通常為抽象類)集合。服務提供者是服務的特定實現(xiàn)。提供者中的類通常實現(xiàn)接口,并子類化在服務本身中定義的子類。服務提供者可以以擴展的形式安裝在 Java 平臺的實現(xiàn)中,也就是將 jar 文件放入任意常用的擴展目錄中。也可通過將提供者加入應用程序類路徑,或者通過其他某些特定于平臺的方式使其可用。
為了加載,服務由單個類型表示,也就是單個接口或抽象類。一個給定服務的提供者包含一個或多個具體類,這些類擴展了此服務類型,具有特定于提供者的數(shù)據(jù)和代碼。提供者類通常不是整個提供者本身而是一個代理,它包含足夠的信息來決定提供者是否能滿足特定請求,還包含可以根據(jù)需要創(chuàng)建實際提供者的代碼。提供者類的詳細信息高度特定于服務;任何單個類或接口都不能統(tǒng)一它們,因此這里沒有定義任何這種類型。此設施唯一強制要求的是,提供者類必須具有不帶參數(shù)的構造方法,以便它們可以在加載中被實例化。
當服務的提供者提供了服務接口的一種實現(xiàn)之后,在jar包的META-INF/services/ 目錄里同時創(chuàng)建一個以服務接口命名的該服務接口具體實現(xiàn)類文件。當外部程序裝配該模塊時,通過該jar包META-INF/services/里的配置文件找到具體的實現(xiàn)類名,從而完成模塊的注入,而不需要在代碼里定制。
在JDBC中使用了ServiceLoader對不同數(shù)據(jù)庫驅(qū)動進行加載。
DriverManager通過ServiceLoader加載數(shù)據(jù)庫驅(qū)動,JDBC原理可移步【JDBC原理】。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
ServiceLoader 使用小栗子
首先定義一個SPIService接口
package com.study;
public interface SPIService {
void excute();
}
再定義兩個實現(xiàn)類
package com.study.impl;
import com.study.SPIService;
public class SPIServiceImpl1 implements SPIService {
@Override
public void excute() {
System.out.println("SPIServiceImpl1");
}
}
package com.study.impl;
import com.study.SPIService;
public class SPIServiceImpl2 implements SPIService {
@Override
public void excute() {
System.out.println("SPIServiceImpl2");
}
}
在ClassPath路徑下配置添加文件,META-INF/services/com.study.SPIService,文件名為接口的全限定類名。在配置文件中加入兩個實現(xiàn)類的全限定類名。
com.study.impl.SPIServiceImpl1
com.study.impl.SPIServiceImpl2
寫一個測試類SPITest.java
public class SPITest {
public static void main(String[] args) {
ServiceLoader<SPIService> loaders = ServiceLoader.load(SPIService.class);
Iterator<SPIService> it = loaders.iterator();
while (it.hasNext()) {
SPIService spiSer= it.next();
spiSer.excute();
}
}
}
運行結果:
SPIServiceImpl1
SPIServiceImpl2
ServiceLoader的load方法將在META-INF/services/com.study.SPIService中配置的子類都進行了加載。
ServiceLoader的核心源碼分析
public final class ServiceLoader<S> implements Iterable<S>{
// 需要加載的資源的配置文件路徑
private static final String PREFIX = "META-INF/services/";
// 加載的服務類或接口
private Class<S> service;
// 類加載時用到的類加載器
private ClassLoader loader;
// 基于實例的已加載的順序緩存類,其中Key為實現(xiàn)類的全限定類名
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// "懶查找"迭代器,ServiceLoader的核心
private LazyIterator lookupIterator;
public void reload() {
// 清空緩存
providers.clear();
// 構造LazyIterator實例
lookupIterator = new LazyIterator(service, loader);
}
// 私有構造方法
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl;
reload();
}
}
ServiceLoader只有一個私有的構造函數(shù),也就是它不能通過構造函數(shù)實例化,但是要實例化ServiceLoader必須依賴于它的靜態(tài)方法調(diào)用私有構造去完成實例化操作。
來看ServiceLoader的提供的靜態(tài)方法,這幾個方法都可以用于構造ServiceLoader的實例。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 獲取當前線程的線程上下文類加載器實例,確保通過此classLoader也能加載到項目中的資源文件
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
load(Class<S> service, ClassLoader loader)是典型的靜態(tài)工廠方法,直接調(diào)用ServiceLoader的私有構造器進行實例化,除了需要指定加載類的目標類型,還需要傳入類加載器的實例。load(Class<S>service)實際上也是委托到load(Class<S> service, ClassLoader loader),不過它使用的類加載器指定為線程上下文類加載器,一般情況下線程上下文類加載器獲取到的就是應用類加載器(系統(tǒng)類加載器)。loadInstalled(Class<S> service)方法又看出了"雙親委派模型"的影子,它指定類加載器為最頂層的啟動類加載器,最后也是委托到load(Class<S> service, ClassLoader loader)。
這里重點關注為什么類加載器使用線程上下文類加載器?以JDBC加載MySQL驅(qū)動舉例。
Java類加載的過程通常是遵循雙親委派模型的。但是對于SPI接口實現(xiàn)類的加載就需要破壞雙親委派模型。
首先java.sql.DriverManager是由啟動類加載器加載的,創(chuàng)建真正的Dirver對象時需要使用到mysql提供的實現(xiàn):com.mysql.jdbc.Dirver,即要初始化該類。但是啟動類加載器加載DirverManager的時候,使用到了啟動類加載器無法加載的類,這時候就需要由系統(tǒng)類加載器來加載。com.mysql.jdbc.Dirver通常放在類路徑下的(其實不一定)。到這里和線程上下文類加載器沒由任何關系。在DriverManager中使用系統(tǒng)類加載的時候,可以直接使用靜態(tài)方法ClassLoader.getSystemClassLoader(),但是這種情況的前提是com.mysql.jdbc.Dirver類在類路徑下。如果不在類路徑下,而且在系統(tǒng)環(huán)境中有其他的類加載器,在通過其他類加載器可能出現(xiàn)無法正確加載擴展點的情況。比如某個類的字節(jié)碼是在數(shù)據(jù)庫中存儲,這時我們需要自定義一個類加載器去加載它,這個類加載器會告訴DriverManager去我們指定放的地方取。因此Thread.currentThread().setContextClassLoader(自定義加載器/默認是系統(tǒng)類加載器); 這個就是線程上下文類加載器起到的介質(zhì)作用。線程上下文中默認放的是系統(tǒng)類加載器。
ServiceLoader實現(xiàn)了Iterable接口。
public Iterator<S> iterator() {
// Iterator的匿名實現(xiàn)
return new Iterator<S>() {
// 基于實例的已加載的順序緩存類Map的Entry迭代器實例
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
// 先從緩存中判斷是否有下一個實例,否則通過懶加載迭代器LazyIterator去判斷是否存在下一個實例
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
// 如果緩存中判斷是否有下一個實例,如果有則從緩存中的值直接返回,否則通過懶加載迭代器LazyIterator獲取下一個實例
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
// 不支持移除
public void remove() {
throw new UnsupportedOperationException();
}
};
}
LazyIterator本身也是一個Iterator接口的實現(xiàn),它是ServiceLoader的一個私有內(nèi)部類。
private class LazyIterator implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
// 加載的資源的URL集合
Enumeration<URL> configs = null;
// 所有需要加載的實現(xiàn)類的全限定類名的集合
Iterator<String> pending = null;
// 下一個需要加載的實現(xiàn)類的全限定類名
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
public boolean hasNext() {
// 如果下一個需要加載的實現(xiàn)類的全限定類名不為null,則說明資源中存在內(nèi)容
if (nextName != null) {
return true;
}
// 如果加載的資源的URL集合為null則嘗試進行加載
if (configs == null) {
try {
// 資源的名稱,META-INF/services/ + '需要加載的類的全限定類名'
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 從ClassPath加載資源
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 從資源中解析出需要加載的所有實現(xiàn)類的全限定類名
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// 獲取下一個需要加載的實現(xiàn)類的全限定類名
nextName = pending.next();
return true;
}
public S next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
String cn = nextName;
nextName = null;
try {
// 反射構造Class<S>實例,同時進行初始化,并且強制轉(zhuǎn)化為對應的類型的實例
S p = service.cast(Class.forName(cn, true, loader).newInstance());
// 添加進緩存,Key為實現(xiàn)類的全限定類名,Value為實現(xiàn)類的實例
providers.put(cn, p);
return p;
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated: " + x, x);
}
throw new Error(); // This cannot happen
}
public void remove() {
throw new UnsupportedOperationException();
}
}
ServiceLoader總結
JDK提供了一種幫第三方實現(xiàn)者加載服務的便捷方式,如JDBC、日志等,第三方實現(xiàn)者需要遵循約定把具體實現(xiàn)的類名放在/META-INF里。當JDK啟動時會去掃描所有jar包里符合約定的類名,再調(diào)用forName進行加載,如果JDK的ClassLoader無法加載,就使用當前執(zhí)行線程的線程上下文類加載器。
但是在通過SPI查找服務的具體實現(xiàn)時,會遍歷所有的實現(xiàn)進行實例化,并在循環(huán)中找到需要的具體實現(xiàn)。
作者:java知路
歡迎關注微信公眾號 :java知路