淺析Java中的SPI原理
在面向?qū)ο蟮某绦蛟O(shè)計(jì)中,模塊之間交互采用接口編程,通常情況下調(diào)用方不需要知道被調(diào)用方的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),因?yàn)橐坏┥婕暗搅司唧w實(shí)現(xiàn),如果需要換一種實(shí)現(xiàn)就需要修改代碼,這違反了程序設(shè)計(jì)的"開閉原則"。所以我們一般有兩種選擇:一種是使用API(Application Programming Interface),另一種是SPI(Service Provider Interface),API通常被應(yīng)用程序開發(fā)人員使用,而SPI通常被框架擴(kuò)展人員使用。
在進(jìn)入下面學(xué)習(xí)之前,我們先來再加深一下API和SPI這兩個(gè)的印象:
API:由實(shí)現(xiàn)方制定接口標(biāo)準(zhǔn)并完成對(duì)接口的不同實(shí)現(xiàn),這種模式服務(wù)接口從概念上更接近于實(shí)現(xiàn)方;
SPI:由調(diào)用方制定接口標(biāo)準(zhǔn),實(shí)現(xiàn)方來針對(duì)接口提供不同的實(shí)現(xiàn);從前半句話我們來看,SPI其實(shí)就是"為接口查找實(shí)現(xiàn)"的一種服務(wù)發(fā)現(xiàn)機(jī)制;這種模式,服務(wù)接口組織上位于調(diào)用方所在的包中,實(shí)現(xiàn)位于獨(dú)立的包中。
API和SPI簡(jiǎn)略圖示:
看完上面的簡(jiǎn)單圖示,相信大家對(duì)API和SPI的區(qū)別有了一個(gè)大致的了解,現(xiàn)在我們使用SPI機(jī)制來實(shí)現(xiàn)我們一個(gè)簡(jiǎn)單的日志框架:
第一步,創(chuàng)建一個(gè)maven項(xiàng)目命名為spi-interface,定義一個(gè)SPI對(duì)外服務(wù)接口,用來后續(xù)提供給調(diào)用者使用;
package cn.com.wwh; /** * * @FileName Logger.java * @version:1.0 * @Description: 服務(wù)提供者接口 * @author: wwh * @date: 2022年9月19日 上午10:31:53 */ public interface Logger { /** * * @Description:(功能描述) * @param msg */ public void info(String msg); /** * * @Description:(功能描述) * @param msg */ public void debug(String msg); }
package cn.com.wwh; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; /** * * @FileName LoggerService.java * @version:1.0 * @Description: 為服務(wù)的調(diào)用者提供特定的功能,是SPI的核心功能 * @author: wwh * @date: 2022年9月19日 上午10:33:30 */ public class LoggerService { private static final LoggerService INSTANCE = new LoggerService(); private final Logger logger; private final List<Logger> loggers = new ArrayList<>(); private LoggerService() { //ServiceLoader是實(shí)現(xiàn)SPI的核心類 ServiceLoader<Logger> sl = ServiceLoader.load(Logger.class); Iterator<Logger> it = sl.iterator(); while (it.hasNext()) { loggers.add(it.next()); } if (!loggers.isEmpty()) { logger = loggers.get(0); } else { logger = null; } } /** * @Description:(功能描述) * @return */ public static LoggerService getLoggerService() { return INSTANCE; } /** * * @Description:(功能描述) * @param msg */ public void info(String msg) { if (logger == null) { System.err.println("在info方法中沒有找到Logger的實(shí)現(xiàn)類..."); } else { logger.info(msg); } } /** * * @Description:(功能描述) * @param msg */ public void debug(String msg) { if (logger == null) { System.err.println("在debug方法中沒有找到Logger的實(shí)現(xiàn)類..."); } else { logger.info(msg); } } }
將上面這個(gè)這個(gè)項(xiàng)目打成spi-interface.jar包。
第二步,新建一個(gè)maven項(xiàng)目并導(dǎo)入第一步中打出來的spi-interface.jar包,這個(gè)項(xiàng)目用來提供服務(wù)的實(shí)現(xiàn),定義一個(gè)類,實(shí)現(xiàn)第一步中定義的cn.com.wwh.Logger接口,示例代碼如下:
package cn.com.wwh; import cn.com.pep.Logger; /** * * @FileName Logback.java * @version:1.0 * @Description: 服務(wù)接口的實(shí)現(xiàn)類 * @author: wwh * @date: 2022年9月19日 上午10:50:31 */ public class Logback implements Logger { @Override public void debug(String msg) { System.err.println("調(diào)用Logback的debug方法,輸出的日志為:" + msg); } @Override public void info(String msg) { System.err.println("調(diào)用Logback的info方法,輸出的日志為:" + msg); } }
同時(shí)在當(dāng)前項(xiàng)目的classpath路徑下建立META-INF/services/文件夾(至于為什么這么建立目錄,我們一會(huì)兒再解釋),并且新建一個(gè)名稱為cn.com.wwh.Logger內(nèi)容為cn.com.wwh.Logback的文件,這一步是關(guān)鍵(具體作用后面再詳細(xì)說明),然后將上面第二步這個(gè)這個(gè)項(xiàng)目打成spi-provider.jar包,供給之后使用,我目前使用的開發(fā)工具是Eclipse,目錄結(jié)構(gòu)如下圖所示:
第三步,編寫測(cè)試類,新建一個(gè)maven項(xiàng)目,命名為spi-test,導(dǎo)入前面兩個(gè)步驟打的spi-interface.jar和spi-provider.jar這兩個(gè)jar包,并編寫測(cè)試代碼,示例如下:
package cn.com.wwh; import cn.com.pep.LoggerService; /** * * @FileName SpiTest.java * @version:1.0 * @Description: * @author: wwh * @date: 2022年9月19日 上午10:56:31 */ public class SpiTest { public static void main(String[] args) { LoggerService logger = LoggerService.getLoggerService(); logger.info("我是中國人"); logger.debug("白菜多少錢一斤"); } }
有了SPI我們可以將服務(wù)和服務(wù)提供者輕松地解耦,假如將來的某一天我們需要將日志保存到數(shù)據(jù)庫,或者通過網(wǎng)絡(luò)發(fā)送,我們直接只需要替換針對(duì)服務(wù)接口的實(shí)現(xiàn)類即可,別的地方都不用修改,這更符合程序設(shè)計(jì)中的“開閉原則”。
SPI的大致原理是:應(yīng)用啟動(dòng)的時(shí)候,掃描classpath下面的所有jar包,將jar包下的/META-INF/services/目錄下的文件加載到內(nèi)存中,進(jìn)行一系列的解析(文件的名稱是spi接口的全路徑名稱,文件內(nèi)容應(yīng)該是spi接口實(shí)現(xiàn)類的全路徑名,可以用多個(gè)實(shí)現(xiàn)類,在文件中換行保存),之后判斷當(dāng)前類和當(dāng)前接口是否是同一類型?結(jié)果為true,則通過反射生成指定類的實(shí)例對(duì)象,保存到一個(gè)map集合中,可以通過遍歷或者迭代的方式拿出來使用。
SPI實(shí)質(zhì)就是一個(gè)加載服務(wù)實(shí)現(xiàn)的工具,核心類是ServiceLoader,其實(shí)了解了SPI的原理,我們?cè)俳又骄縅DK中的源碼就沒有那么費(fèi)力了,下面我們開始源碼分析吧。
ServiceLoader類是定義在java.util包下的,使用final定義禁止子類繼承和修改,實(shí)現(xiàn)了Iterable接口,使得可以通過迭代或者遍歷的方式獲取SPI接口的不同實(shí)現(xiàn)。
從上面的我們所舉的例子中,我們知道SPI的入口是ServiceLoader.load(Class<S> service)方法,我們來看看它都干了什么?
上面的這4步總的來說,就是使用指定的類型和當(dāng)前線程綁定的classLoader實(shí)例化了一個(gè)LazyIterator對(duì)象賦值給lookupIterator這個(gè)引用,并且清除了原來providers列表中緩存的服務(wù)的實(shí)現(xiàn)。接下來我們調(diào)用了ServiceLoader實(shí)例的iterator()方法獲取了一個(gè)迭代器,代碼如下:
public Iterator<S> iterator() { //通過匿名內(nèi)部類方式提供了一個(gè)迭代器 return new Iterator<S>() { //獲取緩存的服務(wù)實(shí)現(xiàn)者的迭代器 Iterator<Map.Entry<String, S>> knownProviders = providers.entrySet().iterator(); //判斷迭代器中是否還有元素 public boolean hasNext() { //緩存的服務(wù)實(shí)現(xiàn)者的迭代器中已經(jīng)沒有元素了 if (knownProviders.hasNext()) return true; return lookupIterator.hasNext();//判斷延遲加載的迭代器中是否還有元素 } //獲取迭代其中的下一個(gè)元素 public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next();//獲取延遲加載的迭代器中的下一個(gè)元素 } public void remove() { throw new UnsupportedOperationException(); } }; }
我們接著調(diào)用上步獲取的迭代器it的hasNext()方法,因?yàn)槲覀冊(cè)赟erviceLoader.load()過程中其實(shí)是清除了providers列表中的緩存服務(wù)實(shí)現(xiàn)的,所以其實(shí)調(diào)用的是lookupIterator.hasNext()方法,如下:
public boolean hasNext() { if (nextName != null) {//存在下一個(gè)元素 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); } } //遍歷配置文件內(nèi)容 while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement());//配置文件內(nèi)容解析 } nextName = pending.next();//獲取服務(wù)實(shí)現(xiàn)類的全路徑名 return true; }
假如上部判斷為true,緊接著我們又調(diào)用了迭代器it的next()方式,同理也調(diào)用的是lookupIterator.next()方法,源碼如下:
public S next() { if (!hasNext()) { throw new NoSuchElementException(); } String cn = nextName;//文件中保存的服務(wù)接口實(shí)現(xiàn)類的全路徑名 nextName = null; Class<?> c = null; try { //獲取全限定名的Class對(duì)象 c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } //判斷實(shí)現(xiàn)類和服務(wù)接口是否是同一類型 if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype"); } try { //通過反射生成服務(wù)接口的實(shí)現(xiàn)類,并判斷這個(gè)實(shí)例是否是接口的實(shí)現(xiàn) S p = service.cast(c.newInstance()); //將服務(wù)接口的實(shí)現(xiàn)緩存起來,并返回 providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
其實(shí)spi實(shí)現(xiàn)的主要流程是:掃描classpath路徑下的所有jar包下的/META-INF/services/目錄(即我們需要將服務(wù)接口的具體實(shí)現(xiàn)類暴露在這個(gè)目錄下,之前我們提到需要在實(shí)現(xiàn)類的classpath下面建立一個(gè)/META-INF/services/文件夾就是這個(gè)原因。),找到對(duì)應(yīng)的文件,讀取這個(gè)文件名找到對(duì)應(yīng)的SPI接口,然后通過InputStream流將文件內(nèi)容讀出來,獲取到實(shí)現(xiàn)類的全路徑名,并得到這個(gè)全路徑名所表示的Class對(duì)象,判斷其與服務(wù)接口是否是同一類型,然后通過反射生成服務(wù)接口的實(shí)現(xiàn),并保存在providers列表中,供給后續(xù)的使用。
SPI這種設(shè)計(jì)方式為我們的應(yīng)用擴(kuò)展提供了極大的便利,但是它的短板也是顯而易見的,Java SPI 在查找擴(kuò)展實(shí)現(xiàn)類的時(shí)候遍歷 SPI 的配置文件并且將實(shí)現(xiàn)類全部實(shí)例化,假設(shè)一個(gè)實(shí)現(xiàn)類初始化過程比較消耗資源且耗時(shí),但是你的代碼里面又用不上它,這就產(chǎn)生了資源的浪費(fèi)。所以說 Java SPI 無法按需加載實(shí)現(xiàn)類。
另外,SPI 機(jī)制在很多框架中都有應(yīng)用:slf4j日志框架、Spring 框架的基本原理也是類似的反射。還有 Dubbo 框架提供同樣的 SPI 擴(kuò)展機(jī)制,只不過 Dubbo 和 spring 框架中的 SPI 機(jī)制具體實(shí)現(xiàn)方式跟咱們今天學(xué)得這個(gè)有些細(xì)微的區(qū)別(Dubbo可以實(shí)現(xiàn)按需加載實(shí)現(xiàn)類),不過整體的原理都是一致的,我們今天先對(duì)SPI有個(gè)簡(jiǎn)單的了解,相信有了今天的基礎(chǔ)理解剩下的那幾個(gè)也不是什么難事。
好了,今天就到這兒了,文章中有說的不對(duì)的地方還請(qǐng)各位大佬批評(píng)指正,一起學(xué)習(xí),共同進(jìn)步,謝謝。
到此這篇關(guān)于淺析Java中的SPI原理的文章就介紹到這了,更多相關(guān)Java SPI原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java開發(fā)者就業(yè)需要掌握的9大專業(yè)技能
這篇文章主要為大家詳細(xì)介紹了java就業(yè)前需要掌握的專業(yè)技能,感興趣的小伙伴們可以參考一下2016-09-09idea導(dǎo)入工程時(shí)不能導(dǎo)入maven項(xiàng)目不能加入tomcatServer的原因
這篇文章主要介紹了idea導(dǎo)入工程時(shí)不能導(dǎo)入maven項(xiàng)目不能加入tomcatServer的原因及解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09Java的JSON轉(zhuǎn)換類庫GSON的基礎(chǔ)使用教程
GSON是谷歌開源的一款Java對(duì)象與JSON對(duì)象互相轉(zhuǎn)換的類庫,Java的JSON轉(zhuǎn)換類庫GSON的基礎(chǔ)使用教程,需要的朋友可以參考下2016-06-06SpringBoot實(shí)現(xiàn)短鏈接系統(tǒng)的使用示例
由于短鏈接可能涉及到用戶隱私和安全問題,所以短鏈接系統(tǒng)也需要符合相關(guān)的數(shù)據(jù)保護(hù)和安全標(biāo)準(zhǔn),本文主要介紹了SpringBoot實(shí)現(xiàn)短鏈接系統(tǒng)的使用示例,感興趣的可以了解一下2023-09-09SpringBoot+MDC實(shí)現(xiàn)鏈路調(diào)用日志的方法
MDC是 log4j 、logback及l(fā)og4j2 提供的一種方便在多線程條件下記錄日志的功能,這篇文章主要介紹了SpringBoot+MDC實(shí)現(xiàn)鏈路調(diào)用日志,需要的朋友可以參考下2022-12-12