目錄
一、SPI是什么
1.1 SPI 和 API 有什么區別?
二、使用場景
三、使用介紹
四、Spring Boot實例運用
五、總結
一、SPI是什么
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API,它可以用來啟用框架擴展和替換組件。
整體機制圖如下:
Java SPI 實際上是“基于接口的編程+策略模式+配置文件”組合實現的動態加載機制。
系統設計的各個抽象,往往有很多不同的實現方案,在面向的對象的設計里,一般推薦模塊之間基于接口編程,模塊之間不對實現類進行硬編碼。一旦代碼里涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。為了實現在模塊裝配的時候能不在程序里動態指明,這就需要一種服務發現機制。
Java SPI就是提供這樣的一個機制:為某個接口尋找服務實現的機制。有點類似IoC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。所以SPI的核心思想就是解耦。
涉及到SPI的地方就是打破了雙親委派機制:2、線程上下文類加載器破壞雙親委派模型
1.1 SPI 和 API 有什么區別?
那 SPI 和 API 有啥區別?
說到 SPI 就不得不說一下 API 了,從廣義上來說它們都屬于接口,而且很容易混淆。下面先用一張圖說明一下:
一般模塊之間都是通過通過接口進行通訊,那我們在服務調用方和服務實現方(也稱服務提供者)之間引入一個“接口”。
當實現方提供了接口和實現,我們可以通過調用實現方的接口從而擁有實現方給我們提供的能力,這就是 API ,這種接口和實現都是放在實現方的。
當接口存在于調用方這邊時,就是 SPI ,由接口調用方確定接口規則,然后由不同的廠商去根據這個規則對這個接口進行實現,從而提供服務。
舉個通俗易懂的例子:公司 H 是一家科技公司,新設計了一款芯片,然后現在需要量產了,而市面上有好幾家芯片制造業公司,這個時候,只要 H 公司指定好了這芯片生產的標準(定義好了接口標準),那么這些合作的芯片公司(服務提供者)就按照標準交付自家特色的芯片(提供不同方案的實現,但是給出來的結果是一樣的)。
二、使用場景
概括地說,適用于:調用者根據實際使用需要,啟用、擴展、或者替換框架的實現策略。
比較常見的例子:
- 數據庫驅動加載接口實現類的加載:JDBC加載不同類型數據庫的驅動
- 日志門面接口實現類加載:SLF4J加載不同提供商的日志實現類
- Spring:Spring中大量使用了SPI,比如:對servlet3.0規范對ServletContainerInitializer的實現、自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)等
- Dubbo:Dubbo中也大量使用SPI的方式實現框架的擴展, 不過它對Java提供的原生SPI做了封裝,允許用戶擴展實現Filter接口
三、使用介紹
要使用Java SPI,需要遵循如下約定:
- 當服務提供者提供了接口的一種具體實現后,在jar包的META-INF/services目錄下創建一個以“接口全限定名”為命名的文件,內容為實現類的全限定名;
- 接口實現類所在的jar包放在主程序的classpath中;
- 主程序通過java.util.ServiceLoder動態裝載實現模塊,它通過掃描META-INF/services目錄下的配置文件找到實現類的全限定名,把類加載到JVM;
- SPI的實現類必須攜帶一個不帶參數的構造方法;
示例代碼:
步驟1
定義一組接口 (假設是org.foo.demo.IShout),并寫出接口的一個或多個實現,(假設是org.foo.demo.animal.Dog、org.foo.demo.animal.Cat)。
public interface IShout {void shout();
}public class Cat implements IShout {@Overridepublic void shout() {System.out.println("miao miao");}
}public class Dog implements IShout {@Overridepublic void shout() {System.out.println("wang wang");}
}
步驟2
在 src/main/resources/ 下建立 /META-INF/services 目錄, 新增一個以接口命名的文件 (org.foo.demo.IShout文件),內容是要應用的實現類(這里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一個類)。
- src
? ? -main
? ? ? ? -resources
? ? ? ? ? ? - META-INF
? ? ? ? ? ? ? ? - services
? ? ? ? ? ? ? ? ? ? - org.foo.demo.IShout
文件內容
org.foo.demo.animal.Dog
org.foo.demo.animal.Cat
步驟3
使用 ServiceLoader 來加載配置文件中指定的實現。
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
*/
四、Spring Boot實例運用
Spring Boot相信很多人都用過,在spring-boot和spring-boot-autoconfigure這兩個jar包的META-INF/spring.factories路徑下,保存的就是Spring Boot使用SPI機制配置的屬性,里面有Spring Boot運行時需要讀取的類,包括EnableAutoConfiguration等自動配置類,其部分關鍵配置如下:
# 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等接口的具體實現類,然后通過SpringFactoriesLoader這個類去加載這個文件,并獲得具體的類路徑。
SpringFactoriesLoader其部分關鍵源碼如下:
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 {// 根據路徑去錄取各個包下的文件Enumeration<URL> urls = (classLoader != null ?classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));result = new LinkedMultiValueMap<>();// 獲取后進行循環遍歷,因為不止一個包有spring.factories文件while (urls.hasMoreElements()) {URL url = urls.nextElement();UrlResource resource = new UrlResource(url);Properties properties = PropertiesLoaderUtils.loadProperties(resource);// 獲取到了key和value對應關系for (Map.Entry<?, ?> entry : properties.entrySet()) {String factoryClassName = ((String) entry.getKey()).trim();// 循環獲取配置文件的value,并放進result集合中for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {result.add(factoryClassName, factoryName.trim());}}}// 并緩存起來,以便后續直接獲取cache.put(classLoader, result);return result;}catch (IOException ex) {...}}
}
當開發者獲取到這些key-value后,便可以直接使用Class.forName()方法獲取Class對象,接著使用Class實例化便可以完成基于接口的編程+策略模式+配置文件這種搭配模式了。
Spring Boot依賴的很多包中都有spring.factories,用于告知Spring Boot需要自動配置的實現類有哪些:
五、總結
優點:
- 使用Java SPI機制的優勢是實現解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一起。應用程序可以根據實際業務情況啟用框架擴展或替換框架組件。
缺點:
- 雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取,也就是接口的實現類全部加載并實例化一遍。如果你并不想用某些實現類,它也被加載并實例化了,這就造成了浪費(Spring Boot中進行了優化,會將要自動配置的類先去重,再過濾,只把真正要用的類進行自動配置)。獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個參數來獲取對應的實現類。
- 多個并發多線程使用ServiceLoader類的實例是不安全的。
相關文章:【JVM筆記】如何打破雙親委派機制?-CSDN博客