【Java】ServiceLoader原始碼分析

鬆餅人發表於2019-05-15

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

先看下它的成員:

 1 public final class ServiceLoader<S>
 2     implements Iterable<S> {
 3 
 4     private static final String PREFIX = "META-INF/services/";
 5 
 6     // The class or interface representing the service being loaded
 7     private final Class<S> service;
 8 
 9     // The class loader used to locate, load, and instantiate providers
10     private final ClassLoader loader;
11 
12     // The access control context taken when the ServiceLoader is created
13     private final AccessControlContext acc;
14 
15     // Cached providers, in instantiation order
16     private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
17 
18     // The current lazy-lookup iterator
19     private LazyIterator lookupIterator;
20     
21     ......
22         
23 }

可以看到他首先是實現了Iterable介面,可以迭代。
PREFIX:指明瞭路徑是在"META-INF/services/"下。
service:表示正在載入的服務的類或介面。
loader:使用的類載入器。
acc:建立ServiceLoader時獲取的訪問控制上下文。
providers :快取的服務提供集合。
lookupIterator:是其內部使用的迭代器,用於類的懶載入,只有在迭代時載入。

其構造方法是一個private方法,不對外提供,在使用時我們需要呼叫其靜態的load方法,由其自身產生ServiceLoader物件:

1 public static <S> ServiceLoader<S> load(Class<S> service) {
2         ClassLoader cl = Thread.currentThread().getContextClassLoader();
3         return ServiceLoader.load(service, cl);
4 }
5 
6 public static <S> ServiceLoader<S> load(Class<S> service,
7                                             ClassLoader loader) {
8         return new ServiceLoader<>(service, loader);
9 }

可以看到對load方法進行了過載,其中引數service是要載入的類;單參方法沒有類載入器,使用的是當前執行緒的類載入器;最後呼叫的是雙參的load方法;而雙參的load方法也很簡單,只是直接呼叫ServiceLoader的構造方法,例項化了一個物件。

 

1 private ServiceLoader(Class<S> svc, ClassLoader cl) {
2         service = Objects.requireNonNull(svc, "Service interface cannot be null");
3         loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
4         acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
5         reload();
6 }

可以看到其構造方法邏輯依舊很簡單,首先是判斷傳入的svc(即傳入的service)是否為空,若是為空直接報異常,否則給service 成員賦值:

1 public static <T> T requireNonNull(T obj, String message) {
2         if (obj == null)
3             throw new NullPointerException(message);
4         return obj;
5 }

然後給進行cl的非空判斷,給loader 成員賦值;接著給acc 成員賦值,其根據是否設定了安全管理器SecurityManager來賦值;最後呼叫reload方法。

1 public void reload() {
2         providers.clear();
3         lookupIterator = new LazyIterator(service, loader);
4 }

可以看到reload方法是一個public方法,那麼在每次呼叫reload時就需要將之前載入的清空掉,所以直接使用providers這個map的clear方法清空掉快取;接著使用剛才賦值後的service和loader產生一個LazyIterator物件賦值給lookupIterator成員。

LazyIterator是ServiceLoader的內部類,其定義如下:

 1 private class LazyIterator
 2         implements Iterator<S> {
 3     Class<S> service;
 4     ClassLoader loader;
 5     Enumeration<URL> configs = null;
 6     Iterator<String> pending = null;
 7     String nextName = null;
 8     
 9     private LazyIterator(Class<S> service, ClassLoader loader) {
10         this.service = service;
11         this.loader = loader;
12     }
13     ......
14 }

這裡就可以看到ServiceLoader的實際載入過程就交給了LazyIterator來做,將ServiceLoader的service和loader成員分別賦值給了LazyIterator的service和loader成員。
configs是服務的URL列舉;
pending是儲存要載入的服務的名稱集合;
nextName是下一個要載入的服務名稱;

ServiceLoader實現了Iterable介面,其實現的iterator方法如下:

 1 public Iterator<S> iterator() {
 2     return new Iterator<S>() {
 3         Iterator<Map.Entry<String,S>> knownProviders
 4             = providers.entrySet().iterator();
 5     
 6         public boolean hasNext() {
 7             if (knownProviders.hasNext())
 8                 return true;
 9             return lookupIterator.hasNext();
10         }
11     
12         public S next() {
13             if (knownProviders.hasNext())
14                 return knownProviders.next().getValue();
15             return lookupIterator.next();
16         }
17     
18         public void remove() {
19             throw new UnsupportedOperationException();
20         }
21     
22     };
23 }

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

先看到lookupIterator的next方法:

 1 public S next() {
 2    if (acc == null) {
 3         return nextService();
 4     } else {
 5         PrivilegedAction<S> action = new PrivilegedAction<S>() {
 6             public S run() { return nextService(); }
 7         };
 8         return AccessController.doPrivileged(action, acc);
 9     }
10 }

首先根據判斷acc是否為空,若為空則說明沒有設定安全策略直接呼叫nextService方法,否則以特權方式呼叫nextService方法。

 

 1 private S nextService() {
 2     if (!hasNextService())
 3         throw new NoSuchElementException();
 4     String cn = nextName;
 5     nextName = null;
 6     Class<?> c = null;
 7     try {
 8         c = Class.forName(cn, false, loader);
 9     } catch (ClassNotFoundException x) {
10         fail(service,
11              "Provider " + cn + " not found");
12     }
13     if (!service.isAssignableFrom(c)) {
14         fail(service,
15              "Provider " + cn  + " not a subtype");
16     }
17     try {
18         S p = service.cast(c.newInstance());
19         providers.put(cn, p);
20         return p;
21     } catch (Throwable x) {
22         fail(service,
23              "Provider " + cn + " could not be instantiated",
24              x);
25     }
26     throw new Error();          // This cannot happen
27 }

首先根據hasNextService方法判斷,若為false直接丟擲NoSuchElementException異常,否則繼續執行。

hasNextService方法:

 1 private boolean hasNextService() {
 2     if (nextName != null) {
 3         return true;
 4     }
 5     if (configs == null) {
 6         try {
 7             String fullName = PREFIX + service.getName();
 8             if (loader == null)
 9                 configs = ClassLoader.getSystemResources(fullName);
10             else
11                 configs = loader.getResources(fullName);
12         } catch (IOException x) {
13             fail(service, "Error locating configuration files", x);
14         }
15     }
16     while ((pending == null) || !pending.hasNext()) {
17         if (!configs.hasMoreElements()) {
18             return false;
19         }
20         pending = parse(service, configs.nextElement());
21     }
22     nextName = pending.next();
23     return true;
24 }

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方法:

 1 private Iterator<String> parse(Class<?> service, URL u)
 2         throws ServiceConfigurationError {
 3     InputStream in = null;
 4     BufferedReader r = null;
 5     ArrayList<String> names = new ArrayList<>();
 6     try {
 7         in = u.openStream();
 8         r = new BufferedReader(new InputStreamReader(in, "utf-8"));
 9         int lc = 1;
10         while ((lc = parseLine(service, u, r, lc, names)) >= 0);
11     } catch (IOException x) {
12         fail(service, "Error reading configuration file", x);
13     } finally {
14         try {
15             if (r != null) r.close();
16             if (in != null) in.close();
17         } catch (IOException y) {
18             fail(service, "Error closing configuration file", y);
19         }
20     }
21     return names.iterator();
22 }

可以看到parse方法直接通過URL開啟輸入流,通過parseLine一行一行地讀取將結果儲存在names陣列裡。

parseLine方法:

 1 private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
 2                           List<String> names)
 3         throws IOException, ServiceConfigurationError {
 4     String ln = r.readLine();
 5     if (ln == null) {
 6         return -1;
 7     }
 8     int ci = ln.indexOf('#');
 9     if (ci >= 0) ln = ln.substring(0, ci);
10     ln = ln.trim();
11     int n = ln.length();
12     if (n != 0) {
13         if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
14             fail(service, u, lc, "Illegal configuration-file syntax");
15         int cp = ln.codePointAt(0);
16         if (!Character.isJavaIdentifierStart(cp))
17             fail(service, u, lc, "Illegal provider-class name: " + ln);
18         for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
19             cp = ln.codePointAt(i);
20             if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
21                 fail(service, u, lc, "Illegal provider-class name: " + ln);
22         }
23         if (!providers.containsKey(ln) && !names.contains(ln))
24             names.add(ln);
25     }
26     return lc + 1;
27 }

parseLine方法就是讀該URL對應地檔案地一行,可以看到通過對“#”的位置判斷,忽略註釋,並且剔除空格,接著是一系列的引數合法檢驗,然後判斷providers和names裡是否都沒包含這個服務名稱,若都沒包含names直接add,最後返回下一行的行標;

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

在nextService方法往下執行時,先用cn儲存nextName的值,再讓nextName=null,為下一次的遍歷做準備;接著通過類載入,載入名為cn的類,再通過該類例項化物件,並用providers快取起來,最後返回該例項物件。

其中cast方法是判斷物件是否合法:

1 public T cast(Object obj) {
2     if (obj != null && !isInstance(obj))
3         throw new ClassCastException(cannotCastMsg(obj));
4     return (T) obj;
5 }

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

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

1 public void remove() {
2     throw new UnsupportedOperationException();
3 }

 

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

 1 private static void fail(Class<?> service, String msg, Throwable cause)
 2         throws ServiceConfigurationError {
 3     throw new ServiceConfigurationError(service.getName() + ": " + msg,
 4                                             cause);
 5 }
 6 
 7 private static void fail(Class<?> service, String msg)
 8         throws ServiceConfigurationError {
 9     throw new ServiceConfigurationError(service.getName() + ": " + msg);
10 }
11 
12 private static void fail(Class<?> service, URL u, int line, String msg)
13         throws ServiceConfigurationError {
14     fail(service, u + ":" + line + ": " + msg);
15 }

 

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

1 public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
2     ClassLoader cl = ClassLoader.getSystemClassLoader();
3     ClassLoader prev = null;
4     while (cl != null) {
5         prev = cl;
6         cl = cl.getParent();
7     }
8     return ServiceLoader.load(service, prev);
9 }

該方法與load方法不同在於loadInstalled使用的是擴充套件類載入器,而load使用的是傳入進來的或者是執行緒的上下文類載入器,其他都一樣。

 

ServiceLoader原始碼分析到此全部結束。

 

相關文章