深入淺析SPI機(jī)制在JDK與Spring?Boot中的應(yīng)用
1. SPI解讀:什么是SPI?
SPI
( Service Provider Interface
) 是一種服務(wù)發(fā)現(xiàn)機(jī)制,它允許第三方提供者為核心庫或主框架提供實(shí)現(xiàn)或擴(kuò)展。這種設(shè)計允許核心庫/框架在不修改自身代碼的情況下,通過第三方實(shí)現(xiàn)來增強(qiáng)功能。
1.JDK原生的SPI:
- 定義和發(fā)現(xiàn):
JDK
的SPI
主要通過在META-INF/services/
目錄下放置特定的文件來指定哪些類實(shí)現(xiàn)了給定的服務(wù)接口。這些文件的名稱應(yīng)為接口的全限定名,內(nèi)容為實(shí)現(xiàn)該接口的全限定類名。 - 加載機(jī)制:
ServiceLoader
類使用Java
的類加載器機(jī)制從META-INF/services/
目錄下加載和實(shí)例化服務(wù)提供者。例如,ServiceLoader.load(MyServiceInterface.class)
會返回一個實(shí)現(xiàn)了MyServiceInterface
的實(shí)例迭代器。 - 缺點(diǎn):
JDK
原生的SPI
每次通過ServiceLoader
加載時都會初始化一個新的實(shí)例,沒有實(shí)現(xiàn)類的緩存,也沒有考慮單例等高級功能。
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)境的不同來選擇加載哪個數(shù)據(jù)庫驅(qū)動。 - 配置:
Spring
允許通過spring.factories
文件在META-INF
目錄下進(jìn)行配置,這與JDK
的SPI
很相似,但它提供了更多的功能和靈活性。
舉個類比的例子:
想象我們正在建造一個電視機(jī), SPI
就像電視機(jī)上的一個 USB
插口。這個插口可以插入各種設(shè)備(例如U盤、游戲手柄、電視棒等),但我們并不關(guān)心這些設(shè)備的內(nèi)部工作方式。這樣只需要提供一個標(biāo)準(zhǔn)的接口,其他公司(例如U盤制造商)可以為此接口提供實(shí)現(xiàn)。這樣,電視機(jī)可以在不更改自己內(nèi)部代碼的情況下使用各種新設(shè)備,而設(shè)備制造商也可以為各種電視機(jī)制造兼容的設(shè)備。
總之, SPI
是一種將接口定義與實(shí)現(xiàn)分離的設(shè)計模式,它鼓勵第三方為一個核心產(chǎn)品或框架提供插件或?qū)崿F(xiàn),從而使核心產(chǎn)品能夠輕松地擴(kuò)展功能。
2. SPI在JDK中的應(yīng)用示例
在 Java
的生態(tài)系統(tǒng)中, SPI
是一個核心概念,允許開發(fā)者提供擴(kuò)展和替代的實(shí)現(xiàn),而核心庫或應(yīng)用不必更改,下面舉出一個例子來說明。
全部代碼和步驟如下:
步驟1:定義一個服務(wù)接口,文件名: MessageService.java
package com.example.demo.service; public interface MessageService { String getMessage(); }
步驟2:為服務(wù)接口提供實(shí)現(xià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)就像不同品牌或型號的U盤或其他 USB
設(shè)備。每個設(shè)備都有自己的功能和特性,但都遵循相同的 USB
標(biāo)準(zhǔn)。
步驟3:注冊服務(wù)提供者
在資源目錄(通常是 src/main/resources/
)下創(chuàng)建一個名為 META-INF/services/
的文件夾。在這個文件夾中,創(chuàng)建一個名為 com.example.demo.service.MessageService
的文件(這是我們接口的全限定名),這個文件沒有任何文件擴(kuò)展名,所以不要加上 .txt
這樣的后綴。文件的內(nèi)容應(yīng)為我們的兩個實(shí)現(xiàn)類的全限定名,每個名字占一行:
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
類來加載服務(wù)提供者時,它會特意去查找這個路徑下的文件。
請確保文件的每一行只有一個名稱,并且沒有額外的空格或隱藏的字符,文件使用 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é)果如下:
這說明 ServiceLoader
成功地加載了我們?yōu)?MessageService
接口提供的兩個實(shí)現(xiàn),并且我們可以在不修改 Main
類的代碼的情況下,通過添加更多的實(shí)現(xiàn)類和更新 META-INF/services/com.example.MessageService
文件來擴(kuò)展我們的服務(wù)。
想象一下買了一臺高端的智能電視,這臺電視上有一個或多個 HDMI
端口,這就是它與外部設(shè)備連接的接口。
- 定義服務(wù)接口:這就像電視定義了
HDMI
端口的標(biāo)準(zhǔn)。在上面的代碼中,MessageService
接口就是這個“HDMI
端口”,定義了如何與外部設(shè)備交流。 - 為服務(wù)接口提供實(shí)現(xiàn):這類似于制造商為
HDMI
接口生產(chǎn)各種設(shè)備,如游戲機(jī)、藍(lán)光播放器或流媒體棒。在代碼中,HelloMessageService
和HiMessageService
就是這些“HDMI
設(shè)備”。每個設(shè)備/實(shí)現(xiàn)都有其獨(dú)特的輸出,但都遵循了統(tǒng)一的HDMI
標(biāo)準(zhǔn)(MessageService
接口)。 - 注冊服務(wù)提供者:當(dāng)我們購買了一個
HDMI
設(shè)備,它通常都會在包裝盒上明確標(biāo)明“適用于HDMI
”。這就像一個標(biāo)識,告訴用戶它可以連接到任何帶有HDMI
接口的電視。在SPI
的例子中,META-INF/services/
目錄和其中的文件就像這個“標(biāo)簽”,告訴JDK
哪些類是MessageService
的實(shí)現(xiàn)。 - 使用ServiceLoader加載和使用服務(wù):當(dāng)插入一個
HDMI
設(shè)備到電視上,并切換到正確的輸入頻道,電視就會顯示該設(shè)備的內(nèi)容。類似地,在代碼的這個步驟中,ServiceLoader
就像電視的輸入選擇功能,能夠發(fā)現(xiàn)和使用所有已連接的HDMI
設(shè)備(即MessageService
的所有實(shí)現(xiàn))。
3. SPI在Spring框架中的應(yīng)用
Spring
官方在其文檔和源代碼中多次提到了 SPI
( Service Provider Interface
)的概念。但是,當(dāng)我們說“ Spring
的 SPI
”時,通常指的是 Spring
框架為開發(fā)者提供的一套可擴(kuò)展的接口和抽象類,開發(fā)者可以基于這些接口和抽象類實(shí)現(xiàn)自己的版本。
在 Spring
中, SPI
的概念與 Spring Boot
使用的 spring.factories
文件的機(jī)制不完全一樣,但是它們都體現(xiàn)了可插拔、可擴(kuò)展的思想。
1.Spring的SPI:
Spring
的核心框架提供了很多接口和抽象類,如BeanPostProcessor
,PropertySource
,ApplicationContextInitializer
等,這些都可以看作是Spring
的SPI
。開發(fā)者可以實(shí)現(xiàn)這些接口來擴(kuò)展Spring
的功能。這些接口允許開發(fā)者在Spring
容器的生命周期的不同階段介入,實(shí)現(xiàn)自己的邏輯。 Spring
2.Boot的spring.factories機(jī)制:
spring.factories
是Spring Boot
的一個特性,允許開發(fā)者自定義自動配置。通過spring.factories
文件,開發(fā)者可以定義自己的自動配置類,這些類在Spring Boot
啟動時會被自動加載。- 在這種情況下,
SpringFactoriesLoader
的使用,尤其是通過spring.factories
文件來加載和實(shí)例化定義的類,可以看作是一種特定的SPI
實(shí)現(xiàn)方式,但它特定于Spring Boot
。
3.1 傳統(tǒng)Spring框架中的SPI思想
在傳統(tǒng)的 Spring
框架中,雖然沒有直接使用名為 "SPI"
的術(shù)語,但其核心思想仍然存在。 Spring
提供了多個擴(kuò)展點(diǎn),其中最具代表性的就是 BeanPostProcessor
。在本節(jié)中,我們將通過一個簡單的 MessageService
接口及其實(shí)現(xiàn)來探討如何利用 Spring
的 BeanPostProcessor
擴(kuò)展點(diǎn)體現(xiàn) SPI
的思想。
提供兩個簡單的實(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)在,每一個 MessageService
實(shí)現(xiàn)都被 BeanPostProcessor
處理了,添加了額外的消息 “[Processed by Spring SPI]”
。這演示了 Spring
的 SPI
概念,通過 BeanPostProcessor
來擴(kuò)展或修改 Spring
容器中的 bean
。
有人可能留意到這里紅色的警告,這個之前在講 BeanPostProcessor
的時候也提到過,當(dāng) BeanPostProcessor
自身被一個或多個 BeanPostProcessor
處理時,就會出現(xiàn)這種情況。簡單地說,由于 BeanPostProcessor
需要在其他 bean
之前初始化,所以某些 BeanPostProcessor
無法處理早期初始化的 bean
,包括配置類和其他 BeanPostProcessor
。解決辦法就是不要把 MessageServicePostProcessor
放在配置類初始化,在配置類刪掉,再把 MessageServicePostProcessor
加上 @Component
注解。
類比文章開頭的電視機(jī)的例子:
- 電視機(jī)與USB插口: 在這個新的示例中,電視機(jī)仍然是核心的
Spring
應(yīng)用程序,具體來說是DemoApplication
類。這個核心應(yīng)用程序需要從某個服務(wù)(即MessageService
)獲取并打印一條消息。 - USB插口: 與之前一樣,
MessageService
接口就是這個"USB
插口"。它為電視機(jī)提供了一個標(biāo)準(zhǔn)化的接口,即getMessage()
方法,但沒有規(guī)定具體怎么實(shí)現(xiàn)。 - 設(shè)備制造商與他們的產(chǎn)品: 在這里,我們有兩種設(shè)備制造商或第三方提供者:
HelloMessageService
和HiMessageService
。它們?yōu)?quot;USB
插口"(即MessageService
接口)提供了不同的設(shè)備或?qū)崿F(xiàn)。一個顯示“Hello from HelloMessageService!”
,另一個顯示“Hi from HiMessageService!”
。 - BeanPostProcessor: 這是一個特殊的“魔法盒子”,可以將其視為一個能夠攔截并修改電視機(jī)顯示內(nèi)容的智能設(shè)備。當(dāng)插入
USB
設(shè)備(即MessageService
的實(shí)現(xiàn))并嘗試從中獲取消息時,這個“魔法盒子”會介入,并為每條消息添加“[Processed by Spring SPI]”
。 - Spring上下文配置: 這依然是電視機(jī)的使用說明書,但現(xiàn)在是使用了基于
Java
的配置方式,即MessageServiceConfig
類。這個“使用說明書”指導(dǎo)Spring
容器如何創(chuàng)建并管理MessageService
的實(shí)例,并且還指導(dǎo)它如何使用“魔法盒子”(即MessageServicePostProcessor
)來處理消息。
總的來說,與之前的例子相比,這個新示例提供了一個更加動態(tài)的場景,其中 Spring
的 BeanPostProcessor
擴(kuò)展點(diǎn)允許我們攔截并修改 bean
的行為,就像一個能夠干預(yù)并改變電視機(jī)顯示內(nèi)容的智能設(shè)備。
3.2 Spring Boot中的SPI思想
Spring Boot
有一個與 SPI
相似的機(jī)制,但它并不完全等同于 Java
的標(biāo)準(zhǔn) SPI
。
Spring Boot
的自動配置機(jī)制主要依賴于 spring.factories
文件。這個文件可以在多個 jar
中存在,并且 Spring Boot
會加載所有可見的 spring.factories
文件。我們可以在這個文件中聲明一系列的自動配置類,這樣當(dāng)滿足某些條件時,這些配置類會自動被 Spring Boot
應(yīng)用。
接下來會展示 Spring SPI
思想的好例子,但是它與 Spring Boot
緊密相關(guān)。
定義接口
package com.example.demo.service; public interface MessageService { String getMessage(); }
這里會提供兩個簡單的實(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!"; } }
注冊服務(wù)
在 resources/META-INF
下創(chuàng)建一個文件名為 spring.factories
。這個文件里,可以注冊 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)類的全路徑。如果有多個實(shí)現(xiàn)類,它們應(yīng)當(dāng)用逗號分隔。
spring.factories
文件中的條目鍵和值之間不能有換行,即 key=value
形式的結(jié)構(gòu)必須在同一行開始。但是,如果有多個值需要列出(如多個實(shí)現(xiàn)類),并且這些值是逗號分隔的,那么可以使用反斜杠( \
)來換行。 spring.factories
的名稱是約定俗成的。如果試圖使用一個不同的文件名,那么 Spring Boot
的自動配置機(jī)制將不會識別它。
這里 spring.factories
又可以寫為
com.example.demo.service.MessageService=com.example.demo.service.HelloMessageService,\ com.example.demo.service.HiMessageService
直接在逗號后面回車 IDEA
會自動補(bǔ)全反斜杠,保證鍵和值之間不能有換行即可。
使用 SpringFactoriesLoader
來加載服務(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
的第二個參數(shù)是類加載器,此處我們使用默認(rèn)的類加載器,所以傳遞 null
。
運(yùn)行結(jié)果:
這種方式利用了 Spring
的 SpringFactoriesLoader
,它允許開發(fā)者提供接口的多種實(shí)現(xiàn),并通過 spring.factories
文件來注冊它們。這與 JDK
的 SPI
思想非常相似,只是在實(shí)現(xiàn)細(xì)節(jié)上有所不同。這也是 Spring Boot
如何自動配置的基礎(chǔ),它會查找各種 spring.factories
文件,根據(jù)其中定義的類來初始化和配置 bean
。
我們繼續(xù)使用電視機(jī)的例子來解釋:
- 電視機(jī): 這是我們的
Spring
應(yīng)用,就像DemoApplication
。電視機(jī)是查看不同信號源或通道的設(shè)備,我們的應(yīng)用程序是為了運(yùn)行并使用不同的服務(wù)實(shí)現(xiàn)。 - USB插口: 這代表我們的
MessageService
接口。USB
插口是一個標(biāo)準(zhǔn)的接口,它允許連接各種設(shè)備,就像MessageService
接口允許有多種實(shí)現(xiàn)方式。 - USB設(shè)備(如U盤或移動硬盤): 這代表我們的服務(wù)實(shí)現(xiàn),例如
HelloMessageService
和HiMessageService
。每個USB
設(shè)備在插入電視機(jī)后都有特定的內(nèi)容或功能,這就像我們的每個服務(wù)實(shí)現(xiàn)返回不同的消息。 - 電視機(jī)的USB設(shè)備目錄: 這是
spring.factories
文件。當(dāng)我們將USB
設(shè)備插入電視機(jī)時,電視機(jī)會檢查設(shè)備的信息或內(nèi)容,spring.factories
文件告訴Spring Boot
哪些服務(wù)實(shí)現(xiàn)是可用的,就像電視機(jī)知道有哪些USB
設(shè)備被插入。 - 電視機(jī)的USB掃描功能: 這就是
SpringFactoriesLoader
。當(dāng)我們要從電視機(jī)上查看USB
內(nèi)容時,電視機(jī)會掃描并顯示內(nèi)容。同樣,當(dāng)DemoApplication
運(yùn)行時,SpringFactoriesLoader
會查找并加載在spring.factories
文件中列出的服務(wù)實(shí)現(xiàn)。
簡化解釋:
- 當(dāng)插入
USB
設(shè)備到電視機(jī),期望電視機(jī)能夠識別并顯示該設(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)行時,它使用SpringFactoriesLoader
(USB
掃描功能)來檢查哪些服務(wù)(USB
設(shè)備)是可用的,并輸出相應(yīng)的消息(顯示USB
內(nèi)容)。
總結(jié):在這個 Spring Boot
的 SPI
例子中,我們展示了核心 Spring
應(yīng)用如何自動地識別和使用 spring.factories
文件中注冊的實(shí)現(xiàn),這與電視機(jī)自動地識別和使用所有插入的 USB
設(shè)備有相似之處。
4. SPI在JDBC驅(qū)動加載中的應(yīng)用
數(shù)據(jù)庫驅(qū)動的 SPI
主要體現(xiàn)在 JDBC
驅(qū)動的自動發(fā)現(xiàn)機(jī)制中。 JDBC 4.0
引入了一個特性,允許驅(qū)動自動注冊到 DriverManager
。這是通過使用 Java
的 SPI
來實(shí)現(xiàn)的。驅(qū)動 jar
包內(nèi)會有一個 META-INF/services/java.sql.Driver
文件,此文件中包含了該驅(qū)動的 Driver
實(shí)現(xiàn)類的全類名。這樣,當(dāng)類路徑中有 JDBC
驅(qū)動的 jar
文件時, Java
應(yīng)用程序可以自動發(fā)現(xiàn)并加載 JDBC
驅(qū)動,而無需明確地加載驅(qū)動類。
這意味著任何數(shù)據(jù)庫供應(yīng)商都可以編寫其自己的 JDBC
驅(qū)動程序,只要它遵循 JDBC
驅(qū)動程序的 SPI
,它就可以被任何使用 JDBC
的 Java
應(yīng)用程序所使用。
當(dāng)我們使用 DriverManager.getConnection()
獲取數(shù)據(jù)庫連接時,背后正是利用 SPI
機(jī)制加載合適的驅(qū)動程序。
以下是 SPI
機(jī)制的具體工作方式:
1.定義服務(wù)接口:
在這里,接口已經(jīng)由 Java
平臺定義,即 java.sql.Driver
。
2.為接口提供實(shí)現(xiàn):
各大數(shù)據(jù)庫廠商(如 Oracle
, MySQL
, PostgreSQL
等)為其數(shù)據(jù)庫提供了 JDBC
驅(qū)動程序,它們都實(shí)現(xiàn)了 java.sql.Driver
接口。例如, MySQL
的驅(qū)動程序中有一個類似于以下的類:
public class com.mysql.cj.jdbc.Driver implements java.sql.Driver { // 實(shí)現(xiàn)接口方法... }
直接上圖:
3.注冊服務(wù)提供者:
對于 MySQL
的驅(qū)動程序,可以在其 JAR
文件的 META-INF/services
目錄下找到一個名為 java.sql.Driver
的文件,文件內(nèi)容如下:
com.mysql.cj.jdbc.Driver
直接上圖:
看到這里是不是發(fā)現(xiàn)和第 2
節(jié)舉的 JDK SPI
的例子一樣?體會一下。
4.使用SPI來加載和使用服務(wù):
當(dāng)我們調(diào)用 DriverManager.getConnection(jdbcUrl, username, password)
時, DriverManager
會使用 ServiceLoader
來查找所有已注冊的 java.sql.Driver
實(shí)現(xiàn)。然后,它會嘗試每一個驅(qū)動程序,直到找到一個可以處理給定 jdbcUrl
的驅(qū)動程序。
以下是一個簡單的示例,展示如何使用 JDBC SPI
獲取數(shù)據(jù)庫連接:
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(); } } }
在上述代碼中,我們沒有明確指定使用哪個 JDBC
驅(qū)動程序,因?yàn)?DriverManager
會自動為我們選擇合適的驅(qū)動程序。
這種模塊化和插件化的機(jī)制使得我們可以輕松地為不同的數(shù)據(jù)庫切換驅(qū)動程序,只需要更改 JDBC URL
并確保相應(yīng)的驅(qū)動程序 JAR
在類路徑上即可。
在 Spring Boot
中,開發(fā)者通常不會直接與 JDBC
的 SPI
機(jī)制交互來獲取數(shù)據(jù)庫連接。 Spring Boot
的自動配置機(jī)制隱藏了許多底層細(xì)節(jié),使得配置和使用數(shù)據(jù)庫變得更加簡單。
一般會在 application.properties
或 application.yml
中配置數(shù)據(jù)庫連接信息。
例如:
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
的自動配置機(jī)制會根據(jù)提供的依賴和配置信息來初始化和配置 DataSource
對象,這個對象管理數(shù)據(jù)庫連接。實(shí)際上,添加 JDBC
驅(qū)動依賴時, Spring Boot
會使用 JDK
的 SPI
機(jī)制(在 JDBC
規(guī)范中應(yīng)用)來找到并加載相應(yīng)的數(shù)據(jù)庫驅(qū)動。開發(fā)者雖然不直接與 JDK
的 SPI
交互,但在背后 Spring Boot
確實(shí)利用了 JDK SPI
機(jī)制來獲取數(shù)據(jù)庫連接。
5. 如何通過Spring Boot自動配置理解SPI思想
這種機(jī)制有點(diǎn)類似于 Java
的 SPI
,因?yàn)樗试S第三方庫提供一些默認(rèn)的配置。但它比 Java
的 SPI
更為強(qiáng)大和靈活,因?yàn)?Spring Boot
提供了大量的注解(如 @ConditionalOnClass
、 @ConditionalOnProperty
、 @ConditionalOnMissingBean
等)來控制自動配置類是否應(yīng)該被加載和應(yīng)用。
總的來說, Spring Boot
的 spring.factories
機(jī)制和 Java
的 SPI
在概念上是相似的,但它們在實(shí)現(xiàn)細(xì)節(jié)和用途上有所不同。
讓我們創(chuàng)建一個簡化的實(shí)際例子,假設(shè)我們要為不同的消息服務(wù)(如 SMS
和 Email
)創(chuà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); } }
自動配置類:
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(); } }
這個類提供兩個條件性的 beans
(組件),分別是 SmsService
和 EmailService
。這些 beans
的創(chuàng)建取決于 application.properties
文件中特定的屬性值。
- @ConditionalOnProperty(name = “message.type”, havingValue = “sms”)
當(dāng) application.properties
或 application.yml
中定義的屬性 message.type
的值為 sms
時,此條件為 true
。此時, smsService()
方法將被調(diào)用,從而創(chuàng)建一個 SmsService
的 bean
。
- @ConditionalOnProperty(name = “message.type”, havingValue = “email”)
當(dāng) application.properties
或 application.yml
中定義的屬性 message.type
的值為 email
時,此條件為 true
。此時, emailService()
方法將被調(diào)用,從而創(chuàng)建一個 EmailService
的 bean
。
spring.factories文件:
在 src/main/resources/META-INF
目錄下創(chuàng)建一個 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)建了一個 MessageService
接口和兩個實(shí)現(xiàn)( SmsService
和 EmailService
)。然后,我們創(chuàng)建了一個自動配置類,其中包含兩個 bean
定義,這兩個 bean
定義分別基于 application.properties
中的屬性值條件性地創(chuàng)建。在 spring.factories
文件中,我們聲明了這個自動配置類,以便 Spring Boot
在啟動時能夠自動加載它。
在此,繼續(xù)用電視機(jī)的例子升華理解下
電視機(jī)類比
1.總體概念:
- 假設(shè)電視機(jī)(
TV
)是一個Java
應(yīng)用。 - 電視機(jī)的各種插槽,如
HDMI
、USB
、VGA
等,可以視為應(yīng)用中的SPI
接口。 - 插入這些插槽的設(shè)備(如
DVD
播放器、游戲機(jī)、USB
驅(qū)動器等)可以視為SPI
的實(shí)現(xiàn)。
2.Java的SPI:
- 當(dāng)我們購買電視機(jī)時,不知道將會連接哪種設(shè)備,可能是
DVD
播放器,也可能是游戲機(jī)。 - 但是,只要這些設(shè)備遵循了插槽的標(biāo)準(zhǔn)(例如,
HDMI
標(biāo)準(zhǔn)),就可以將其插入電視機(jī)并使其工作。 - 這就像
Java
的SPI
機(jī)制:為了能讓多個供應(yīng)商提供實(shí)現(xiàn),Java
定義了一個接口,供應(yīng)商提供具體的實(shí)現(xiàn)。
3.Spring Boot的自動配置:
- 現(xiàn)在,想象一下現(xiàn)代的智能電視。當(dāng)插入一個設(shè)備,電視機(jī)不僅可以識別它,還可能根據(jù)所連接的設(shè)備類型自動調(diào)整設(shè)置,例如選擇正確的輸入源、優(yōu)化圖像質(zhì)量等。
- 這就像
Spring Boot
的自動配置:當(dāng)Spring Boot
應(yīng)用啟動時,它會檢查classpath
上的庫,并根據(jù)存在的庫自動配置應(yīng)用。 - 電視機(jī)的自動設(shè)置可以類比為
Spring Boot
中的spring.factories
和各種@Conditional
…注解。它們決定在什么條件下進(jìn)行哪種配置。
4.擴(kuò)展性:
- 如果電視制造商想為新型的插槽或連接技術(shù)開發(fā)電視,它可以很容易地在其電視機(jī)型中添加新的插槽。
- 同樣地,使用
Spring Boot
,如果要為應(yīng)用添加新功能或庫,只需添加相關(guān)的依賴,然后Spring Boot
會自動識別并配置這些新功能。
通過這種類比,電視機(jī)的插槽和自動設(shè)置功能為我們提供了一個直觀的方式來理解 Java
的 SPI
機(jī)制和 Spring Boot
的自動配置如何工作,以及它們?nèi)绾螢閼?yīng)用開發(fā)者提供便利。
6. SPI(Service Provider Interface)總結(jié)
SPI
,即服務(wù)提供者接口,是一種特定的設(shè)計模式。它允許框架或核心庫為第三方開發(fā)者提供一個預(yù)定義的接口,從而使他們能夠?yàn)榭蚣芴峁┳远x的實(shí)現(xiàn)或擴(kuò)展。
核心目標(biāo):
- 解耦:
SPI
機(jī)制讓框架的核心與其擴(kuò)展部分保持解耦,使核心代碼不依賴于具體的實(shí)現(xiàn)。 - 動態(tài)加載:系統(tǒng)能夠通過特定的機(jī)制(如
Java
的ServiceLoader
)動態(tài)地發(fā)現(xiàn)和加載所需的實(shí)現(xiàn)。 - 靈活性:框架用戶可以根據(jù)自己的需求選擇、更換或增加新的實(shí)現(xiàn),而無需修改核心代碼。
可插拔:第三方提供的服務(wù)或?qū)崿F(xiàn)可以輕松地添加到或從系統(tǒng)中移除,無需更改現(xiàn)有的代碼結(jié)構(gòu)。
價值:
- 為框架或庫的用戶提供更多的自定義選項(xiàng)和靈活性。
- 允許框架的核心部分保持穩(wěn)定,同時能夠容納新的功能和擴(kuò)展。
SPI與“開閉原則”:
“開閉原則”提倡軟件實(shí)體應(yīng)該對擴(kuò)展開放,但對修改封閉。即在不改變現(xiàn)有代碼的前提下,通過擴(kuò)展來增加新的功能。
SPI如何體現(xiàn)“開閉原則”:
對擴(kuò)展開放: SPI
提供了一種標(biāo)準(zhǔn)化的方式,使第三方開發(fā)者可以為現(xiàn)有系統(tǒng)提供新的實(shí)現(xiàn)或功能。
對修改封閉:添加新的功能或特性時,原始框架或庫的代碼不需要進(jìn)行修改。
獨(dú)立發(fā)展:框架與其 SPI
實(shí)現(xiàn)可以獨(dú)立地進(jìn)化和發(fā)展,互不影響。
總之, SPI
是一種使軟件框架或庫更加模塊化、可擴(kuò)展和可維護(hù)的有效方法。通過遵循“開閉原則”, 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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis實(shí)現(xiàn)動態(tài)SQL的方法
動態(tài)SQL是MyBatis強(qiáng)大特性之一,極大的簡化我們拼裝SQL的操作,本文主要介紹了MyBatis實(shí)現(xiàn)動態(tài)SQL的方法,具有一定的參考價值,感興趣的可以了解一下2024-06-06使用Mybatis遇到的there is no getter異常
這篇文章主要介紹了使用Mybatis遇到的there is no getter異常,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09Spark Streaming算子開發(fā)實(shí)例
這篇文章主要介紹了Spark Streaming算子開發(fā)實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06只需兩步實(shí)現(xiàn)Eclipse+Maven快速構(gòu)建第一個Spring Boot項(xiàng)目
這篇文章主要介紹了只需兩步實(shí)現(xiàn)Eclipse+Maven快速構(gòu)建第一個Spring Boot項(xiàng)目,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12Springboot集成Mybatis-plus、ClickHouse實(shí)現(xiàn)增加數(shù)據(jù)、查詢數(shù)據(jù)功能
本文給大家講解Springboot + mybatis-plus 集成ClickHouse,實(shí)現(xiàn)增加數(shù)據(jù)、查詢數(shù)據(jù)功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-08-08