JAVA,Spring,Dubbo的SPI機(jī)制講解

1 SPI機(jī)制講解

1.1 引言

SPI(Service Provider Interface)JDK內(nèi)置的一種服務(wù)提供發(fā)現(xiàn)機(jī)制,可以用來(lái)啟用框架擴(kuò)展和替換組件,主要用于框架中開(kāi)發(fā),例如Dubbo、Spring、Common-Logging,JDBC等采用采用SPI機(jī)制,針對(duì)同一接口采用不同的實(shí)現(xiàn)提供給不同的用戶(hù),從而提高了框架的擴(kuò)展性。

1.2 Java SPI實(shí)現(xiàn)

Java內(nèi)置的SPI通過(guò)java.util.ServiceLoader類(lèi)解析classPathjar包的META-INF/services/目錄下的以接口全限定名命名的文件,并加載該文件中指定的接口實(shí)現(xiàn)類(lèi),以此完成調(diào)用。

1.2.1 示例說(shuō)明

創(chuàng)建動(dòng)態(tài)接口

public interface VedioSPI
{
    void call();
}

實(shí)現(xiàn)類(lèi)1

public class Mp3Vedio implements VedioSPI
{
    @Override
    public void call()
    {
        System.out.println("this is mp3 call");
    }

}

實(shí)現(xiàn)類(lèi)2

public class Mp4Vedio implements VedioSPI
{
    @Override
    public void call()
    {
       System.out.println("this is mp4 call");
    }

}

在項(xiàng)目的source目錄下新建META-INF/services/目錄下,創(chuàng)建com.skywares.fw.juc.spi.VedioSPI文件,并在文件中寫(xiě)入實(shí)現(xiàn)類(lèi)全限定類(lèi)名

com.skywares.fw.juc.spi.Mp4Vedio
com.skywares.fw.juc.spi.Mp3Vedio

1.2.2 相關(guān)測(cè)試

public class VedioSPITest{
    public static void main(String[] args)
    {
        ServiceLoader<VedioSPI> serviceLoader =ServiceLoader.load(VedioSPI.class);
        
        serviceLoader.forEach(t->{
            t.call();
        });
    }
}

說(shuō)明:Java實(shí)現(xiàn)spi是通過(guò)ServiceLoader來(lái)查找服務(wù)提供的工具類(lèi)。

運(yùn)行結(jié)果:

this is mp4 call
this is mp3 call

1.2.3 源碼分析

上述只是通過(guò)簡(jiǎn)單的示例來(lái)實(shí)現(xiàn)下java的內(nèi)置的SPI功能。其實(shí)現(xiàn)原理是ServiceLoaderJava內(nèi)置的用于查找服務(wù)提供接口的工具類(lèi),通過(guò)調(diào)用load()方法實(shí)現(xiàn)對(duì)服務(wù)提供接口的查找,最后遍歷來(lái)逐個(gè)訪(fǎng)問(wèn)服務(wù)提供接口的實(shí)現(xiàn)類(lèi)。

public final class ServiceLoader<S>
    implements Iterable<S>
{
    //服務(wù)提供接口對(duì)應(yīng)文件放置目錄
    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    private final Class<S> service;

    // 類(lèi)加載器
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    private final AccessControlContext acc;

    // 按照初始化順序緩存服務(wù)提供接口實(shí)例
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 內(nèi)部類(lèi)實(shí)現(xiàn)了iterator接口
    private LazyIterator lookupIterator;
}

從源碼可以發(fā)現(xiàn):

  • ServiceLoader類(lèi)本身實(shí)現(xiàn)了Iterable接口并實(shí)現(xiàn)了其中的iterator方法,iterator方法的實(shí)現(xiàn)中調(diào)用了LazyIterator這個(gè)內(nèi)部類(lèi)中的方法,迭代器創(chuàng)建實(shí)例。
  • 所有服務(wù)提供接口的對(duì)應(yīng)文件都是放置在META-INF/services/目錄下,final類(lèi)型決定了PREFIX目錄不可變更。

雖然java提供的SPI機(jī)制的思想非常好,但是也存在相應(yīng)的弊端。具體如下:

  • Java內(nèi)置的方法方式只能通過(guò)遍歷來(lái)獲取
  • 服務(wù)提供接口必須放到META-INF/services/目錄下。

針對(duì)javaspi存在的問(wèn)題,SpringSPI機(jī)制沿用的SPI的思想,但對(duì)其進(jìn)行擴(kuò)展和優(yōu)化

1.3 Spring SPI

Spring SPI沿用了Java SPI的設(shè)計(jì)思想,Spring采用的是spring.factories方式實(shí)現(xiàn)SPI機(jī)制,可以在不修改Spring源碼的前提下,提供Spring框架的擴(kuò)展性。

1.3.1 Spring 示例

定義接口

public interface DataBaseSPI
{
   void getConnection();
}

相關(guān)實(shí)現(xiàn)

##DB2實(shí)現(xiàn)
public class DB2DataBase implements DataBaseSPI
{
    @Override
    public void getConnection()
    {
        System.out.println("this database is db2");
    }

}

##Mysql實(shí)現(xiàn)
public class MysqlDataBase implements DataBaseSPI
{
    @Override
    public void getConnection()
    {
       System.out.println("this is mysql database");
    }

}

1、在項(xiàng)目的META-INF目錄下,新增spring.factories文件

在這里插入圖片描述

2、填寫(xiě)相關(guān)的接口信息,內(nèi)容如下:

com.skywares.fw.juc.springspi.DataBaseSPI = com.skywares.fw.juc.springspi.DB2DataBase, com.skywares.fw.juc.springspi.MysqlDataBase

說(shuō)明多個(gè)實(shí)現(xiàn)采用逗號(hào)分隔

1.3.2 相關(guān)測(cè)試類(lèi)

public class SpringSPITest
{
    public static void main(String[] args)
    {
         List<DataBaseSPI> dataBaseSPIs =SpringFactoriesLoader.loadFactories(DataBaseSPI.class, 
                 Thread.currentThread().getContextClassLoader());
         
         for(DataBaseSPI datBaseSPI:dataBaseSPIs){
            datBaseSPI.getConnection();
         }
    }
}

輸出結(jié)果

this database is db2
this is mysql database

從示例中我們看出,Spring 采用spring.factories實(shí)現(xiàn)SPIjava實(shí)現(xiàn)SPI非常相似,但是springspi方式針對(duì)javaspi進(jìn)行的相關(guān)優(yōu)化具體內(nèi)容如下:

  • Java SPI是一個(gè)服務(wù)提供接口對(duì)應(yīng)一個(gè)配置文件,配置文件中存放當(dāng)前接口的所有實(shí)現(xiàn)類(lèi),多個(gè)服務(wù)提供接口對(duì)應(yīng)多個(gè)配置文件,所有配置都在services目錄下;
  • Spring factories SPI是一個(gè)spring.factories配置文件存放多個(gè)接口及對(duì)應(yīng)的實(shí)現(xiàn)類(lèi),以接口全限定名作為key,實(shí)現(xiàn)類(lèi)作為value來(lái)配置,多個(gè)實(shí)現(xiàn)類(lèi)用逗號(hào)隔開(kāi),僅spring.factories一個(gè)配置文件。

那么spring是如何通過(guò)加載spring.factories來(lái)實(shí)現(xiàn)SpI的呢?我們可以通過(guò)源碼來(lái)進(jìn)一步分析。

1.3.3 源碼分析

在這里插入圖片描述

說(shuō)明:loadFactoryNames解析spring.factories文件中指定接口的實(shí)現(xiàn)類(lèi)的全限定名,具體實(shí)現(xiàn)如下:

在這里插入圖片描述

說(shuō)明:獲取所有jar包中META-INF/spring.factories文件路徑,以枚舉值返回。遍歷spring.factories文件路徑,逐個(gè)加載解析,整合factoryClass類(lèi)型的實(shí)現(xiàn)類(lèi)名稱(chēng),獲取到實(shí)現(xiàn)類(lèi)的全類(lèi)名稱(chēng)后進(jìn)行類(lèi)的實(shí)例話(huà)操作,其相關(guān)源碼如下:

在這里插入圖片描述

說(shuō)明:實(shí)例化是通過(guò)反射來(lái)實(shí)現(xiàn)對(duì)應(yīng)的初始化。

1.3.4 與@Component相比

Spring Boot 中,SPI(Service Provider Interface)@Component 注冊(cè)是兩種不同的組件注冊(cè)方式,它們有不同的使用場(chǎng)景和優(yōu)先級(jí)。

  • SPI 注冊(cè):
    • SPI 是一種 Java 標(biāo)準(zhǔn)的服務(wù)提供者注冊(cè)機(jī)制,通過(guò)在 META-INF/services 目錄下提供對(duì)應(yīng)接口的實(shí)現(xiàn)類(lèi),實(shí)現(xiàn)了自動(dòng)發(fā)現(xiàn)和加載。
    • SPI 注冊(cè)通常用于定義接口,然后允許多個(gè)實(shí)現(xiàn)方注冊(cè),Spring Boot 通過(guò) spring.factories 文件實(shí)現(xiàn)了對(duì) SPI 的支持。
    • SPI 注冊(cè)的優(yōu)先級(jí)較低,多個(gè)實(shí)現(xiàn)類(lèi)會(huì)按照加載的先后順序進(jìn)行注冊(cè)。
  • @Component 注冊(cè):
    • @Component 注解是 Spring 框架提供的注解,用于將 Java 類(lèi)標(biāo)記為 Spring 組件(Bean)。
    • 通過(guò) @ComponentScan 掃描指定的包,或者使用 @Component、@Service、@Repository、@Controller 等注解標(biāo)記類(lèi),將它們注冊(cè)為 Spring 管理的組件。
    • @Component 注冊(cè)的優(yōu)先級(jí)較高,Spring Boot 在啟動(dòng)時(shí)會(huì)主動(dòng)掃描并注冊(cè)這些組件。 而 @Service、@Controller 等注解本質(zhì)上都是 @Component 的衍生注解,它們具有相同的優(yōu)先級(jí)

1.4 Dubbo SPI

1.4.1 簡(jiǎn)介

Dubbo 并未使用 Java SPI,而是重新實(shí)現(xiàn)了一套功能更強(qiáng)的 SPI 機(jī)制。Dubbo SPI 的相關(guān)邏輯被封裝在了 ExtensionLoader 類(lèi)中,通過(guò) ExtensionLoader,我們可以加載指定的實(shí)現(xiàn)類(lèi)。

DubboSPIService Provider Interface)機(jī)制是一種服務(wù)發(fā)現(xiàn)和加載的機(jī)制,它允許第三方為Dubbo提供擴(kuò)展實(shí)現(xiàn)。Dubbo SPI機(jī)制的核心思想是面向接口編程,將接口和實(shí)現(xiàn)分離,使得在不修改原有代碼的情況下,可以靈活地?cái)U(kuò)展和替換功能。這種機(jī)制在很大程度上提高了Dubbo框架的可擴(kuò)展性和靈活性。

Dubbo SPI機(jī)制的實(shí)現(xiàn)主要包括以下幾個(gè)步驟:

  • 定義接口:首先需要定義一個(gè)接口,這個(gè)接口將作為擴(kuò)展點(diǎn),供第三方實(shí)現(xiàn)。例如,定義一個(gè)Filter接口,用于實(shí)現(xiàn)不同的過(guò)濾器功能。
  • 實(shí)現(xiàn)接口:第三方可以根據(jù)自己的需求,實(shí)現(xiàn)接口中的方法。例如,實(shí)現(xiàn)一個(gè) AuthFilter 類(lèi),用于實(shí)現(xiàn)權(quán)限驗(yàn)證功能。
  • 配置文件:在 META-INF/dubbo 目錄下創(chuàng)建一個(gè)與接口同名的文件,文件內(nèi)容為key值=接口實(shí)現(xiàn)類(lèi)的全限定名。
    Dubbo是通過(guò)鍵值對(duì)的方式進(jìn)行配置,內(nèi)容和property配置文件類(lèi)似,也是key=value格式,我們可以直接通過(guò)Key獲取我們想要加載的實(shí)體類(lèi)
    例如,創(chuàng)建一個(gè)名為com.example.Filter的文件,文件內(nèi)容為auth=com.example.AuthFilter。
  • 加載擴(kuò)展Dubbo 在運(yùn)行時(shí)會(huì)自動(dòng)加載配置文件中指定的實(shí)現(xiàn)類(lèi),并根據(jù)需要?jiǎng)?chuàng)建實(shí)例。用戶(hù)可以通過(guò)ExtensionLoader類(lèi)來(lái)獲取擴(kuò)展實(shí)例,例如ExtensionLoader.getExtensionLoader(Filter.class).getExtension("auth")
  • 使用擴(kuò)展:在Dubbo框架中,可以通過(guò)@SPI注解來(lái)聲明一個(gè)接口為擴(kuò)展點(diǎn),并指定默認(rèn)的實(shí)現(xiàn)。在需要使用擴(kuò)展的地方,可以通過(guò)ExtensionLoader來(lái)獲取擴(kuò)展實(shí)例,并調(diào)用其方法。

1.4.2 示例說(shuō)明

package test.spi;

public interface Animal {
    /**
     * 動(dòng)物叫
     */
    void call();
}

然后我們分別寫(xiě)兩個(gè)不同的實(shí)現(xiàn)類(lèi),實(shí)現(xiàn)這個(gè)接口

package test.spi.impl;

import test.spi.Animal;

public class Cat implements Animal {
    @Override
    public void call() {
        System.out.println("貓叫:喵喵喵~");
    }
}
package test.spi.impl;

import test.spi.Animal;
public class Dog implements Animal {
    @Override
    public void call() {
        System.out.println("狗叫:汪汪汪~(yú)");
    }
}

./resources/META-INF/dubbo 創(chuàng)建配置文件 test.spi.Animal,并在文件中寫(xiě)入鍵值對(duì)和實(shí)現(xiàn)類(lèi)全限定類(lèi)名

cat=test.spi.impl.Cat
dog=test.spi.impl.Dog
public class Main {
    public static void main(String[] args) {
        Animal cat = ExtensionLoader.getExtensionLoader(Animal.class)
                .getExtension("cat");
        Animal dog = ExtensionLoader.getExtensionLoader(Animal.class)
                .getExtension("dog");
        cat.call();
        dog.call();

    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容