2.4 SPI機制(Service Provider Interface)
- 問題:依賴倒轉(zhuǎn)原則提到,應(yīng)該依賴接口而不是實現(xiàn)類烁落,但接口最終要有實現(xiàn)類落地令杈。如果因為業(yè)務(wù)調(diào)整需要替換某個接口的實現(xiàn)類,就不得不改動實現(xiàn)類,也就是修改源碼摔蓝。
- 解決:SPI機制解決了這個問題扶平。通過一種“服務(wù)尋找”的機制,動態(tài)地加載接口/抽象類對應(yīng)的具體實現(xiàn)類独榴。把接口的具體實現(xiàn)類的定義和聲明交給了外部化的配置文件僧叉。
-
如下圖,一個接口可以有多個實現(xiàn)類棺榔,通過SPI機制瓶堕,可以將一個接口需要創(chuàng)建的實現(xiàn)類的對象都羅列到一個特殊的文件中,SPI機制會依次將這些實現(xiàn)類的對象進(jìn)行創(chuàng)建并返回症歇。
2.4.1 JDK原生SPI
簡單了解即可郎笆,使用范圍有限,只能通過接口或抽象類來加載具體的實現(xiàn)類忘晤。
1.定義接口+實現(xiàn)類
模擬一套Dao接口的不同數(shù)據(jù)庫訪問支持
public interface DemoDao {
}
public class DemoMysqlDao implements DemoDao {
}
public class DemoOracleDao implements DemoDao {
}
2.聲明SPI文件
JDK的SPI需要遵循以下規(guī)范:
- 所有定義的SPI文件都必須放在項目的META-INF/services目錄下
- 文件名必須命名為接口或抽象類的全限定名
- 文件內(nèi)容為接口或抽象類的具體實現(xiàn)類的全限定名宛蚓;如果有多個,則每行聲明一個具體實現(xiàn)類的全限定名德频,多個類之間沒有分隔符
具體步驟:
(1)在resources目錄下創(chuàng)建新目錄META-INF/services
(2)新建文件:com.star.springboot.spi.DemoDao
(3)輸入文件內(nèi)容:
com.star.springboot.spi.DemoMysqlDao
com.star.springboot.spi.DemoOracleDao
3.測試
public class JdkSpiApplication {
public static void main(String[] args) {
ServiceLoader<DemoDao> serviceLoader = ServiceLoader.load(DemoDao.class);
serviceLoader.iterator().forEachRemaining(dao -> {
System.out.println(dao);
});
}
}
輸出結(jié)果:
com.star.springboot.spi.DemoMysqlDao@65b3120a
com.star.springboot.spi.DemoOracleDao@6f539caf
控制臺成功打印出DemoDao的兩個實現(xiàn)類對象苍息,這說明JDK原生的SPI機制已成功使用。
2.4.2 SpringFramework 3.2 的SPI
SpringFramework中的SPI比JDK原生的SPI更高級實用壹置,因為它不僅限于接口或抽象類竞思,而可以是任何一個類、接口或注解钞护。
SpringBoot中大量用到SPI機制加載自動配置類和特殊組件等(如@EnableAutoConfiguration)盖喷。
1.聲明SPI文件
SpringFramework的SPI需要遵循以下規(guī)范:
- SPI文件必須放在項目的META-INF目錄下。
- 文件名必須命名為spring.factories(實際上是一個properties文件)难咕。
- 文件內(nèi)容:被檢索的類/接口/注解的全限定名作為properties的key课梳,具體要檢索的類的全限定名作為value芒帕,多個類之間用英文逗號隔開公壤。
具體步驟:
(1)在resources/META-INF目錄下新建文件:spring.factories
(2)輸入文件內(nèi)容:
com.star.springboot.spi.DemoDao=com.star.springboot.spi.DemoMysqlDaoImpl,com.star.springboot.spi.DemoOracleDaoImpl
2.測試
public class SpringSpiApplication {
public static void main(String[] args) {
List<DemoDao> demoDaos = SpringFactoriesLoader.loadFactories(DemoDao.class, SpringSpiApplication.class.getClassLoader());
demoDaos.forEach(dao -> {
System.out.println(dao);
});
System.out.println("----------");
List<String> daoClassNames = SpringFactoriesLoader.loadFactoryNames(DemoDao.class, SpringSpiApplication.class.getClassLoader());
daoClassNames.forEach(className -> {
System.out.println(className);
});
}
}
輸出結(jié)果:
com.star.springboot.spi.DemoMysqlDaoImpl@52d455b8
com.star.springboot.spi.DemoOracleDaoImpl@4f4a7090
----------
com.star.springboot.spi.DemoMysqlDaoImpl
com.star.springboot.spi.DemoOracleDaoImpl
控制臺成功打印出DemoDao的兩個實現(xiàn)類對象及其全限定名玉雾,這說明SpringFramework的SPI機制已成功使用帆疟。
延伸:
SpringFactoriesLoader不僅可以加載聲明的類的對象(loadFactories),還可以直接把預(yù)定義好的全限定名提取出來(loadFactoryNames)椭懊。
3.Spring SPI機制的實現(xiàn)原理
SPI的核心使用方法是SpringFactoriesLoader.loadFactoryNames诸蚕,通過這個方法可以獲得指定全限定名對應(yīng)配置的所有類的全限定名。
// 規(guī)定SPI文件名稱及位置
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// 存儲SPI機制加載的類及其映射
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
// 利用緩存機制提高加載速度
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
// 解析之前先檢查緩存氧猬,有則直接返回
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
// 真正的加載動作背犯,利用類加載器加載所有的spring.factories(多個,包括我們自定義框架本身自帶的)盅抚,并逐個配置解析
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
// 提取出每個spring.factories文件
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 以properties的方式讀取
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
// 逐個收集key和value
String factoryTypeName = ((String) entry.getKey()).trim();
// 如果一個key配置了多個value漠魏,使用英文逗號分割
for (StrinfactoryImplementationName:StringUtils.commaDelimitedListToStringArray((Strinentry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
// 存入緩存中
cache.put(classLoader, result);
return result;
} catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location ["+
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
邏輯梳理:SpringFactoriesLoader中有一塊緩存區(qū),這塊緩存區(qū)會在SPI機制第一次被利用時妄均,將項目類路徑下所有的spring.factories文件都加載并解析柱锹,然后存入緩存區(qū)。解析的具體邏輯丛晦,是將每一個spring.factories文件都當(dāng)作properties文件解析奕纫,提取每一對映射關(guān)系,保存到Map中烫沙,最終存入全局緩存。
通過Debug隙笆,可以看到SPI機制不僅讀取自定義的spring.factories锌蓄,還讀取了框架自帶的:
最終保存到Map的映射關(guān)系非常多,但返回給main只有自己定義的:
SpringBoot源碼解讀與原理分析(合集)