Java SPI機制及Springboot的使用實例代碼
一、SPI是什么
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現(xiàn)或者擴展的API,它可以用來啟用框架擴展和替換組件。
整體機制圖如下:

Java SPI 實際上是“基于接口的編程+策略模式+配置文件”組合實現(xiàn)的動態(tài)加載機制。
系統(tǒng)設(shè)計的各個抽象,往往有很多不同的實現(xiàn)方案,在面向的對象的設(shè)計里,一般推薦模塊之間基于接口編程,模塊之間不對實現(xiàn)類進行硬編碼。一旦代碼里涉及具體的實現(xiàn)類,就違反了可拔插的原則,如果需要替換一種實現(xiàn),就需要修改代碼。為了實現(xiàn)在模塊裝配的時候能不在程序里動態(tài)指明,這就需要一種服務(wù)發(fā)現(xiàn)機制。
Java SPI就是提供這樣的一個機制:為某個接口尋找服務(wù)實現(xiàn)的機制。有點類似IOC的思想,就是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計中這個機制尤其重要。所以SPI的核心思想就是解耦。
二、使用場景
概括地說,適用于:調(diào)用者根據(jù)實際使用需要,啟用、擴展、或者替換框架的實現(xiàn)策略
比較常見的例子:
- 數(shù)據(jù)庫驅(qū)動加載接口實現(xiàn)類的加載:JDBC加載不同類型數(shù)據(jù)庫的驅(qū)動
- 日志門面接口實現(xiàn)類加載:SLF4J加載不同提供商的日志實現(xiàn)類
- Spring:Spring中大量使用了SPI,比如:對servlet3.0規(guī)范對ServletContainerInitializer的實現(xiàn)、自動類型轉(zhuǎn)換Type Conversion SPI(Converter SPI、Formatter SPI)等
- Dubbo:Dubbo中也大量使用SPI的方式實現(xiàn)框架的擴展, 不過它對Java提供的原生SPI做了封裝,允許用戶擴展實現(xiàn)Filter接口
三、使用介紹
要使用Java SPI,需要遵循如下約定:
- 當服務(wù)提供者提供了接口的一種具體實現(xiàn)后,在jar包的META-INF/services目錄下創(chuàng)建一個以“接口全限定名”為命名的文件,內(nèi)容為實現(xiàn)類的全限定名;
- 接口實現(xiàn)類所在的jar包放在主程序的classpath中;
- 主程序通過java.util.ServiceLoder動態(tài)裝載實現(xiàn)模塊,它通過掃描META-INF/services目錄下的配置文件找到實現(xiàn)類的全限定名,把類加載到JVM;
- SPI的實現(xiàn)類必須攜帶一個不帶參數(shù)的構(gòu)造方法;
示例代碼
- 步驟1:
定義一組接口 (假設(shè)是org.foo.demo.IShout),并寫出接口的一個或多個實現(xiàn),(假設(shè)是org.foo.demo.animal.Dog、org.foo.demo.animal.Cat)。
public interface IShout {
void shout();
}
public class Cat implements IShout {
@Override
public void shout() {
System.out.println("miao miao");
}
}
public class Dog implements IShout {
@Override
public void shout() {
System.out.println("wang wang");
}
}- 步驟2:
在 src/main/resources/ 下建立 /META-INF/services 目錄, 新增一個以接口命名的文件 (org.foo.demo.IShout文件),內(nèi)容是要應(yīng)用的實現(xiàn)類(這里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一個類)。
- src
-main
-resources
- META-INF
- services
- org.foo.demo.IShout文件內(nèi)容
org.foo.demo.animal.Dog org.foo.demo.animal.Cat
- 步驟3:
使用 ServiceLoader 來加載配置文件中指定的實現(xiàn)。
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);
for (IShout s : shouts) {
s.shout();
}
}
}
/**
* 此代碼輸出為:
* wang wang
* miao miao
*/四、Springboot實例運用
Springboot相信很多人都用過,在spring-boot和spring-boot-autoconfigure這兩個jar包的META-INF/spring.factories路徑下,保存的就是springboot使用SPI機制配置的屬性,里面有sprignboot運行時需要讀取的類,包括EnableAutoConfiguration等自動配置類,其部分關(guān)鍵配置如下:
# PropertySource Loaders org.springframework.boot.env.PropertySourceLoader=\ org.springframework.boot.env.PropertiesPropertySourceLoader,\ org.springframework.boot.env.YamlPropertySourceLoader # Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.boot.ClearCachesApplicationListener,\ org.springframework.boot.builder.ParentContextCloserApplicationListener,\ org.springframework.boot.context.FileEncodingApplicationListener,\ org.springframework.boot.context.config.AnsiOutputApplicationListener,\ org.springframework.boot.context.config.ConfigFileApplicationListener,\
在這里面配置了PropertySourceLoader和ApplicationListener等接口的具體實現(xiàn)類,然后通過SpringFactoriesLoader這個類去加載這個文件,并獲得具體的類路徑。
SpringFactoriesLoader其部分關(guān)鍵源碼如下:
public final class SpringFactoriesLoader {
// 加載器所需要加載的路徑
public static final String FACTORIES_RESOURCE_LOCATION =
"META-INF/spring.factories";
private static Map<String, List<String>> loadSpringFactories(
@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
// 根據(jù)路徑去錄取各個包下的文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
// 獲取后進行循環(huán)遍歷,因為不止一個包有spring.factories文件
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils
.loadProperties(resource);
// 獲取到了key和value對應(yīng)關(guān)系
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
// 循環(huán)獲取配置文件的value,并放進result集合中
for (String factoryName :
StringUtils
.commaDelimitedListToStringArray(
(String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
// 并緩存起來,以便后續(xù)直接獲取
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
...
}
}
}當開發(fā)者獲取到這些key-value后,便可以直接使用Class.forName()方法獲取Class對象,接著使用Class實例化便可以完成基于接口的編程+策略模式+配置文件這種搭配模式了。
總結(jié)
優(yōu)點:
- 使用Java SPI機制的優(yōu)勢是實現(xiàn)解耦,使得第三方服務(wù)模塊的裝配控制的邏輯與調(diào)用者的業(yè)務(wù)代碼分離,而不是耦合在一起。應(yīng)用程序可以根據(jù)實際業(yè)務(wù)情況啟用框架擴展或替換框架組件。
缺點:
- 雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取,也就是接口的實現(xiàn)類全部加載并實例化一遍。如果你并不想用某些實現(xiàn)類,它也被加載并實例化了,這就造成了浪費。獲取某個實現(xiàn)類的方式不夠靈活,只能通過Iterator形式獲取,不能根據(jù)某個參數(shù)來獲取對應(yīng)的實現(xiàn)類。
- 多個并發(fā)多線程使用ServiceLoader類的實例是不安全的。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
mybatis中mapper.xml文件的常用屬性及標簽講解
這篇文章主要介紹了mybatis中mapper.xml文件的常用屬性及標簽講解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
Java泛型之協(xié)變與逆變及extends與super選擇
這篇文章主要介紹了Java泛型之協(xié)變與逆變及extends與super選擇,文章圍繞主題內(nèi)容展開詳細內(nèi)容介紹,需要的小伙伴可以參考一下2022-05-05
Javaweb開發(fā)中通過Servlet生成驗證碼圖片
這篇文章主要為大家詳細介紹了Javaweb開發(fā)中通過Servlet生成驗證碼圖片的相關(guān)資料,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-05-05
SpringBoot實現(xiàn)項目健康檢查與監(jiān)控
這篇文章主要介紹了SpringBoot實現(xiàn)項目健康檢查與監(jiān)控,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2018-06-06
Admin - SpringBoot + Maven 多啟動環(huán)境配置實例詳解
這篇文章主要介紹了Admin - SpringBoot + Maven 多啟動環(huán)境配置,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03
mybatisplus的坑?insert標簽insert?into?select無參數(shù)問題的解決
這篇文章主要介紹了mybatisplus的坑?insert標簽insert?into?select無參數(shù)問題的解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12

