前提
緊接着上一篇《通過源碼淺析JDK中的資源加載》,ServiceLoader是SPI(Service Provider Interface)中的服務類加載的核心類,也就是,這篇文章先介紹ServiceLoader的使用方式,再分析它的源碼。
ServiceLoader的使用
這里先列舉一個經典的例子,MySQL的Java驅動就是通過ServiceLoader加載的,先引入mysql-connector-java
的依賴:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
查看這個依賴的源碼包下的META-INF目錄,可見:
我們接着查看java.lang.DriverManager,靜態代碼塊里面有:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
其中,可以查看loadInitialDrivers()
有如下的代碼片段:
java.lang.DriverManager是啟動類加載器加載的基礎類,但是它可以加載rt.jar
包之外的類,上篇文章提到,這里打破了雙親委派模型,原因是:ServiceLoader中使用了線程上下文類加載器去加載類。這里JDBC加載的過程就是典型的SPI的使用,總結規律如下:
- 1、需要定義一個接口。
- 2、接口提供商需要實現第1步中的接口。
- 3、接口提供商在META-INF/services目錄下建立一個文本文件,文件名是第1步中定義的接口的全限定類名,文本內容是接口的實現類的全限定類名,每個不同的實現占獨立的一行。
- 4、使用ServiceLoader加載接口類,獲取接口的實現的實例迭代器。
舉個簡單的實例,先定義一個接口和兩個實現:
public interface Say {
void say();
}
public class SayBye implements Say {
@Override
public void say() {
System.out.println("Bye!");
}
}
public class SayHello implements Say {
@Override
public void say() {
System.out.println("Hello!");
}
}
接着在項目的META-INF/services中添加文件如下:
最后通過main函數驗證:
基於SPI或者說ServiceLoader加載接口實現這種方式也可以廣泛使用在相對基礎的組件中,因為這是一個成熟的規范。
ServiceLoader源碼分析
上面通過一個經典例子和一個實例介紹了ServiceLoader的使用方式,接着我們深入分析ServiceLoader的源碼。我們先看ServiceLoader的類簽名和屬性定義:
public final class ServiceLoader<S> implements Iterable<S>{
//需要加載的資源的路徑的目錄,固定是ClassPath下的META-INF/services/
private static final String PREFIX = "META-INF/services/";
// ServiceLoader需要正在需要加載的類或者接口
// The class or interface representing the service being loaded
private final Class<S> service;
// ServiceLoader進行類加載的時候使用的類加載器引用
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// 權限控制上下文
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
//基於實例的順序緩存類的實現實例,其中Key為實現類的全限定類名
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 當前的"懶查找"迭代器,這個是ServiceLoader的核心
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
//暫時忽略其他代碼...
}
ServiceLoader實現了Iterable接口,這一點提示了等下我們在分析它源碼的時候,需要重點分析iterator()
方法的實現。ServiceLoader依賴於類加載器實例進行類加載,它的核心屬性LazyIterator是就是用來實現iterator()
方法的,下文再重點分析。接着,我們分析ServiceLoader的構造函數:
public void reload() {
//清空緩存
providers.clear();
//構造LazyIterator實例
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
ServiceLoader只有一個私有的構造函數,也就是它不能通過構造函數實例化,但是要實例化ServiceLoader必須依賴於它的靜態方法調用私有構造去完成實例化操作,而實例化過程主要做了幾步:
- 1、判斷傳入的接口或者類的Class實例不能為null,否則會拋出異常。
- 2、如果傳入的ClassLoader實例為null,則使用應用類加載器(Application ClassLoader)。
- 3、實例化訪問控制上下文。
- 4、調用實例方法
reload()
,清空目標加載類的實現類實例的緩存並且構造LazyIterator實例。
注意一點是實例方法reload()
的修飾符是public,也就是可以主動調用去清空目標加載類的實現類實例的緩存和重新構造LazyIterator實例。接着看ServiceLoader提供的靜態方法:
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
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);
}
上面的三個公共靜態方法都是用於構造ServiceLoader實例,其中load(Class<S> service, ClassLoader loader)
就是典型的靜態工廠方法,直接調用ServiceLoader的私有構造器進行實例化,除了需要指定加載類的目標類型,還需要傳入類加載器的實例。load(Class<S> service)
實際上也是委托到load(Class<S> service, ClassLoader loader)
,不過它使用的類加載器指定為線程上下文類加載器,一般情況下,線程上下文類加載器獲取到的就是應用類加載器(系統類加載器)。loadInstalled(Class<S> service)
方法又看出了"雙親委派模型"的影子,它指定類加載器為最頂層的啟動類加載器,最后也是委托到load(Class<S> service, ClassLoader loader)
。接着我們需要重點分析ServiceLoader#iterator()
:
public Iterator<S> iterator() {
//Iterator的匿名實現
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();
}
};
}
iterator()
內部僅僅是Iterator接口的匿名實現,hasNext()
和next()
方法都是優先判斷緩存中是否已經存在實現類的實例,如果存在則直接從緩存中返回,否則調用懶加載迭代器LazyIterator的實例去獲取,而LazyIterator本身也是一個Iterator接口的實現,它是ServiceLoader的一個私有內部類,源碼如下:
private class LazyIteratorimplements Iterator<S>{
Class<S> service;
ClassLoader loader;
//加載的資源的URL集合
Enumeration<URL> configs = null;
//所有需要加載的實現類的全限定類名的集合
Iterator<String> pending = null;
//下一個需要加載的實現類的全限定類名
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
//如果下一個需要加載的實現類的全限定類名不為null,則說明資源中存在內容
if (nextName != null) {
return true;
}
//如果加載的資源的URL集合為null則嘗試進行加載
if (configs == null) {
try {
//資源的名稱,META-INF/services + '需要加載的類的全限定類名'
//這樣得到的剛好是需要加載的文件的資源名稱
String fullName = PREFIX + service.getName();
//這里其實ClassLoader實例應該不會為null
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
//從ClassPath加載資源
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//從資源中解析出需要加載的所有實現類的全限定類名
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
//獲取下一個需要加載的實現類的全限定類名
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//反射構造Class<S>實例
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
//這里會做一次類型判斷,也就是實現類必須是當前加載的類或者接口的派生類,否則拋出異常終止
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//通過Class#newInstance()進行實例化,並且強制轉化為對應的類型的實例
S p = service.cast(c.newInstance());
//添加緩存,Key為實現類的全限定類名,Value為實現類的實例
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
LazyIterator
也是Iterator接口的實現,它的Lazy特性表明它總是在ServiceLoader的Iterator接口匿名實現iterator()
執行hasNext()
判斷是否有下一個實現或者next()
獲取下一個實現類的實例的時候才會"懶判斷"或者"懶加載"下一個實現類的實例。最后是加載資源文件后對資源文件的解析過程的源碼:
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError{
InputStream in = null;
BufferedReader r = null;
//存放文件中所有的實現類的全類名,每一行是一個元素
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
//返回的是ArrayList的迭代器實例
return names.iterator();
}
//解析資源文件中每一行的內容
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)throws IOException, ServiceConfigurationError{
// 下一行沒有內容,返回-1,便於上層可以跳出循環
String ln = r.readLine();
if (ln == null) {
return -1;
}
//如果存在'#'字符,截取第一個'#'字符串之前的內容,'#'字符之后的屬於注釋內容
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
//不能存在空格字符' '和特殊字符'\t'
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
//判斷第一個char是否一個合法的Java起始標識符
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
//判斷所有其他字符串是否屬於合法的Java標識符
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
//如果緩存中不存在加載出來的全類名或者已經加載的列表中不存在加載出來的全類名則添加進去加載的全類名列表中
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
整個資源文件的解析過程並不復雜,主要包括文件內容的字符合法性判斷和緩存避免重復加載的判斷。
小結
SPI被廣泛使用在第三方插件式類庫的加載,最常見的如JDBC、JNDI、JCE(Java加密模塊擴展)等類庫。理解ServiceLoader的工作原理有助於編寫擴展性良好的可插拔的類庫。
(本文完 c-1-d e-20181014)
技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):
娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力: