1. 程式人生 > >Java程式設計技術之淺析SPI服務發現機制

Java程式設計技術之淺析SPI服務發現機制

### SPI服務發現機制 >SPI是Java JDK內部提供的一種服務發現機制。 * SPI->Service Provider Interface,服務提供介面,是Java JDK內建的一種服務發現機制 * 通過在ClassPath路徑下的META-INF/services資料夾查詢檔案,自動載入檔案裡所定義的類 >[⚠️注意事項]: >面向物件的設計裡,一般推薦模組之間基於介面程式設計,模組之間不對實現類進行編碼。如果涉及實現類就會違反可插拔的原則,針對於模組裝配,Java SPI提供了為某個介面尋找服務的實現機制。 ### SPI規範 * 使用約定: [1].編寫服務提供介面,可以是抽象介面和函式介面,JDK1.8之後推薦使用函式介面 [2].在jar包的META-INF/services/目錄裡建立一個以服務介面命名的檔案。其實就是實現該服務介面的具體實現類。 ```txt 提供一個目錄: META-INF/services/ 放到ClassPath下面 ``` [3].當外部程式裝配這個模組的時候,就能通過該Jar包META-INF/services/配置檔案找到具體的實現類名,並裝載例項化,完成模組注入。 ```txt 目錄下放置一個配置檔案: 檔名是需要拓展的介面全限定名稱 檔案內部為要實現的介面實現類 檔案必須為UTF-8編碼 ``` [4].尋找服務介面實現,不用在程式碼中提供,而是利用JDK提供服務查詢工具類:java.util.ServiceLoader類來載入使用: ```Java ServiceLoader.load(xxx.class) ServiceLoader loads = ServiceLoader.load(xxx.class) ``` ### SPI原始碼分析 [1].ServiceLoader原始碼: ![YMEMlV.png](https://img2020.cnblogs.com/other/1778572/202101/1778572-20210128165726192-2105759965.png) ```java package java.util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.security.AccessController; import java.security.AccessControlContext; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; public final class ServiceLoader implements Iterable { //[1].初始化定義全域性配置檔案路徑Path private static final String PREFIX = "META-INF/services/"; //[2].初始化定義載入的服務類或介面 private final Class service; //[3].初始化定義類載入器 private final ClassLoader loader; //[4].初始化定義訪問控制上下文 private final AccessControlContext acc; //[5].初始化定義載入服務類的快取集合 private LinkedHashMap providers = new LinkedHashMap<>(); //[6].初始化定義私有內部LazyIterator類,真正載入服務類的實現類 private LazyIterator lookupIterator; //私有化有參構造-> ServiceLoader(Class svc, ClassLoader cl) private ServiceLoader(Class svc, ClassLoader cl) { //[1].例項化服務介面->Class service = Objects.requireNonNull(svc, "Service interface cannot be null"); //[2].例項化類載入器->ClassLoader loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; //[3].例項化訪問控制上下文->AccessControlContext acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; //[4].回撥函式->reload reload(); } public void reload() { //[1].清空快取例項集合 providers.clear(); //[2].例項化私有內部LazyIterator類->LazyIterator lookupIterator = new LazyIterator(service, loader); } public static ServiceLoader load(Class service,ClassLoader loader) { return new ServiceLoader<>(service, loader); } public static ServiceLoader load(Class service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } } ``` 2.LazyIterator原始碼: ![YMeW11.png](https://img2020.cnblogs.com/other/1778572/202101/1778572-20210128165727366-1724987721.png) ```java private class LazyIterator implements Iterator { Class service; ClassLoader loader; Enumeration configs = null; Iterator pending = null; String nextName = null; private LazyIterator(Class service, ClassLoader loader) { this.service = service; this.loader = loader; } 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; } 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 } public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction action = new PrivilegedAction() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } public void remove() { throw new UnsupportedOperationException(); } } ``` ### 使用舉例 [1].Dubbo SPI 機制: ``` META-INF/dubbo.internal/xxx=介面全限定名 ``` Dubbo 並未使用 Java SPI,而是重新實現了一套功能更強的 SPI 機制。 Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以載入指定的實現類。Dubbo SPI 所需的配置檔案需放置在 META-INF/dubbo 路徑下。 與Java SPI 實現類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣我們可以按需載入指定的實現類。另外,在測試 Dubbo SPI 時,需要在 Robot 介面上標註 @SPI 註解。 [2].Cache SPI 機制: ``` META-INF/service/javax.cache.spi.CachingProvider=xxx ``` [3]Spring SPI 機制: ``` META-INF/services/org.apache.commons.logging.LogFactory=xxx ``` [4].SpringBoot SPI機制: ``` META-INF/spring.factories/org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx ``` 在springboot的自動裝配過程中,最終會載入META-INF/spring.factories檔案,而載入的過程是由SpringFactoriesLoader載入的。從CLASSPATH下的每個Jar包中搜尋所有META-INF/spring.factories配置檔案,然後將解析properties檔案,找到指定名稱的配置後返回 原始碼: ``` public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; // spring.factories檔案的格式為:key=value1,value2,value3 // 從所有的jar包中找到META-INF/spring.factories檔案 // 然後從檔案中解析出key=factoryClass類名稱的所有value值 public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); // 取得資原始檔的URL Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List result = new ArrayList(); // 遍歷所有的URL while (urls.hasMoreElements()) { URL url = urls.nextElement(); // 根據資原始檔URL解析properties檔案,得到對應的一組@Configuration類 Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); // 組裝資料,並返回 result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result; } ``` [5].自定義序列化實現SPI:META-INF/services/xxx=介面全限定名 ``` 參考學習Java SPI 和Dubbo SPI機制原始碼,自己動手實現序列化工具類等 ``` > 版權宣告:本文為博主原創文章,遵循相關版權協議,如若轉載或者分享請附上原文出處連結和連結