深入淺析SPI機(jī)制在JDK與Spring?Boot中的應(yīng)用
1. SPI解讀:什么是SPI?
SPI ( Service Provider Interface ) 是一種服務(wù)發(fā)現(xiàn)機(jī)制,它允許第三方提供者為核心庫(kù)或主框架提供實(shí)現(xiàn)或擴(kuò)展。這種設(shè)計(jì)允許核心庫(kù)/框架在不修改自身代碼的情況下,通過(guò)第三方實(shí)現(xiàn)來(lái)增強(qiáng)功能。
1.JDK原生的SPI:
- 定義和發(fā)現(xiàn):
JDK的SPI主要通過(guò)在META-INF/services/目錄下放置特定的文件來(lái)指定哪些類實(shí)現(xiàn)了給定的服務(wù)接口。這些文件的名稱應(yīng)為接口的全限定名,內(nèi)容為實(shí)現(xiàn)該接口的全限定類名。 - 加載機(jī)制:
ServiceLoader類使用Java的類加載器機(jī)制從META-INF/services/目錄下加載和實(shí)例化服務(wù)提供者。例如,ServiceLoader.load(MyServiceInterface.class)會(huì)返回一個(gè)實(shí)現(xiàn)了MyServiceInterface的實(shí)例迭代器。 - 缺點(diǎn):
JDK原生的SPI每次通過(guò)ServiceLoader加載時(shí)都會(huì)初始化一個(gè)新的實(shí)例,沒(méi)有實(shí)現(xiàn)類的緩存,也沒(méi)有考慮單例等高級(jí)功能。
2.Spring的SPI:
- 更加靈活:
Spring的SPI不僅僅是服務(wù)發(fā)現(xiàn),它提供了一套完整的插件機(jī)制。例如,可以為Spring定義新的PropertySource,ApplicationContextInitializer等。 - 與IoC集成:與
JDK的SPI不同,Spring的SPI與其IoC(Inversion of Control) 容器集成,使得在SPI實(shí)現(xiàn)中可以利用Spring的全部功能,如依賴注入。 - 條件匹配:
Spring提供了基于條件的匹配機(jī)制,這允許在某些條件下只加載特定的SPI實(shí)現(xiàn),例如,可以基于當(dāng)前運(yùn)行環(huán)境的不同來(lái)選擇加載哪個(gè)數(shù)據(jù)庫(kù)驅(qū)動(dòng)。 - 配置:
Spring允許通過(guò)spring.factories文件在META-INF目錄下進(jìn)行配置,這與JDK的SPI很相似,但它提供了更多的功能和靈活性。
舉個(gè)類比的例子:
想象我們正在建造一個(gè)電視機(jī), SPI 就像電視機(jī)上的一個(gè) USB 插口。這個(gè)插口可以插入各種設(shè)備(例如U盤(pán)、游戲手柄、電視棒等),但我們并不關(guān)心這些設(shè)備的內(nèi)部工作方式。這樣只需要提供一個(gè)標(biāo)準(zhǔn)的接口,其他公司(例如U盤(pán)制造商)可以為此接口提供實(shí)現(xiàn)。這樣,電視機(jī)可以在不更改自己內(nèi)部代碼的情況下使用各種新設(shè)備,而設(shè)備制造商也可以為各種電視機(jī)制造兼容的設(shè)備。
總之, SPI 是一種將接口定義與實(shí)現(xiàn)分離的設(shè)計(jì)模式,它鼓勵(lì)第三方為一個(gè)核心產(chǎn)品或框架提供插件或?qū)崿F(xiàn),從而使核心產(chǎn)品能夠輕松地?cái)U(kuò)展功能。
2. SPI在JDK中的應(yīng)用示例
在 Java 的生態(tài)系統(tǒng)中, SPI 是一個(gè)核心概念,允許開(kāi)發(fā)者提供擴(kuò)展和替代的實(shí)現(xiàn),而核心庫(kù)或應(yīng)用不必更改,下面舉出一個(gè)例子來(lái)說(shuō)明。
全部代碼和步驟如下:
步驟1:定義一個(gè)服務(wù)接口,文件名: MessageService.java
package com.example.demo.service;
public interface MessageService {
String getMessage();
}步驟2:為服務(wù)接口提供實(shí)現(xiàn),這里會(huì)提供兩個(gè)簡(jiǎn)單的實(shí)現(xiàn)類。
HelloMessageService.java
package com.example.demo.service;
public class HelloMessageService implements MessageService {
@Override
public String getMessage() {
return "Hello from HelloMessageService!";
}
}HiMessageService.java
package com.example.demo.service;
public class HiMessageService implements MessageService {
@Override
public String getMessage() {
return "Hi from HiMessageService!";
}
}這些實(shí)現(xiàn)就像不同品牌或型號(hào)的U盤(pán)或其他 USB 設(shè)備。每個(gè)設(shè)備都有自己的功能和特性,但都遵循相同的 USB 標(biāo)準(zhǔn)。
步驟3:注冊(cè)服務(wù)提供者
在資源目錄(通常是 src/main/resources/ )下創(chuàng)建一個(gè)名為 META-INF/services/ 的文件夾。在這個(gè)文件夾中,創(chuàng)建一個(gè)名為 com.example.demo.service.MessageService 的文件(這是我們接口的全限定名),這個(gè)文件沒(méi)有任何文件擴(kuò)展名,所以不要加上 .txt 這樣的后綴。文件的內(nèi)容應(yīng)為我們的兩個(gè)實(shí)現(xiàn)類的全限定名,每個(gè)名字占一行:
com.example.demo.service.HelloMessageService com.example.demo.service.HiMessageService

META-INF/services/ 是 Java SPI ( Service Provider Interface ) 機(jī)制中約定俗成的特定目錄。它不是隨意選擇的,而是 SPI 規(guī)范中明確定義的。因此,當(dāng)使用 JDK 的 ServiceLoader 類來(lái)加載服務(wù)提供者時(shí),它會(huì)特意去查找這個(gè)路徑下的文件。
請(qǐng)確保文件的每一行只有一個(gè)名稱,并且沒(méi)有額外的空格或隱藏的字符,文件使用 UTF-8 編碼。
步驟4:使用 ServiceLoader 加載和使用服務(wù)
package com.example.demo;
import com.example.demo.service.MessageService;
import java.util.ServiceLoader;
public class DemoApplication {
public static void main(String[] args) {
ServiceLoader<MessageService> loaders = ServiceLoader.load(MessageService.class);
for (MessageService service : loaders) {
System.out.println(service.getMessage());
}
}
}運(yùn)行結(jié)果如下:

這說(shuō)明 ServiceLoader 成功地加載了我們?yōu)?MessageService 接口提供的兩個(gè)實(shí)現(xiàn),并且我們可以在不修改 Main 類的代碼的情況下,通過(guò)添加更多的實(shí)現(xiàn)類和更新 META-INF/services/com.example.MessageService 文件來(lái)擴(kuò)展我們的服務(wù)。
想象一下買了一臺(tái)高端的智能電視,這臺(tái)電視上有一個(gè)或多個(gè) HDMI 端口,這就是它與外部設(shè)備連接的接口。
- 定義服務(wù)接口:這就像電視定義了
HDMI端口的標(biāo)準(zhǔn)。在上面的代碼中,MessageService接口就是這個(gè)“HDMI端口”,定義了如何與外部設(shè)備交流。 - 為服務(wù)接口提供實(shí)現(xiàn):這類似于制造商為
HDMI接口生產(chǎn)各種設(shè)備,如游戲機(jī)、藍(lán)光播放器或流媒體棒。在代碼中,HelloMessageService和HiMessageService就是這些“HDMI設(shè)備”。每個(gè)設(shè)備/實(shí)現(xiàn)都有其獨(dú)特的輸出,但都遵循了統(tǒng)一的HDMI標(biāo)準(zhǔn)(MessageService接口)。 - 注冊(cè)服務(wù)提供者:當(dāng)我們購(gòu)買了一個(gè)
HDMI設(shè)備,它通常都會(huì)在包裝盒上明確標(biāo)明“適用于HDMI”。這就像一個(gè)標(biāo)識(shí),告訴用戶它可以連接到任何帶有HDMI接口的電視。在SPI的例子中,META-INF/services/目錄和其中的文件就像這個(gè)“標(biāo)簽”,告訴JDK哪些類是MessageService的實(shí)現(xiàn)。 - 使用ServiceLoader加載和使用服務(wù):當(dāng)插入一個(gè)
HDMI設(shè)備到電視上,并切換到正確的輸入頻道,電視就會(huì)顯示該設(shè)備的內(nèi)容。類似地,在代碼的這個(gè)步驟中,ServiceLoader就像電視的輸入選擇功能,能夠發(fā)現(xiàn)和使用所有已連接的HDMI設(shè)備(即MessageService的所有實(shí)現(xiàn))。
3. SPI在Spring框架中的應(yīng)用
Spring 官方在其文檔和源代碼中多次提到了 SPI ( Service Provider Interface )的概念。但是,當(dāng)我們說(shuō)“ Spring 的 SPI ”時(shí),通常指的是 Spring 框架為開(kāi)發(fā)者提供的一套可擴(kuò)展的接口和抽象類,開(kāi)發(fā)者可以基于這些接口和抽象類實(shí)現(xiàn)自己的版本。
在 Spring 中, SPI 的概念與 Spring Boot 使用的 spring.factories 文件的機(jī)制不完全一樣,但是它們都體現(xiàn)了可插拔、可擴(kuò)展的思想。
1.Spring的SPI:
Spring的核心框架提供了很多接口和抽象類,如BeanPostProcessor,PropertySource,ApplicationContextInitializer等,這些都可以看作是Spring的SPI。開(kāi)發(fā)者可以實(shí)現(xiàn)這些接口來(lái)擴(kuò)展Spring的功能。這些接口允許開(kāi)發(fā)者在Spring容器的生命周期的不同階段介入,實(shí)現(xiàn)自己的邏輯。 Spring
2.Boot的spring.factories機(jī)制:
spring.factories是Spring Boot的一個(gè)特性,允許開(kāi)發(fā)者自定義自動(dòng)配置。通過(guò)spring.factories文件,開(kāi)發(fā)者可以定義自己的自動(dòng)配置類,這些類在Spring Boot啟動(dòng)時(shí)會(huì)被自動(dòng)加載。- 在這種情況下,
SpringFactoriesLoader的使用,尤其是通過(guò)spring.factories文件來(lái)加載和實(shí)例化定義的類,可以看作是一種特定的SPI實(shí)現(xiàn)方式,但它特定于Spring Boot。
3.1 傳統(tǒng)Spring框架中的SPI思想
在傳統(tǒng)的 Spring 框架中,雖然沒(méi)有直接使用名為 "SPI" 的術(shù)語(yǔ),但其核心思想仍然存在。 Spring 提供了多個(gè)擴(kuò)展點(diǎn),其中最具代表性的就是 BeanPostProcessor 。在本節(jié)中,我們將通過(guò)一個(gè)簡(jiǎn)單的 MessageService 接口及其實(shí)現(xiàn)來(lái)探討如何利用 Spring 的 BeanPostProcessor 擴(kuò)展點(diǎn)體現(xiàn) SPI 的思想。
提供兩個(gè)簡(jiǎn)單的實(shí)現(xiàn)類。
HelloMessageService.java
package com.example.demo.service;
public class HelloMessageService implements MessageService {
@Override
public String getMessage() {
return "Hello from HelloMessageService!";
}
}HiMessageService.java
package com.example.demo.service;
public class HiMessageService implements MessageService {
@Override
public String getMessage() {
return "Hi from HiMessageService!";
}
}定義 BeanPostProcessor
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class MessageServicePostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean instanceof MessageService) {
return new MessageService() {
@Override
public String getMessage() {
return ((MessageService) bean).getMessage() + " [Processed by Spring SPI]";
}
};
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}修改 Spring 配置
將 MessageServicePostProcessor 添加到 Spring 配置中:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessageServiceConfig {
@Bean
public MessageService helloMessageService() {
return new HelloMessageService();
}
@Bean
public MessageService hiMessageService() {
return new HiMessageService();
}
@Bean
public MessageServicePostProcessor messageServicePostProcessor() {
return new MessageServicePostProcessor();
}
}執(zhí)行程序
使用之前提供的 DemoApplication 示例類:
package com.example.demo;
import com.example.demo.configuration.MessageServiceConfig;
import com.example.demo.service.MessageService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class DemoApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MessageServiceConfig.class);
MessageService helloMessageService = context.getBean("helloMessageService", MessageService.class);
MessageService hiMessageService = context.getBean("hiMessageService", MessageService.class);
System.out.println(helloMessageService.getMessage());
System.out.println(hiMessageService.getMessage());
}
}運(yùn)行結(jié)果:

現(xiàn)在,每一個(gè) MessageService 實(shí)現(xiàn)都被 BeanPostProcessor 處理了,添加了額外的消息 “[Processed by Spring SPI]” 。這演示了 Spring 的 SPI 概念,通過(guò) BeanPostProcessor 來(lái)擴(kuò)展或修改 Spring 容器中的 bean 。
有人可能留意到這里紅色的警告,這個(gè)之前在講 BeanPostProcessor 的時(shí)候也提到過(guò),當(dāng) BeanPostProcessor 自身被一個(gè)或多個(gè) BeanPostProcessor 處理時(shí),就會(huì)出現(xiàn)這種情況。簡(jiǎn)單地說(shuō),由于 BeanPostProcessor 需要在其他 bean 之前初始化,所以某些 BeanPostProcessor 無(wú)法處理早期初始化的 bean ,包括配置類和其他 BeanPostProcessor 。解決辦法就是不要把 MessageServicePostProcessor 放在配置類初始化,在配置類刪掉,再把 MessageServicePostProcessor 加上 @Component 注解。
類比文章開(kāi)頭的電視機(jī)的例子:
- 電視機(jī)與USB插口: 在這個(gè)新的示例中,電視機(jī)仍然是核心的
Spring應(yīng)用程序,具體來(lái)說(shuō)是DemoApplication類。這個(gè)核心應(yīng)用程序需要從某個(gè)服務(wù)(即MessageService)獲取并打印一條消息。 - USB插口: 與之前一樣,
MessageService接口就是這個(gè)"USB插口"。它為電視機(jī)提供了一個(gè)標(biāo)準(zhǔn)化的接口,即getMessage()方法,但沒(méi)有規(guī)定具體怎么實(shí)現(xiàn)。 - 設(shè)備制造商與他們的產(chǎn)品: 在這里,我們有兩種設(shè)備制造商或第三方提供者:
HelloMessageService和HiMessageService。它們?yōu)?quot;USB插口"(即MessageService接口)提供了不同的設(shè)備或?qū)崿F(xiàn)。一個(gè)顯示“Hello from HelloMessageService!”,另一個(gè)顯示“Hi from HiMessageService!”。 - BeanPostProcessor: 這是一個(gè)特殊的“魔法盒子”,可以將其視為一個(gè)能夠攔截并修改電視機(jī)顯示內(nèi)容的智能設(shè)備。當(dāng)插入
USB設(shè)備(即MessageService的實(shí)現(xiàn))并嘗試從中獲取消息時(shí),這個(gè)“魔法盒子”會(huì)介入,并為每條消息添加“[Processed by Spring SPI]”。 - Spring上下文配置: 這依然是電視機(jī)的使用說(shuō)明書(shū),但現(xiàn)在是使用了基于
Java的配置方式,即MessageServiceConfig類。這個(gè)“使用說(shuō)明書(shū)”指導(dǎo)Spring容器如何創(chuàng)建并管理MessageService的實(shí)例,并且還指導(dǎo)它如何使用“魔法盒子”(即MessageServicePostProcessor)來(lái)處理消息。
總的來(lái)說(shuō),與之前的例子相比,這個(gè)新示例提供了一個(gè)更加動(dòng)態(tài)的場(chǎng)景,其中 Spring 的 BeanPostProcessor 擴(kuò)展點(diǎn)允許我們攔截并修改 bean 的行為,就像一個(gè)能夠干預(yù)并改變電視機(jī)顯示內(nèi)容的智能設(shè)備。
3.2 Spring Boot中的SPI思想
Spring Boot 有一個(gè)與 SPI 相似的機(jī)制,但它并不完全等同于 Java 的標(biāo)準(zhǔn) SPI 。
Spring Boot 的自動(dòng)配置機(jī)制主要依賴于 spring.factories 文件。這個(gè)文件可以在多個(gè) jar 中存在,并且 Spring Boot 會(huì)加載所有可見(jiàn)的 spring.factories 文件。我們可以在這個(gè)文件中聲明一系列的自動(dòng)配置類,這樣當(dāng)滿足某些條件時(shí),這些配置類會(huì)自動(dòng)被 Spring Boot 應(yīng)用。
接下來(lái)會(huì)展示 Spring SPI 思想的好例子,但是它與 Spring Boot 緊密相關(guān)。
定義接口
package com.example.demo.service;
public interface MessageService {
String getMessage();
}這里會(huì)提供兩個(gè)簡(jiǎn)單的實(shí)現(xiàn)類。
HelloMessageService.java
package com.example.demo.service;
public class HelloMessageService implements MessageService {
@Override
public String getMessage() {
return "Hello from HelloMessageService!";
}
}HiMessageService.java
package com.example.demo.service;
public class HiMessageService implements MessageService {
@Override
public String getMessage() {
return "Hi from HiMessageService!";
}
}注冊(cè)服務(wù)
在 resources/META-INF 下創(chuàng)建一個(gè)文件名為 spring.factories 。這個(gè)文件里,可以注冊(cè) MessageService 實(shí)現(xiàn)類。
com.example.demo.service.MessageService=com.example.demo.service.HelloMessageService,com.example.demo.service.HiMessageService

注意這里 com.example.demo.service.MessageService 是接口的全路徑,而 com.example.demo.service.HelloMessageService,com.example.demo.service.HiMessageService 是實(shí)現(xiàn)類的全路徑。如果有多個(gè)實(shí)現(xiàn)類,它們應(yīng)當(dāng)用逗號(hào)分隔。
spring.factories 文件中的條目鍵和值之間不能有換行,即 key=value 形式的結(jié)構(gòu)必須在同一行開(kāi)始。但是,如果有多個(gè)值需要列出(如多個(gè)實(shí)現(xiàn)類),并且這些值是逗號(hào)分隔的,那么可以使用反斜杠( \ )來(lái)?yè)Q行。 spring.factories 的名稱是約定俗成的。如果試圖使用一個(gè)不同的文件名,那么 Spring Boot 的自動(dòng)配置機(jī)制將不會(huì)識(shí)別它。
這里 spring.factories 又可以寫(xiě)為
com.example.demo.service.MessageService=com.example.demo.service.HelloMessageService,\ com.example.demo.service.HiMessageService
直接在逗號(hào)后面回車 IDEA 會(huì)自動(dòng)補(bǔ)全反斜杠,保證鍵和值之間不能有換行即可。
使用 SpringFactoriesLoader 來(lái)加載服務(wù)
package com.example.demo;
import com.example.demo.service.MessageService;
import org.springframework.core.io.support.SpringFactoriesLoader;
import java.util.List;
public class DemoApplication {
public static void main(String[] args) {
List<MessageService> services = SpringFactoriesLoader.loadFactories(MessageService.class, null);
for (MessageService service : services) {
System.out.println(service.getMessage());
}
}
}SpringFactoriesLoader.loadFactories 的第二個(gè)參數(shù)是類加載器,此處我們使用默認(rèn)的類加載器,所以傳遞 null 。
運(yùn)行結(jié)果:

這種方式利用了 Spring 的 SpringFactoriesLoader ,它允許開(kāi)發(fā)者提供接口的多種實(shí)現(xiàn),并通過(guò) spring.factories 文件來(lái)注冊(cè)它們。這與 JDK 的 SPI 思想非常相似,只是在實(shí)現(xiàn)細(xì)節(jié)上有所不同。這也是 Spring Boot 如何自動(dòng)配置的基礎(chǔ),它會(huì)查找各種 spring.factories 文件,根據(jù)其中定義的類來(lái)初始化和配置 bean 。
我們繼續(xù)使用電視機(jī)的例子來(lái)解釋:
- 電視機(jī): 這是我們的
Spring應(yīng)用,就像DemoApplication。電視機(jī)是查看不同信號(hào)源或通道的設(shè)備,我們的應(yīng)用程序是為了運(yùn)行并使用不同的服務(wù)實(shí)現(xiàn)。 - USB插口: 這代表我們的
MessageService接口。USB插口是一個(gè)標(biāo)準(zhǔn)的接口,它允許連接各種設(shè)備,就像MessageService接口允許有多種實(shí)現(xiàn)方式。 - USB設(shè)備(如U盤(pán)或移動(dòng)硬盤(pán)): 這代表我們的服務(wù)實(shí)現(xiàn),例如
HelloMessageService和HiMessageService。每個(gè)USB設(shè)備在插入電視機(jī)后都有特定的內(nèi)容或功能,這就像我們的每個(gè)服務(wù)實(shí)現(xiàn)返回不同的消息。 - 電視機(jī)的USB設(shè)備目錄: 這是
spring.factories文件。當(dāng)我們將USB設(shè)備插入電視機(jī)時(shí),電視機(jī)會(huì)檢查設(shè)備的信息或內(nèi)容,spring.factories文件告訴Spring Boot哪些服務(wù)實(shí)現(xiàn)是可用的,就像電視機(jī)知道有哪些USB設(shè)備被插入。 - 電視機(jī)的USB掃描功能: 這就是
SpringFactoriesLoader。當(dāng)我們要從電視機(jī)上查看USB內(nèi)容時(shí),電視機(jī)會(huì)掃描并顯示內(nèi)容。同樣,當(dāng)DemoApplication運(yùn)行時(shí),SpringFactoriesLoader會(huì)查找并加載在spring.factories文件中列出的服務(wù)實(shí)現(xiàn)。
簡(jiǎn)化解釋:
- 當(dāng)插入
USB設(shè)備到電視機(jī),期望電視機(jī)能夠識(shí)別并顯示該設(shè)備的內(nèi)容。 - 在我們的例子中,
USB設(shè)備的內(nèi)容就是從MessageService實(shí)現(xiàn)類返回的消息。 spring.factories文件就像電視機(jī)的內(nèi)置目錄,告訴電視機(jī)哪些USB設(shè)備是已知的和可以使用的。- 當(dāng)我們的
DemoApplication(電視機(jī))運(yùn)行時(shí),它使用SpringFactoriesLoader(USB掃描功能)來(lái)檢查哪些服務(wù)(USB設(shè)備)是可用的,并輸出相應(yīng)的消息(顯示USB內(nèi)容)。
總結(jié):在這個(gè) Spring Boot 的 SPI 例子中,我們展示了核心 Spring 應(yīng)用如何自動(dòng)地識(shí)別和使用 spring.factories 文件中注冊(cè)的實(shí)現(xiàn),這與電視機(jī)自動(dòng)地識(shí)別和使用所有插入的 USB 設(shè)備有相似之處。
4. SPI在JDBC驅(qū)動(dòng)加載中的應(yīng)用
數(shù)據(jù)庫(kù)驅(qū)動(dòng)的 SPI 主要體現(xiàn)在 JDBC 驅(qū)動(dòng)的自動(dòng)發(fā)現(xiàn)機(jī)制中。 JDBC 4.0 引入了一個(gè)特性,允許驅(qū)動(dòng)自動(dòng)注冊(cè)到 DriverManager 。這是通過(guò)使用 Java 的 SPI 來(lái)實(shí)現(xiàn)的。驅(qū)動(dòng) jar 包內(nèi)會(huì)有一個(gè) META-INF/services/java.sql.Driver 文件,此文件中包含了該驅(qū)動(dòng)的 Driver 實(shí)現(xiàn)類的全類名。這樣,當(dāng)類路徑中有 JDBC 驅(qū)動(dòng)的 jar 文件時(shí), Java 應(yīng)用程序可以自動(dòng)發(fā)現(xiàn)并加載 JDBC 驅(qū)動(dòng),而無(wú)需明確地加載驅(qū)動(dòng)類。
這意味著任何數(shù)據(jù)庫(kù)供應(yīng)商都可以編寫(xiě)其自己的 JDBC 驅(qū)動(dòng)程序,只要它遵循 JDBC 驅(qū)動(dòng)程序的 SPI ,它就可以被任何使用 JDBC 的 Java 應(yīng)用程序所使用。
當(dāng)我們使用 DriverManager.getConnection() 獲取數(shù)據(jù)庫(kù)連接時(shí),背后正是利用 SPI 機(jī)制加載合適的驅(qū)動(dòng)程序。
以下是 SPI 機(jī)制的具體工作方式:
1.定義服務(wù)接口:
在這里,接口已經(jīng)由 Java 平臺(tái)定義,即 java.sql.Driver 。
2.為接口提供實(shí)現(xiàn):
各大數(shù)據(jù)庫(kù)廠商(如 Oracle , MySQL , PostgreSQL 等)為其數(shù)據(jù)庫(kù)提供了 JDBC 驅(qū)動(dòng)程序,它們都實(shí)現(xiàn)了 java.sql.Driver 接口。例如, MySQL 的驅(qū)動(dòng)程序中有一個(gè)類似于以下的類:
public class com.mysql.cj.jdbc.Driver implements java.sql.Driver {
// 實(shí)現(xiàn)接口方法...
}直接上圖:

3.注冊(cè)服務(wù)提供者:
對(duì)于 MySQL 的驅(qū)動(dòng)程序,可以在其 JAR 文件的 META-INF/services 目錄下找到一個(gè)名為 java.sql.Driver 的文件,文件內(nèi)容如下:
com.mysql.cj.jdbc.Driver
直接上圖:

看到這里是不是發(fā)現(xiàn)和第 2 節(jié)舉的 JDK SPI 的例子一樣?體會(huì)一下。
4.使用SPI來(lái)加載和使用服務(wù):
當(dāng)我們調(diào)用 DriverManager.getConnection(jdbcUrl, username, password) 時(shí), DriverManager 會(huì)使用 ServiceLoader 來(lái)查找所有已注冊(cè)的 java.sql.Driver 實(shí)現(xiàn)。然后,它會(huì)嘗試每一個(gè)驅(qū)動(dòng)程序,直到找到一個(gè)可以處理給定 jdbcUrl 的驅(qū)動(dòng)程序。
以下是一個(gè)簡(jiǎn)單的示例,展示如何使用 JDBC SPI 獲取數(shù)據(jù)庫(kù)連接:
import java.sql.Connection;
import java.sql.DriverManager;
public class JdbcExample {
public static void main(String[] args) {
String jdbcUrl = "jdbc:mysql://localhost:3306/mydatabase";
String username = "root";
String password = "password";
try {
Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
System.out.println("Connected to the database!");
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
} 在上述代碼中,我們沒(méi)有明確指定使用哪個(gè) JDBC 驅(qū)動(dòng)程序,因?yàn)?DriverManager 會(huì)自動(dòng)為我們選擇合適的驅(qū)動(dòng)程序。
這種模塊化和插件化的機(jī)制使得我們可以輕松地為不同的數(shù)據(jù)庫(kù)切換驅(qū)動(dòng)程序,只需要更改 JDBC URL 并確保相應(yīng)的驅(qū)動(dòng)程序 JAR 在類路徑上即可。
在 Spring Boot 中,開(kāi)發(fā)者通常不會(huì)直接與 JDBC 的 SPI 機(jī)制交互來(lái)獲取數(shù)據(jù)庫(kù)連接。 Spring Boot 的自動(dòng)配置機(jī)制隱藏了許多底層細(xì)節(jié),使得配置和使用數(shù)據(jù)庫(kù)變得更加簡(jiǎn)單。
一般會(huì)在 application.properties 或 application.yml 中配置數(shù)據(jù)庫(kù)連接信息。
例如:
spring.datasource.url=jdbc:mysql://localhost:3306/mydatabase spring.datasource.username=root spring.datasource.password=password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
在上述步驟中, Spring Boot 的自動(dòng)配置機(jī)制會(huì)根據(jù)提供的依賴和配置信息來(lái)初始化和配置 DataSource 對(duì)象,這個(gè)對(duì)象管理數(shù)據(jù)庫(kù)連接。實(shí)際上,添加 JDBC 驅(qū)動(dòng)依賴時(shí), Spring Boot 會(huì)使用 JDK 的 SPI 機(jī)制(在 JDBC 規(guī)范中應(yīng)用)來(lái)找到并加載相應(yīng)的數(shù)據(jù)庫(kù)驅(qū)動(dòng)。開(kāi)發(fā)者雖然不直接與 JDK 的 SPI 交互,但在背后 Spring Boot 確實(shí)利用了 JDK SPI 機(jī)制來(lái)獲取數(shù)據(jù)庫(kù)連接。
5. 如何通過(guò)Spring Boot自動(dòng)配置理解SPI思想
這種機(jī)制有點(diǎn)類似于 Java 的 SPI ,因?yàn)樗试S第三方庫(kù)提供一些默認(rèn)的配置。但它比 Java 的 SPI 更為強(qiáng)大和靈活,因?yàn)?Spring Boot 提供了大量的注解(如 @ConditionalOnClass 、 @ConditionalOnProperty 、 @ConditionalOnMissingBean 等)來(lái)控制自動(dòng)配置類是否應(yīng)該被加載和應(yīng)用。
總的來(lái)說(shuō), Spring Boot 的 spring.factories 機(jī)制和 Java 的 SPI 在概念上是相似的,但它們?cè)趯?shí)現(xiàn)細(xì)節(jié)和用途上有所不同。
讓我們創(chuàng)建一個(gè)簡(jiǎn)化的實(shí)際例子,假設(shè)我們要為不同的消息服務(wù)(如 SMS 和 Email )創(chuàng)建自動(dòng)配置。
MessageService接口:
package com.example.demo.service;
public interface MessageService {
void send(String message);
}SMS服務(wù)實(shí)現(xiàn):
package com.example.demo.service.impl;
import com.example.demo.service.MessageService;
public class SmsService implements MessageService {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}Email服務(wù)實(shí)現(xiàn):
package com.example.demo.service.impl;
import com.example.demo.service.MessageService;
public class EmailService implements MessageService {
@Override
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}自動(dòng)配置類:
package com.example.demo.configuration;
import com.example.demo.service.EmailService;
import com.example.demo.service.MessageService;
import com.example.demo.service.SmsService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessageAutoConfiguration {
@Bean
@ConditionalOnProperty(name = "message.type", havingValue = "sms")
public MessageService smsService() {
return new SmsService();
}
@Bean
@ConditionalOnProperty(name = "message.type", havingValue = "email")
public MessageService emailService() {
return new EmailService();
}
} 這個(gè)類提供兩個(gè)條件性的 beans (組件),分別是 SmsService 和 EmailService 。這些 beans 的創(chuàng)建取決于 application.properties 文件中特定的屬性值。
- @ConditionalOnProperty(name = “message.type”, havingValue = “sms”)
當(dāng) application.properties 或 application.yml 中定義的屬性 message.type 的值為 sms 時(shí),此條件為 true 。此時(shí), smsService() 方法將被調(diào)用,從而創(chuàng)建一個(gè) SmsService 的 bean 。
- @ConditionalOnProperty(name = “message.type”, havingValue = “email”)
當(dāng) application.properties 或 application.yml 中定義的屬性 message.type 的值為 email 時(shí),此條件為 true 。此時(shí), emailService() 方法將被調(diào)用,從而創(chuàng)建一個(gè) EmailService 的 bean 。
spring.factories文件:
在 src/main/resources/META-INF 目錄下創(chuàng)建一個(gè) spring.factories 文件,內(nèi)容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.demo.configuration.MessageAutoConfiguration
application.properties文件:
message.type=sms
MessageTester組件:
package com.example.demo;
import com.example.demo.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class MessageTester {
@Autowired
private MessageService messageService;
@PostConstruct
public void init() {
messageService.send("Hello World");
}
}DemoApplication主程序:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}運(yùn)行結(jié)果:

在上述例子中,我們創(chuàng)建了一個(gè) MessageService 接口和兩個(gè)實(shí)現(xiàn)( SmsService 和 EmailService )。然后,我們創(chuàng)建了一個(gè)自動(dòng)配置類,其中包含兩個(gè) bean 定義,這兩個(gè) bean 定義分別基于 application.properties 中的屬性值條件性地創(chuàng)建。在 spring.factories 文件中,我們聲明了這個(gè)自動(dòng)配置類,以便 Spring Boot 在啟動(dòng)時(shí)能夠自動(dòng)加載它。
在此,繼續(xù)用電視機(jī)的例子升華理解下
電視機(jī)類比
1.總體概念:
- 假設(shè)電視機(jī)(
TV)是一個(gè)Java應(yīng)用。 - 電視機(jī)的各種插槽,如
HDMI、USB、VGA等,可以視為應(yīng)用中的SPI接口。 - 插入這些插槽的設(shè)備(如
DVD播放器、游戲機(jī)、USB驅(qū)動(dòng)器等)可以視為SPI的實(shí)現(xiàn)。
2.Java的SPI:
- 當(dāng)我們購(gòu)買電視機(jī)時(shí),不知道將會(huì)連接哪種設(shè)備,可能是
DVD播放器,也可能是游戲機(jī)。 - 但是,只要這些設(shè)備遵循了插槽的標(biāo)準(zhǔn)(例如,
HDMI標(biāo)準(zhǔn)),就可以將其插入電視機(jī)并使其工作。 - 這就像
Java的SPI機(jī)制:為了能讓多個(gè)供應(yīng)商提供實(shí)現(xiàn),Java定義了一個(gè)接口,供應(yīng)商提供具體的實(shí)現(xiàn)。
3.Spring Boot的自動(dòng)配置:
- 現(xiàn)在,想象一下現(xiàn)代的智能電視。當(dāng)插入一個(gè)設(shè)備,電視機(jī)不僅可以識(shí)別它,還可能根據(jù)所連接的設(shè)備類型自動(dòng)調(diào)整設(shè)置,例如選擇正確的輸入源、優(yōu)化圖像質(zhì)量等。
- 這就像
Spring Boot的自動(dòng)配置:當(dāng)Spring Boot應(yīng)用啟動(dòng)時(shí),它會(huì)檢查classpath上的庫(kù),并根據(jù)存在的庫(kù)自動(dòng)配置應(yīng)用。 - 電視機(jī)的自動(dòng)設(shè)置可以類比為
Spring Boot中的spring.factories和各種@Conditional…注解。它們決定在什么條件下進(jìn)行哪種配置。
4.擴(kuò)展性:
- 如果電視制造商想為新型的插槽或連接技術(shù)開(kāi)發(fā)電視,它可以很容易地在其電視機(jī)型中添加新的插槽。
- 同樣地,使用
Spring Boot,如果要為應(yīng)用添加新功能或庫(kù),只需添加相關(guān)的依賴,然后Spring Boot會(huì)自動(dòng)識(shí)別并配置這些新功能。
通過(guò)這種類比,電視機(jī)的插槽和自動(dòng)設(shè)置功能為我們提供了一個(gè)直觀的方式來(lái)理解 Java 的 SPI 機(jī)制和 Spring Boot 的自動(dòng)配置如何工作,以及它們?nèi)绾螢閼?yīng)用開(kāi)發(fā)者提供便利。
6. SPI(Service Provider Interface)總結(jié)
SPI ,即服務(wù)提供者接口,是一種特定的設(shè)計(jì)模式。它允許框架或核心庫(kù)為第三方開(kāi)發(fā)者提供一個(gè)預(yù)定義的接口,從而使他們能夠?yàn)榭蚣芴峁┳远x的實(shí)現(xiàn)或擴(kuò)展。
核心目標(biāo):
- 解耦:
SPI機(jī)制讓框架的核心與其擴(kuò)展部分保持解耦,使核心代碼不依賴于具體的實(shí)現(xiàn)。 - 動(dòng)態(tài)加載:系統(tǒng)能夠通過(guò)特定的機(jī)制(如
Java的ServiceLoader)動(dòng)態(tài)地發(fā)現(xiàn)和加載所需的實(shí)現(xiàn)。 - 靈活性:框架用戶可以根據(jù)自己的需求選擇、更換或增加新的實(shí)現(xiàn),而無(wú)需修改核心代碼。
可插拔:第三方提供的服務(wù)或?qū)崿F(xiàn)可以輕松地添加到或從系統(tǒng)中移除,無(wú)需更改現(xiàn)有的代碼結(jié)構(gòu)。
價(jià)值:
- 為框架或庫(kù)的用戶提供更多的自定義選項(xiàng)和靈活性。
- 允許框架的核心部分保持穩(wěn)定,同時(shí)能夠容納新的功能和擴(kuò)展。
SPI與“開(kāi)閉原則”:
“開(kāi)閉原則”提倡軟件實(shí)體應(yīng)該對(duì)擴(kuò)展開(kāi)放,但對(duì)修改封閉。即在不改變現(xiàn)有代碼的前提下,通過(guò)擴(kuò)展來(lái)增加新的功能。
SPI如何體現(xiàn)“開(kāi)閉原則”:
對(duì)擴(kuò)展開(kāi)放: SPI 提供了一種標(biāo)準(zhǔn)化的方式,使第三方開(kāi)發(fā)者可以為現(xiàn)有系統(tǒng)提供新的實(shí)現(xiàn)或功能。
對(duì)修改封閉:添加新的功能或特性時(shí),原始框架或庫(kù)的代碼不需要進(jìn)行修改。
獨(dú)立發(fā)展:框架與其 SPI 實(shí)現(xiàn)可以獨(dú)立地進(jìn)化和發(fā)展,互不影響。
總之, SPI 是一種使軟件框架或庫(kù)更加模塊化、可擴(kuò)展和可維護(hù)的有效方法。通過(guò)遵循“開(kāi)閉原則”, SPI 確保了系統(tǒng)的穩(wěn)定性和靈活性,從而滿足了不斷變化的業(yè)務(wù)需求。
到此這篇關(guān)于SPI機(jī)制在JDK與Spring Boot中的應(yīng)用的文章就介紹到這了,更多相關(guān)JDK與Spring Boot應(yīng)用內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 14 發(fā)布了,你還會(huì)使用Lombok?
2020年3月17日發(fā)布,Java正式發(fā)布了JDK 14 ,目前已經(jīng)可以開(kāi)放下載。在JDK 14中,共有16個(gè)新特性,本文主要來(lái)介紹其中的一個(gè)特性:JEP 359: Records,需要的朋友可以參考下2020-04-04
基于Java實(shí)現(xiàn)進(jìn)制轉(zhuǎn)換工具類的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何基于Java實(shí)現(xiàn)進(jìn)制轉(zhuǎn)換工具類,從而實(shí)現(xiàn)減少參數(shù)長(zhǎng)度的效果,文中的示例代碼講解詳細(xì),需要的可以參考一下2023-02-02
在deepin上如何使用Fleet開(kāi)發(fā)SpringBoot?3.0.0項(xiàng)目
這篇文章主要介紹了在deepin上使用Fleet開(kāi)發(fā)SpringBoot?3.0.0項(xiàng)目的過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09
JPA如何將查詢結(jié)果轉(zhuǎn)換為DTO對(duì)象
這篇文章主要介紹了JPA如何將查詢結(jié)果轉(zhuǎn)換為DTO對(duì)象,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02
SpringBoot通過(guò)自定義注解實(shí)現(xiàn)配置類的自動(dòng)注入的實(shí)現(xiàn)
本文主要介紹了SpringBoot通過(guò)自定義注解實(shí)現(xiàn)配置類的自動(dòng)注入的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
SpringBoot集成SFTP客戶端實(shí)現(xiàn)文件上傳下載實(shí)例
這篇文章主要為大家介紹了SpringBoot集成SFTP客戶端實(shí)現(xiàn)文件上傳下載實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
java 中 poi解析Excel文件版本問(wèn)題解決辦法
這篇文章主要介紹了java 中 poi解析Excel文件版本問(wèn)題解決辦法的相關(guān)資料,需要的朋友可以參考下2017-08-08

