一言不合就貼概念:java
SPI 全稱爲 (Service Provider Interface) ,是JDK內置的一種服務提供發現機制。 目前有很多框架用它來作服務的擴展發現, 簡單來講,它就是一種動態替換髮現的機制, 舉個例子來講, 有個接口,想運行時動態的給它添加實現,你只須要添加一個實現緩存
是否是說了這麼多還不知道他到底是幹什麼的?我也是。。安全
不過給個人理解是,我只須要接口就夠了,實現類我不去管,也不須要new,有點相似於IOC的模式,至於適用場景嘛,你們能夠本身YY一下。。app
這時候就會有人問,有了SPI就不須要寫實現類了嗎?錯。爲何呢,且聽我一一道來。框架
首先引用一張圖ide
那麼能夠看得出,首先咱們須要在Classpath中有一個叫META-INF/services的文件夾,至於爲何是這個,以後在源碼中會體現出來,暫時賣個關子測試
第二步就是建立接口類。ui
好比咱們弄一個叫DemoService的類。spa
/** * SPI Test * @author Autorun * Created by Autorun on 2018/1/26. */ public interface DemoService { void sayHello(); }
第三步就是建立該service對應的實現類。code
/** * SPI Test impl * @author Autorun * Created by Autorun on 2018/1/26. */ public class DemoServiceImpl implements DemoService { @Override public void sayHello() { System.out.println("hello SPI!!!!"); } }
第四步:咱們須要在 META-INF/services 中建立對應的映射文件(暫且叫映射文件吧), 我以爲也比較形象
文件名稱與接口類的全類名保持一致
我這裏放到了 org.jdk.demo.java.util.spi.DemoService 這個下面,因此對應的文件名應該是 org.jdk.demo.java.util.spi.DemoService
而後將實現類的路徑配置到該文件中。
我這裏對這個接口有2個實現類。那麼能夠寫兩個(原則上應該是能夠寫無數個的)
而後咱們寫一個對應的測試類,用於測試咱們寫的方法
/** * @author Autorun * Created by Autorun on 2018/1/26. */ public class DemoServiceTest { public static void main(String[] args) { ServiceLoader<DemoService> services = ServiceLoader.load(DemoService.class); Iterator<DemoService> it = services.iterator(); while (it.hasNext()) { DemoService service = it.next(); service.sayHello(); } } }
在這裏。咱們測試一下。
會發現。他把咱們實現的方法所有都調用了一遍。經過迭代器中調用sayHello方法
先貼屬性
咱們看到了META-INF/services/ 因此,呵呵,知道爲何必須寫在這個目錄下了吧?
而後咱們開始看看ServiceLoader.load(DemoService.class)這一步他到底作了什麼,它是如何將咱們的每個實現類的方法執行起來的
public static <S> ServiceLoader<S> load(Class<S> service) { // 獲取上下文的類加載器。而後調用重載的方法(ServiceClass, 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); }
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這個對象,並將類加載器和接口類賦值給私有變量並調用了reload方法。咱們看看reload方法作了什麼
public void reload() { providers.clear(); lookupIterator = new LazyIterator(service, loader); }
它又建立了一個內部類,叫lazyIterator,經過名字咱們能夠看得出他是一個迭代器對象的實現類。順便說一嘴,這個類纔是真正去調用SPI類的地方,咱們從上面本身寫的測試代碼中應該就明白了。
廢話很少說,繼續貼代碼。。
咱們while中調用了hasNext方法。咱們看看它內部作了什麼
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); } }
第一次進來的時候acc確定是空的,由於沒有看到有給它賦值的地方。
它調用了內部的hasNextService方法。
private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // meta-inf/services/xxx.DemoService 全名稱 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); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; }
// 解析文件細節 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(); }
繼續看獲取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); } }
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 中 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
那麼能夠得出,他會把該文件中全部的行都讀到,而且初始化,放入providers 的集合中。而後將該實例返回回去。
咱們自行調用sayHello方法。那就執行了對應的實現類代碼啦
說到這裏,咱們應該很清晰的對JDK本身提供的SPI機制有了詳細的認知。那麼,他有沒有什麼問題呢?
答案是確定的,
1:懶加載,這個沒毛病。可是隻要一調用next方法,就會把全部的實現類都加載進來。對咱們來說是不科學的。這也不是咱們須要的,由於在特定場合咱們只須要指定的類就夠了,而不須要那麼多實現類。
2:內部緩存沒對外開放,也能夠理解,安全。那代價就是咱們須要本身去將具體的實現類放入咱們的容器中。
總結:
其實他的SPI作了以下事情:
1.根據對應的接口類去找META-INF/services/下的接口類名
2:經過service獲取對應的迭代器,調用hasNext方法去將META-INF/services/下對應全限定名文件中的類解析出來。
3:調用next方法加載類,並將其緩存到provides的map中。
並將其對應的實現類返回
小做業(留給有心的人): 自行實現一套SPI機制,實現根據指定類去加載對應的實現類。並將其緩存起來。
最後歡迎你們加入 JAVA那些事 77174608 一塊兒成長進步。。