大家好,我是不才陳某~
SPI(Service Provider Interface)是JDK內建的一種服務提供發現機制,可以用來啟用框架擴充套件和替換元件,主要用於框架中開發,例如Dubbo、Spring、Common-Logging,JDBC等採用採用SPI機制,針對同一介面採用不同的實現提供給不同的使用者,從而提高了框架的擴充套件性。
之前也寫過Java SPI的深入剖析:聊聊 Java SPI 機制
推薦Java工程師技術指南:https://github.com/chenjiabin...
Java SPI實現
Java內建的SPI透過java.util.ServiceLoader類解析classPath和jar包的META-INF/services/目錄 下的以介面全限定名命名的檔案,並載入該檔案中指定的介面實現類,以此完成呼叫。
示例說明
建立動態介面
public interface VedioSPI
{
void call();
}
實現類1
public class Mp3Vedio implements VedioSPI
{
@Override
public void call()
{
System.out.println("this is mp3 call");
}
}
實現類2
public class Mp4Vedio implements VedioSPI
{
@Override
public void call()
{
System.out.println("this is mp4 call");
}
}
在專案的source目錄下新建META-INF/services/目錄下,建立com.skywares.fw.juc.spi.VedioSPI檔案。
相關測試
public class VedioSPITest
{
public static void main(String[] args)
{
ServiceLoader<VedioSPI> serviceLoader =ServiceLoader.load(VedioSPI.class);
serviceLoader.forEach(t->{
t.call();
});
}
}
說明:Java實現spi是透過ServiceLoader來查詢服務提供的工具類。
執行結果:
原始碼分析
上述只是透過簡單的示例來實現下java的內建的SPI功能。其實現原理是ServiceLoader是Java內建的用於查詢服務提供介面的工具類,透過呼叫load()方法實現對服務提供介面的查詢,最後遍歷來逐個訪問服務提供介面的實現類。
從原始碼可以發現:
- ServiceLoader類本身實現了Iterable介面並實現了其中的iterator方法,iterator方法的實現中呼叫了LazyIterator這個內部類中的方法,迭代器建立例項。
- 所有服務提供介面的對應檔案都是放置在META-INF/services/目錄下,final型別決定了PREFIX目錄不可變更。
雖然java提供的SPI機制的思想非常好,但是也存在相應的弊端。具體如下:
- Java內建的方法方式只能透過遍歷來獲取
- 服務提供介面必須放到META-INF/services/目錄下。
針對java的spi存在的問題,Spring的SPI機制沿用的SPI的思想,但對其進行擴充套件和最佳化。
推薦Java工程師技術指南:https://github.com/chenjiabin...
Spring SPI
Spring SPI沿用了Java SPI的設計思想,Spring採用的是spring.factories方式實現SPI機制,可以在不修改Spring原始碼的前提下,提供Spring框架的擴充套件性。
Spring 示例
定義介面
public interface DataBaseSPI
{
void getConnection();
}
相關實現
##DB2實現
public class DB2DataBase implements DataBaseSPI
{
@Override
public void getConnection()
{
System.out.println("this database is db2");
}
}
##Mysql實現
public class MysqlDataBase implements DataBaseSPI
{
@Override
public void getConnection()
{
System.out.println("this is mysql database");
}
}
1、在專案的META-INF目錄下,新增spring.factories檔案
2、填寫相關的介面資訊,內容如下:
com.skywares.fw.juc.springspi.DataBaseSPI = com.skywares.fw.juc.springspi.DB2DataBase, com.skywares.fw.juc.springspi.MysqlDataBase
說明多個實現採用逗號分隔。
相關測試類
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();
}
}
}
輸出結果
從示例中我們看出,Spring 採用spring.factories實現SPI與java實現SPI非常相似,但是spring的spi方式針對java的spi進行的相關最佳化具體內容如下:
- Java SPI是一個服務提供介面對應一個配置檔案,配置檔案中存放當前介面的所有實現類,多個服務提供介面對應多個配置檔案,所有配置都在services目錄下;
- Spring factories SPI是一個spring.factories配置檔案存放多個介面及對應的實現類,以介面全限定名作為key,實現類作為value來配置,多個實現類用逗號隔開,僅spring.factories一個配置檔案。
那麼spring是如何透過載入spring.factories來實現SpI的呢?我們可以透過原始碼來進一步分析。
推薦Java工程師技術指南:https://github.com/chenjiabin...
原始碼分析
說明:loadFactoryNames解析spring.factories檔案中指定介面的實現類的全限定名,具體實現如下:
說明: 獲取所有jar包中META-INF/spring.factories檔案路徑,以列舉值返回。 遍歷spring.factories檔案路徑,逐個載入解析,整合factoryClass型別的實現類名稱,獲取到實現類的全類名稱後進行類的例項話操作,其相關原始碼如下:
說明:例項化是透過反射來實現對應的初始化。