欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

淺析Java中的SPI原理

 更新時(shí)間:2022年09月20日 08:36:19   作者:一只烤鴨朝北走  
SPI:由調(diào)用方制定接口標(biāo)準(zhǔn),實(shí)現(xiàn)方來針對(duì)接口提供不同的實(shí)現(xiàn),SPI其實(shí)就是"為接口查找實(shí)現(xiàn)"的一種服務(wù)發(fā)現(xiàn)機(jī)制。本文將淺談一下SPI機(jī)制的原理,需要的可以參考一下

在面向?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反射框架Reflections示例詳解

    Java反射框架Reflections示例詳解

    這篇文章主要介紹了Java反射框架Reflections示例詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07
  • Java開發(fā)者就業(yè)需要掌握的9大專業(yè)技能

    Java開發(fā)者就業(yè)需要掌握的9大專業(yè)技能

    這篇文章主要為大家詳細(xì)介紹了java就業(yè)前需要掌握的專業(yè)技能,感興趣的小伙伴們可以參考一下
    2016-09-09
  • Java初學(xué)之繼承與多態(tài)

    Java初學(xué)之繼承與多態(tài)

    這篇文章主要介紹了Java 繼承與多態(tài)的深入理解的相關(guān)資料,子類繼承父類的特征和行為,使得子類具有父類的各種屬性和方法。或子類從父類繼承方法,使得子類具有父類相同的行為,需要的朋友可以參考下
    2021-07-07
  • Java線程池框架核心代碼解析

    Java線程池框架核心代碼解析

    這篇文章主要針對(duì)Java線程池框架核心代碼進(jìn)行詳細(xì)解析,分析Java線程池框架的實(shí)現(xiàn)ThreadPoolExecutor,感興趣的小伙伴們可以參考一下
    2016-07-07
  • idea導(dǎo)入工程時(shí)不能導(dǎo)入maven項(xiàng)目不能加入tomcatServer的原因

    idea導(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-09
  • Java單例模式的講解

    Java單例模式的講解

    今天小編就為大家分享一篇關(guān)于Java單例模式的講解,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧
    2019-01-01
  • SpringBoot整合Web之AOP配置詳解

    SpringBoot整合Web之AOP配置詳解

    面向切面編程(aspect-oriented programming,AOP)主要實(shí)現(xiàn)的目的是針對(duì)業(yè)務(wù)處理過程中的切面進(jìn)行提取,諸如日志、事務(wù)管理和安全這樣的系統(tǒng)服務(wù),從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率
    2022-08-08
  • Java的JSON轉(zhuǎn)換類庫GSON的基礎(chǔ)使用教程

    Java的JSON轉(zhuǎn)換類庫GSON的基礎(chǔ)使用教程

    GSON是谷歌開源的一款Java對(duì)象與JSON對(duì)象互相轉(zhuǎn)換的類庫,Java的JSON轉(zhuǎn)換類庫GSON的基礎(chǔ)使用教程,需要的朋友可以參考下
    2016-06-06
  • SpringBoot實(shí)現(xiàn)短鏈接系統(tǒng)的使用示例

    SpringBoot實(shí)現(xiàn)短鏈接系統(tǒng)的使用示例

    由于短鏈接可能涉及到用戶隱私和安全問題,所以短鏈接系統(tǒng)也需要符合相關(guān)的數(shù)據(jù)保護(hù)和安全標(biāo)準(zhǔn),本文主要介紹了SpringBoot實(shí)現(xiàn)短鏈接系統(tǒng)的使用示例,感興趣的可以了解一下
    2023-09-09
  • SpringBoot+MDC實(shí)現(xiàn)鏈路調(diào)用日志的方法

    SpringBoot+MDC實(shí)現(xiàn)鏈路調(diào)用日志的方法

    MDC是 log4j 、logback及l(fā)og4j2 提供的一種方便在多線程條件下記錄日志的功能,這篇文章主要介紹了SpringBoot+MDC實(shí)現(xiàn)鏈路調(diào)用日志,需要的朋友可以參考下
    2022-12-12

最新評(píng)論