詳解java實(shí)踐SPI機(jī)制及淺析源碼
1.概念
正式步入今天的核心內(nèi)容之前,溪源先給大家介紹一下關(guān)于SPI機(jī)制的相關(guān)概念,最后會(huì)提供實(shí)踐源代碼。
SPI即Service Provider Interface,屬于JDK內(nèi)置的一種動(dòng)態(tài)的服務(wù)提供發(fā)現(xiàn)機(jī)制,可以理解為運(yùn)行時(shí)動(dòng)態(tài)加載接口的實(shí)現(xiàn)類。更甚至,大家可以將SPI機(jī)制與設(shè)計(jì)模式中的策略模式建立聯(lián)系。
SPI機(jī)制:
從上圖中理解SPI機(jī)制:標(biāo)準(zhǔn)化接口+策略模式+配置文件;
SPI機(jī)制核心思想:系統(tǒng)設(shè)計(jì)的各個(gè)抽象,往往有很多不同的實(shí)現(xiàn)方案,在面向的對(duì)象的設(shè)計(jì)里,一般推薦模塊之間基于接口編程,模塊之間不對(duì)實(shí)現(xiàn)類進(jìn)行硬編碼。一旦代碼里涉及具體的實(shí)現(xiàn)類,就違反了可拔插的原則,如果需要替換一種實(shí)現(xiàn),就需要修改代碼。為了實(shí)現(xiàn)在模塊裝配的時(shí)候能不在程序里動(dòng)態(tài)指明,這就需要一種服務(wù)發(fā)現(xiàn)機(jī)制
使用場景:
1.數(shù)據(jù)庫驅(qū)動(dòng)加載:面對(duì)不同廠商的數(shù)據(jù)庫,JDBC需要加載不同類型的數(shù)據(jù)庫驅(qū)動(dòng); 2.日志接口實(shí)現(xiàn):SLF4J加載不同日志實(shí)現(xiàn)類; 3.溪源在實(shí)際開發(fā)中也使用了SPI機(jī)制:面對(duì)不同儀器平臺(tái)的結(jié)果文件上傳需要解析具體的結(jié)果,文件不同,解析邏輯不同,因此采用SPI機(jī)制能夠解耦和降低維護(hù)成本;SPI機(jī)制使用約定:
從上面的圖中,我們可以清晰的知道SPI的三部分:接口+實(shí)現(xiàn)類+配置文件;因此,項(xiàng)目中若要利用SPI機(jī)制,則需要遵循以下約定:
當(dāng)服務(wù)提供者提供了接口的一種具體實(shí)現(xiàn)后,在jar包的META-INF/services目錄下創(chuàng)建一個(gè)以“接口全限定名”為命名的文件,內(nèi)容為實(shí)現(xiàn)類的全限定名。 主程序通過java.util.ServiceLoder動(dòng)態(tài)裝載實(shí)現(xiàn)模塊,它通過掃描META-INF/services目錄下的配置文件找到實(shí)現(xiàn)類的全限定名,把類加載到JVM;注意:除SPI,我還發(fā)布了最新Java架構(gòu)項(xiàng)目實(shí)戰(zhàn)教程+大廠面試題庫, 點(diǎn)擊此處免費(fèi)獲取,小白勿進(jìn)!
2.實(shí)踐
整體包結(jié)構(gòu)如圖:
新建標(biāo)準(zhǔn)化接口:
public interface SayService { void say(String word);}
建立兩個(gè)實(shí)現(xiàn)類
@Servicepublic class ASayServiceImpl implements SayService { @Override public void say(String word) { System.out.println(word + ' A say: I am a boy'); }}@Servicepublic class BSayServiceImpl implements SayService { @Override public void say(String word) { System.out.println(word + ' B say: I am a girl'); }}
新建META-INF/services目錄和配置文件(以接口全限定名)
配置文件內(nèi)容為實(shí)現(xiàn)類全限定名
com.qxy.spi.impl.ASayServiceImplcom.qxy.spi.impl.BSayServiceImpl
單測
@SpringBootTest@RunWith(SpringRunner.class)public class SpiTest { static ServiceLoader<SayService> services = ServiceLoader.load(SayService.class); @Test public void test1() { for (SayService sayService : services) { sayService.say('Hello'); } }}
結(jié)果
Hello A say: I am a boyHello B say: I am a girl
3.源碼
源碼主要加載流程如下:
應(yīng)用程序調(diào)用ServiceLoader.load方法 ServiceLoader.load方法內(nèi)先創(chuàng)建一個(gè)新的ServiceLoader,并實(shí)例化該類中的成員變量;
loader(ClassLoader類型,類加載器) acc(AccessControlContext類型,訪問控制器) providers(LinkedHashMap<String,S>類型,用于緩存加載成功的類) lookupIterator(實(shí)現(xiàn)迭代器功能)應(yīng)用程序通過迭代器接口獲取對(duì)象實(shí)例 ServiceLoader先判斷成員變量providers對(duì)象中(LinkedHashMap<String,S>類型)是否有緩存實(shí)例對(duì)象,如果有緩存,直接返回。如果沒有緩存,執(zhí)行類的裝載。
讀取META-INF/services/下的配置文件,獲得所有能被實(shí)例化的類的名稱,值得注意的是,ServiceLoader可以跨越j(luò)ar包獲取META-INF下的配置文件; 通過反射方法Class.forName()加載類對(duì)象,并用instance()方法將類實(shí)例化。 把實(shí)例化后的類緩存到providers對(duì)象中,(LinkedHashMap<String,S>類型) 然后返回實(shí)例對(duì)象。public final class ServiceLoader<S> implements Iterable<S>{ // 加載具體實(shí)現(xiàn)類信息的前綴 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; // 創(chuàng)建ServiceLoader時(shí)采用的訪問控制上下文 // The access control context taken when the ServiceLoader is created private final AccessControlContext acc; // 用于緩存已經(jīng)加載的接口實(shí)現(xiàn)類,其中key為實(shí)現(xiàn)類的完整類名 // Cached providers, in instantiation order private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); // 用于延遲加載接口的實(shí)現(xiàn)類 // The current lazy-lookup iterator private LazyIterator lookupIterator; public void reload() { providers.clear(); 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(); } 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); } // Parse a single line from the given configuration file, adding the name // on the line to the names list. //具體解析資源文件中的每一行內(nèi)容 private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError { String ln = r.readLine(); if (ln == null) { //-1表示解析完成 return -1; } // 如果存在’#’字符,截取第一個(gè)’#’字符串之前的內(nèi)容,’#’字符之后的屬于注釋內(nèi)容 int ci = ln.indexOf(’#’); if (ci >= 0) ln = ln.substring(0, ci); ln = ln.trim(); int n = ln.length(); if (n != 0) { //不合法的標(biāo)識(shí):’ ’、’t’ if ((ln.indexOf(’ ’) >= 0) || (ln.indexOf(’t’) >= 0))fail(service, u, lc, 'Illegal configuration-file syntax'); int cp = ln.codePointAt(0); //判斷第一個(gè) char 是否一個(gè)合法的 Java 起始標(biāo)識(shí)符 if (!Character.isJavaIdentifierStart(cp))fail(service, u, lc, 'Illegal provider-class name: ' + ln); //判斷所有其他字符串是否屬于合法的Java標(biāo)識(shí)符 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; } 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(); } // Private inner class implementing fully-lazy provider lookup // private class LazyIterator implements Iterator<S> { Class<S> service; ClassLoader loader; // 加載資源的URL集合 Enumeration<URL> configs = null; // 需加載的實(shí)現(xiàn)類的全限定類名的集合 Iterator<String> pending = null; // 下一個(gè)需要加載的實(shí)現(xiàn)類的全限定類名 String nextName = null; private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; } private boolean hasNextService() { if (nextName != null) {return true; } if (configs == null) {try {// 資源名稱,META-INF/services + 全限定名 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);} } // 從資源中解析出需要加載的所有實(shí)現(xiàn)類的全限定名 while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) { return false;}pending = parse(service, configs.nextElement()); } //下一個(gè)需要加載的實(shí)現(xiàn)類全限定名 nextName = pending.next(); return true; } private S nextService() { if (!hasNextService())throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { //反射構(gòu)造Class實(shí)例c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) {fail(service, 'Provider ' + cn + ' not found'); } // 類型判斷,校驗(yàn)實(shí)現(xiàn)類必須與當(dāng)前加載的類/接口的關(guān)系是派生或相同,否則拋出異常終止 if (!service.isAssignableFrom(c)) {fail(service, 'Provider ' + cn + ' not a subtype'); } try { //強(qiáng)轉(zhuǎn)S p = service.cast(c.newInstance()); // 實(shí)例完成,添加緩存,Key:實(shí)現(xiàn)類全限定類名,Value:實(shí)現(xiàn)類實(shí)例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(); } } 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(); } }; } public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { // 返回ServiceLoader的實(shí)例 return new ServiceLoader<>(service, loader); } 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); } public String toString() { return 'java.util.ServiceLoader[' + service.getName() + ']'; }}
4.總結(jié)
SPI機(jī)制在實(shí)際開發(fā)中使用得場景也有很多。特別是統(tǒng)一標(biāo)準(zhǔn)的不同廠商實(shí)現(xiàn),溪源也正是利用SPI機(jī)制(但略做改進(jìn),避免過多加載資源浪費(fèi))實(shí)現(xiàn)不同技術(shù)平臺(tái)的結(jié)果文件解析需求。
優(yōu)點(diǎn)
使用Java SPI機(jī)制的優(yōu)勢是實(shí)現(xiàn)解耦,使得第三方服務(wù)模塊的裝配控制的邏輯與調(diào)用者的業(yè)務(wù)代碼分離,而不是耦合在一起。應(yīng)用程序可以根據(jù)實(shí)際業(yè)務(wù)情況啟用框架擴(kuò)展或替換框架組件。
缺點(diǎn)
雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取,也就是接口的實(shí)現(xiàn)類全部加載并實(shí)例化一遍。如果你并不想用某些實(shí)現(xiàn)類,它也被加載并實(shí)例化了,這就造成了浪費(fèi)。
源碼傳送門:SPI Service
到此這篇關(guān)于詳解java實(shí)踐SPI機(jī)制及淺析源碼的文章就介紹到這了,更多相關(guān)java SPI機(jī)制內(nèi)容請搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!
相關(guān)文章:
1. ASP基礎(chǔ)入門第三篇(ASP腳本基礎(chǔ))2. ASP基礎(chǔ)入門第八篇(ASP內(nèi)建對(duì)象Application和Session)3. XML入門的常見問題(二)4. Jsp中request的3個(gè)基礎(chǔ)實(shí)踐5. 如何在jsp界面中插入圖片6. jsp+servlet實(shí)現(xiàn)猜數(shù)字游戲7. ASP.NET MVC使用異步Action的方法8. 低版本IE正常運(yùn)行HTML5+CSS3網(wǎng)站的3種解決方案9. jsp EL表達(dá)式詳解10. XML解析錯(cuò)誤:未組織好 的解決辦法
