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)解析classPath和jar包的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)原理是ServiceLoader是Java內(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ì)java的spi存在的問(wèn)題,Spring的SPI機(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)SPI與java實(shí)現(xiàn)SPI非常相似,但是spring的spi方式針對(duì)java的spi進(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)。
Dubbo的 SPI(Service 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();
}
}