詳解Java—ServiceLoader之源碼分析

ServiceLoader主要的功能是用來完成對SPI的provider的加載。sql

先看下它的成員:數組

public final class ServiceLoader<S>
     implements Iterable<S> {
	private static final String PREFIX = "META-INF/services/";
	// The class or interface representing the service being loaded
	private final Class<S> service;
	// 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;
	// Cached providers, in instantiation order
	private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
	// The current lazy-lookup iterator
	private LazyIterator lookupIterator;
	......
}
複製代碼

能夠看到他首先是實現了Iterable接口,能夠迭代。緩存

  • PREFIX:指明瞭路徑是在"META-INF/services/"下。
  • service:表示正在加載的服務的類或接口。
  • loader:使用的類加載器。
  • acc:建立ServiceLoader時獲取的訪問控制上下文。
  • providers:緩存的服務提供集合。
  • lookupIterator:是其內部使用的迭代器,用於類的懶加載,只有在迭代時加載。

其構造方法是一個private方法,不對外提供,在使用時咱們須要調用其靜態的load方法,由其自身產生ServiceLoader對象:安全

public static <S> ServiceLoader<S> load(Class<S> service) {
	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);
}
複製代碼

能夠看到對load方法進行了重載,其中參數service是要加載的類;單參方法沒有類加載器,使用的是當前線程的類加載器;最後調用的是雙參的load方法;而雙參的load方法也很簡單,只是直接調用ServiceLoader的構造方法,實例化了一個對象。bash

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();
複製代碼

能夠看到其構造方法邏輯依舊很簡單,首先是判斷傳入的svc(即傳入的service)是否爲空,如果爲空直接報異常,不然給service 成員賦值:架構

public static <T> T requireNonNull(T obj, String message) {
	if (obj == null)
	             throw new NullPointerException(message);
	return obj;
}
複製代碼

而後給進行cl的非空判斷,給loader 成員賦值;接着給acc 成員賦值,其根據是否設置了安全管理器SecurityManager來賦值;最後調用reload方法。併發

public void reload() {
	providers.clear();
	lookupIterator = new LazyIterator(service, loader);
}
複製代碼

能夠看到reload方法是一個public方法,那麼在每次調用reload時就須要將以前加載的清空掉,因此直接使用providers這個map的clear方法清空掉緩存;接着使用剛纔賦值後的service和loader產生一個LazyIterator對象賦值給lookupIterator成員。app

LazyIterator是ServiceLoader的內部類,其定義以下:分佈式

private class LazyIterator
          implements Iterator<S> {
	Class<S> service;
	ClassLoader loader;
	Enumeration<URL> configs = null;
	Iterator<String> pending = null;
	String nextName = null;
	private LazyIterator(Class<S> service, ClassLoader loader) {
		this.service = service;
		this.loader = loader;
	}
	......
}
複製代碼

這裏就能夠看到ServiceLoader的實際加載過程就交給了LazyIterator來作,將ServiceLoader的service和loader成員分別賦值給了LazyIterator的service和loader成員。 configs是服務的URL枚舉; pending是保存要加載的服務的名稱集合; nextName是下一個要加載的服務名稱;ide

ServiceLoader實現了Iterable接口,其實現的iterator方法以下:

public Iterator<S> iterator() {
	return new Iterator<S>() {
		Iterator<Map.Entry<String,S>> knownProviders
		              = providers.entrySet().iterator();
		public Boolean hasNext() {
			if (knownProviders.hasNext())
			                  return true;
			return lookupIterator.hasNext();
		}
		public S next() {
			if (knownProviders.hasNext())
			                 return knownProviders.next().getValue();
			return lookupIterator.next();
		}
		public void remove() {
			throw new UnsupportedOperationException();
		}
	}
	;
}
複製代碼

能夠看到它是直接建立了一個Iterator對象返回;其knownProviders成員直接獲取providers的entrySet集合的迭代器;在hasNext和next方法中咱們能夠看到,它是先經過判斷knownProviders裏有沒有(即providers),若沒有再去lookupIterator中找; 前面咱們能夠看到providers裏並沒用put任何東西,那麼就說明put操做也是在lookupIterator中完成的。

先看到lookupIterator的next方法:

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);
	}
}
複製代碼

首先根據判斷acc是否爲空,若爲空則說明沒有設置安全策略直接調用nextService方法,不然以特權方式調用nextService方法。

private S nextService() {
	if (!hasNextService())
	          throw new NoSuchElementException();
	String cn = nextName;
	nextName = null;
	Class<?> c = null;
	try {
		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 {
		S p = service.cast(c.newInstance());
		providers.put(cn, p);
		return p;
	}
	catch (Throwable x) {
		fail(service,
		              "Provider " + cn + " could not be instantiated",
		              x);
	}
	throw new Error();
	// This cannot happen
}
複製代碼

首先根據hasNextService方法判斷,若爲false直接拋出NoSuchElementException異常,不然繼續執行。

hasNextService方法:

private Boolean hasNextService() {
	if (nextName != null) {
		return true;
	}
	if (configs == null) {
		try {
			String fullName = PREFIX + service.getName();
			if (loader == null)
			                  configs = ClassLoader.getSystemResources(fullName); else
			                 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;
}
複製代碼

hasNextService方法首先根據nextName成員是否爲空判斷,若不爲空,則說明已經初始化過了,直接返回true,不然繼續執行。接着configs成員是否爲空,configs 是一個URL的枚舉,如果configs 沒有初始化,就須要對configs初始化。 configs初始化邏輯也很簡單,首先根據PREFIX前綴加上PREFIX的全名獲得完整路徑,再根據loader的有無,獲取URL的枚舉。其中fail方法時ServiceLoader的靜態方法,用於異常的處理,後面給出。 在configs初始化完成後,還須要完成pending的初始化或者添加。 能夠看到只有當pending爲null,或者沒有元素時才進行循環。循環時如果configs裏沒有元素,則直接返回false;不然調用ServiceLoader的parse方法,經過service和URL給pending賦值;

parse方法:

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);
		}
	}
	return names.iterator();
}
複製代碼

能夠看到parse方法直接經過URL打開輸入流,經過parseLine一行一行地讀取將結果保存在names數組裏。

parseLine方法:

private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                            List<String> names)
          throws IOException, ServiceConfigurationError {
	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) {
		if ((ln.indexOf(' ') >= 0) || (ln.indexOf('t') >= 0))
             fail(service, u, lc, "Illegal configuration-file syntax");
         int cp = ln.codePointAt(0);
         if (!Character.isJavaIdentifierStart(cp))
             fail(service, u, lc, "Illegal provider-class name: " + ln);
         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;
 }
複製代碼

parseLine方法就是讀該URL對應地文件地一行,能夠看到經過對「#」的位置判斷,忽略註釋,而且剔除空格,接着是一系列的參數合法檢驗,而後判斷providers和names裏是否都沒包含這個服務名稱,若都沒包含names直接add,最後返回下一行的行標;

當parse將全部內容讀取完畢,返回names.iterator()賦值給hasNextService中的pending。循環結束,獲取pending中的第一個元素賦值給nextName,返回true,hasNextService方法結束。

在nextService方法往下執行時,先用cn保存nextName的值,再讓nextName=null,爲下一次的遍歷作準備;接着經過類加載,加載名爲cn的類,再經過該類實例化對象,並用providers緩存起來,最後返回該實例對象。

其中cast方法是判斷對象是否合法:

public T cast(Object obj) {
	if (obj != null && !isInstance(obj))
	         throw new ClassCastException(cannotCastMsg(obj));
	return (T) obj;
}
複製代碼

至此ServiceLoader的迭代器的next方法結束。其hasNext方法與其相似,就不詳細分析了。

而其remove方法就更直接,直接拋出異常來避免可能出現的危險狀況:

public void remove() {
	throw new UnsupportedOperationException();
}
複製代碼

其中使用到的靜態fail方法只是拋出異常:

private static void fail(Class<?> service, String msg, Throwable cause)
          throws ServiceConfigurationError {
	throw new ServiceConfigurationError(service.getName() + ": " + msg,
	                                              cause);
}
private static void fail(Class<?> service, String msg)
          throws ServiceConfigurationError {
	throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
         throws ServiceConfigurationError {
	fail(service, u + ":" + line + ": " + msg);
}
複製代碼

ServiceLoader除了load的兩個方法外還有個loadInstalled方法:

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方法不一樣在於loadInstalled使用的是擴展類加載器,而load使用的是傳入進來的或者是線程的上下文類加載器,其餘都同樣。

ServiceLoader源碼分析到此所有結束。

寫在最後

最後,歡迎作Java的工程師朋友們加入Java高級架構進階Qqun:963944895

羣內有技術大咖指點難題,還提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)

比你優秀的對手在學習,你的仇人在磨刀,你的閨蜜在減肥,隔壁老王在練腰, 咱們必須不斷學習,不然咱們將被學習者超越!

趁年輕,使勁拼,給將來的本身一個交代!

相關文章
相關標籤/搜索