Java SPI在數(shù)據(jù)庫驅(qū)動、SpringBoot自動裝配中的應(yīng)用方式
1. 初識SPI
- Java SPI(Service Provider Interface)是Java提供的一種服務(wù)發(fā)現(xiàn)機(jī)制,允許第三方為某個接口或類庫提供實現(xiàn)。SPI的核心在于java.util.ServiceLoader類,它允許在運行時查找和加載服務(wù)的實現(xiàn)。
- 一般理解是:專門提供給服務(wù)提供者或者擴(kuò)展框架功能的開發(fā)者去使用的一個接口。
1.1 SPI的作用
- SPI 的合作作用:解耦。
- SPI 將服務(wù)接口和具體的服務(wù)實現(xiàn)分離開來,將服務(wù)調(diào)用方和服務(wù)實現(xiàn)者解耦,能夠提升程序的擴(kuò)展性、可維護(hù)性。修改或者替換服務(wù)實現(xiàn)并不需要修改調(diào)用方。
- 很多框架都使用了 Java 的 SPI 機(jī)制,比如:Spring 框架、數(shù)據(jù)庫加載驅(qū)動、日志接口、以及Dubbo 的擴(kuò)展實現(xiàn)等。
1.2 SPI的工作原理
- 定義接口:首先,在API中定義一個接口或者抽象類作為服務(wù)接口,所有服務(wù)提供者都需要按照這個接口來實現(xiàn)其功能。
- 創(chuàng)建配置文件:在jar包的META-INF/services目錄下,創(chuàng)建一個以服務(wù)接口全限定名命名的文本文件,該文件中每一行寫入一個實現(xiàn)了服務(wù)接口的具體類的全限定名。
- 服務(wù)發(fā)現(xiàn)與加載:當(dāng)應(yīng)用程序需要使用某項服務(wù)時,通過ServiceLoader.load(ServiceInterface.class)方法動態(tài)加載服務(wù)接口的所有實現(xiàn)類,并將其實例化。這樣,系統(tǒng)就可以在運行時根據(jù)不同的配置加載不同的服務(wù)實現(xiàn)。
- SPI常用于框架擴(kuò)展、插件機(jī)制以及模塊間解耦等場景,使得系統(tǒng)的可擴(kuò)展性和靈活性大大增強(qiáng)。例如,數(shù)據(jù)庫驅(qū)動的加載就是Java SPI的一個典型應(yīng)用案例。
對于 Java 原生 SPI,只需要滿足下面幾個條件:
- 定義服務(wù)的通用接口,針對通用的服務(wù)接口,提供具體的實現(xiàn)類
- 在 src/main/resources/META-INF/services 或者 jar包的 META-INF/services/ 目錄中,新建一個文件,文件名為 接口的全名。文件內(nèi)容為該接口的具體實現(xiàn)類的全名
- 將 spi 所在 jar 放在主程序的 classpath 中
- 服務(wù)調(diào)用方用java.util.ServiceLoader,用服務(wù)接口為參數(shù),去動態(tài)加載具體的實現(xiàn)類到JVM中,然后就可以正常使用服務(wù)了
1.3 SPI的三個組件:Service、Service Provider、ServiceLoader
SPI其實是一種思想:約定大于配置
簡單來說就是:jdk提供一個公共API,具體的實現(xiàn)由具體的應(yīng)用程序自己完成,并約定一套規(guī)則來讓java的類加載機(jī)制發(fā)現(xiàn)實現(xiàn)類所在位置
通過這個約定,就不需要把服務(wù)放在代碼中了,通過模塊被裝配的時候就可以發(fā)現(xiàn)服務(wù)類了
Service:是一個公開的接口或抽象類,定義了一個抽象的功能模塊(文件名稱)Service Provider:是Service的實現(xiàn)類(文件內(nèi)容)ServiceLoader(java.util包下):是SPI機(jī)制中的核心組件,負(fù)責(zé)在運行時發(fā)現(xiàn)并加載Service Provider

1.4 SPI使用場景
很多開源第三方j(luò)ar包都有基于SPI的實現(xiàn),在jar包META-INF/services中都有相關(guān)配置文件。
如下幾個常見的場景:
- JDBC加載不同類型的數(shù)據(jù)庫驅(qū)動
- Slf4j日志框架
- Dubbo框架
看看 Dubbo 的擴(kuò)展實現(xiàn),就知道 SPI 機(jī)制用的多么廣泛:

1.5 具體的SPI 源碼分析(SPI的核心就是ServiceLoader.load()方法)
- 調(diào)用ServiceLoader.load(),創(chuàng)建一個ServiceLoader實例對象
- 創(chuàng)建LazyIterator實例對象lookupIterator
- 通過lookupIterator.hasNextService()方法讀取固定目錄META-INF/services/下面service全限定名文件,放在Enumeration對象configs中
- 解析configs得到迭代器對象Iterator pending
- 通過lookupIterator.nextService()方法初始化讀取到的實現(xiàn)類,通過Class.forName()初始化
從上面的步驟可以總結(jié)以下兩點:
- 實現(xiàn)類工程必須創(chuàng)建定目錄META-INF/services/,并創(chuàng)建service全限定名文件,文件內(nèi)容是實現(xiàn)類全限定名
- 實現(xiàn)類必須有一個無參構(gòu)造函數(shù)
1.6 SPI 的優(yōu)缺點
優(yōu)點:
- 通過 SPI 機(jī)制能夠大大地提高接口設(shè)計的靈活性,可以另起jar實現(xiàn),不必寫在一個模塊中
缺點:
- 需要遍歷加載所有的實現(xiàn)類,不能做到按需加載,這樣效率還是相對較低的。
- 當(dāng)多個 ServiceLoader 同時 load 時,會有并發(fā)問題。
- SPI 缺少實例的維護(hù),作用域沒有定義singleton和prototype的定義,不利于用戶自由定制。
- ServiceLoader不像 Spring,只能一次獲取所有的接口實例, 不支持排序,隨著新的實例加入,會出現(xiàn)排序不穩(wěn)定的情況,作用域沒有定- 義singleton和prototype的定義,不利于用戶自由定制
2. API、SPI、JNDI釋義
- API(Application Programming Interface)就是接口,比如java.util.List 是一個接口,是一個API
- SPI(Service Provider Interface)是一種思想
- JNDI(Java Naming and Directory Interface)
是Java平臺中的一種標(biāo)準(zhǔn)服務(wù),它提供了一組API,允許Java應(yīng)用程序查找和訪問命名以及目錄服務(wù)。
這個接口的設(shè)計目的是統(tǒng)一不同命名服務(wù)和目錄服務(wù)的訪問方式,使得開發(fā)人員無需關(guān)注具體的底層實現(xiàn)細(xì)節(jié),就能在分布式環(huán)境中查找、綁定和管理資源。
通過JNDI,開發(fā)者可以:
- 命名:將對象與名稱關(guān)聯(lián)起來存儲,并可以通過名稱來獲取這些對象。
- 目錄服務(wù):除了基本的命名功能外,還支持屬性查詢,即根據(jù)對象的屬性信息進(jìn)行檢索。目錄服務(wù)中的條目類似于文件系統(tǒng)中的文件,每個條目都有名稱,并且可以攜帶多個屬性。
JNDI常用于以下場景:
- 在Java EE應(yīng)用服務(wù)器中,配置數(shù)據(jù)源(DataSource)、EJB引用、JMS隊列和主題等資源,并通過統(tǒng)一的名字查找和使用它們。
- 在企業(yè)級應(yīng)用中,定位網(wǎng)絡(luò)服務(wù)的位置或?qū)傩裕鏛DAP(輕量級目錄訪問協(xié)議)目錄服務(wù)中的用戶賬戶信息。
- 簡而言之,JNDI是一個抽象層,它隱藏了各種各樣的命名和目錄服務(wù)的具體實現(xiàn)細(xì)節(jié),為Java程序提供了統(tǒng)一、靈活且易于使用的編程接口。
3. SPI應(yīng)用舉例1:加載數(shù)據(jù)庫驅(qū)動
依賴引用:
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>21.9.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>SPI是JDK內(nèi)置的一種動態(tài)擴(kuò)展點的實現(xiàn)。
簡單來說,就是我們可以定義一個標(biāo)準(zhǔn)的接口,然后第三方的庫里面可以實現(xiàn)這個接口。
那么,程序在運行的時候,會根據(jù)配置信息動態(tài)加載第三方實現(xiàn)的類,從而完成功能的動態(tài)擴(kuò)展機(jī)制。
- 解釋圖1:

- 解釋圖2:

在Java里面,SPI機(jī)制有一個非常典型的實現(xiàn)案例,就是數(shù)據(jù)庫驅(qū)動java.jdbc.Driver,JDK里面定義了數(shù)據(jù)庫驅(qū)動類Driver,它是一個接口,JDK并沒有提供實現(xiàn)。
具體的實現(xiàn)是由第三方數(shù)據(jù)庫廠商來完成的。在程序運行的時候,會根據(jù)我們聲明的驅(qū)動類型,來動態(tài)加載對應(yīng)的擴(kuò)展實現(xiàn),從而完成數(shù)據(jù)庫的連接。

除此之外,在很多開源框架里面都借鑒了Java SPI的思想,提供了自己的SPI框架,比如Dubbo定義了ExtensionLoader,實現(xiàn)功能的擴(kuò)展。Spring提供了SpringFactoriesLoader,實現(xiàn)外部功能的集成。
當(dāng)服務(wù)的提供者,提供了服務(wù)接口的一種實現(xiàn)之后,在jar包的META-INF/services/目錄里同時創(chuàng)建一個以服務(wù)接口命名的文件。該文件里就是實現(xiàn)該服務(wù)接口的具體實現(xiàn)類。而當(dāng)外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/里的配置文件找到具體的實現(xiàn)類名,并裝載實例化,完成模塊的注入。通過這個約定,就不需要把服務(wù)放在代碼中了,通過模塊被裝配的時候就可以發(fā)現(xiàn)服務(wù)類了。
3.1 Jdbc連接數(shù)據(jù)庫示例代碼
有了SPI機(jī)制之后,Class.forName(“com.mysql.jdbc.Driver”);這條語句就不需要了,
java.util.ServiceLoader會負(fù)責(zé)到j(luò)ar包的META-INF/services/java.sql.Driver中獲取具體驅(qū)動實現(xiàn)類
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcDriverManagerDemo {
static final String MYSQL_JDBC_DRIVER = "com.mysql.cj.jdbc.Driver";
private static final String MYSQL_URL = "jdbc:mysql://localhost:3306/lelele?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASSWORD = "admin123";
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 有了SPI機(jī)制之后,這條語句就不要了,
// java.util.ServiceLoader會負(fù)責(zé)到j(luò)ar包的META-INF/services/java.sql.Driver中獲取具體驅(qū)動實現(xiàn)類
// Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(MYSQL_URL, USER, PASSWORD);
ps = conn.prepareStatement("select * from xin_stu_t_bak");
rs = ps.executeQuery();
while (rs.next()) {
System.out.println(rs.getInt("id"));
}
// 處理查詢結(jié)果
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
System.out.println("ID: " + id + ", Name: " + name);
}
} catch (SQLException se) {
// 處理SQL錯誤
se.printStackTrace();
} catch (Exception e) {
// 處理其他異常
e.printStackTrace();
} finally {
// 關(guān)閉資源,確保在任何情況下都能正確關(guān)閉
try {
if (rs != null)
rs.close();
if (ps != null)
ps.close();
if (conn != null)
conn.close();
} catch (SQLException se2) {
se2.printStackTrace();
}
}
System.out.println("end!");
}
}3.2 為什么獲取連接前不用獲取驅(qū)動了,是怎么獲取到驅(qū)動的?
源碼分析參照 本文章1.5節(jié) 具體的SPI 源碼分析(SPI的核心就是ServiceLoader.load()方法)
下面是獲取連接對象代碼調(diào)試中的一些斷點截圖
- 3.2.1 程序開始獲取連接

- 3.2.2 DriverManager開始初始化驅(qū)動

- 3.2.3 調(diào)用ServiceLoader來發(fā)現(xiàn)驅(qū)動,使用類加載器加載jar中META-INF/services/下資源文件中的實現(xiàn)類
使用線程上下文類加載器(ContextClassLoader)加載
如果不做任何的設(shè)置,Java應(yīng)用的線程的上下文類加載器默認(rèn)就是AppClassLoader。在核心類庫使用SPI接口時,傳遞的類加載器使用線程上下文類加載器,就可以成功的加載到SPI實現(xiàn)的類。線程上下文類加載器在很多SPI的實現(xiàn)中都會用到。通常我們可以通過Thread.currentThread().getClassLoader()和Thread.currentThread().getContextClassLoader()獲取線程上下文類加載器。


- 3.2.3.1 oracle驅(qū)動具體實現(xiàn)類oracle.jdbc.OracleDriver,實現(xiàn)了java.sql.Driver

- 3.2.3.2 mysql驅(qū)動具體實現(xiàn)類com.mysql.cj.jdbc.Driver,實現(xiàn)了java.sql.Driver

4. SPI應(yīng)用舉例2:SpringBoot自動裝配
4.1 SpringBoot自動裝配借鑒了SPI思想
Spring Factories自動裝配借用了SPI機(jī)制,區(qū)別如圖:

Spring Boot的自動裝配機(jī)制確實與SPI(Service Provider Interface)有關(guān)聯(lián),但并不是直接使用Java標(biāo)準(zhǔn)SPI來實現(xiàn)自動裝配。Spring Boot借鑒了SPI的思想,在其內(nèi)部設(shè)計了一套更加靈活和強(qiáng)大的自動配置體系。
在Spring Boot中,META-INF/spring.factories文件的使用方式類似于SPI的服務(wù)提供者配置文件(META-INF/services/下資源文件),它允許jar包聲明自己提供的自動配置類。這些自動配置類通過條件注解(如@ConditionalOnClass、@ConditionalOnBean等)來檢查應(yīng)用環(huán)境并決定是否生效,從而實現(xiàn)了根據(jù)項目依賴和運行時環(huán)境進(jìn)行自動裝配的功能。
因此,雖然Spring Boot沒有完全采用Java SPI的標(biāo)準(zhǔn)流程,但其自動裝配過程中對第三方庫和服務(wù)的發(fā)現(xiàn)和加載機(jī)制受到了SPI思想的啟發(fā),并在此基礎(chǔ)上進(jìn)行了擴(kuò)展和創(chuàng)新。

另:
META-IF/spring.factories是在Maven引入的Jar包中,每一個Jar都有自己META-IF/spring.factories,所以SpringBoot是去每一個Jar包里面尋找META-IF/spring.factories,而不是我的項目中存在META-IF/spring.factories(當(dāng)然也可以存在,但是我項目的META-IF/spring.factories肯定沒有類似以下這些東西)

4.2 Spring Boot 的自動裝配(Auto-configuration)原理
@SpringBootApplication注解作用圖:

好處:簡化了配置
Springboot的SPI機(jī)制是怎么實現(xiàn)的?
- 程序啟動,注冊配置類處理器
- Spring刷新上下文,執(zhí)行配置類處理器
- 掃描spring.factories將得到的BeanDefinition注冊到容器
- spring實例化/初始化這些BeanDefinition
Spring Boot 的自動裝配(Auto-configuration)原理(上面也是)
啟動類與@SpringBootApplication注解:
- Spring Boot應(yīng)用通常包含一個主入口類,上面會標(biāo)注@SpringBootApplication注解。
- 這個注解包含了@Configuration、@EnableAutoConfiguration和@ComponentScan三個注解
- 其中@EnableAutoConfiguration是開啟自動裝配的關(guān)鍵。
@EnableAutoConfiguration:
- @EnableAutoConfiguration注解指示Spring Boot根據(jù)項目的classpath和已存在的Bean來自動配置應(yīng)用程序。
- 它會掃描項目依賴中所有jar包的META-INF/spring.factories文件,這些文件定義了哪些自動配置類應(yīng)該被應(yīng)用到當(dāng)前項目中。
spring.factories中的自動配置類:
- 各個Spring Boot Starter模塊在它們的jar包里都提供了一個spring.factories文件
- 該文件中列出了一系列org.springframework.boot.autoconfigure.EnableAutoConfiguration對應(yīng)的實現(xiàn)類全名。

條件化裝配:
- 自動配置類本身使用了Spring Boot提供的條件化裝配機(jī)制,例如@ConditionalOnClass、@ConditionalOnMissingBean等注解。
- 這意味著只有當(dāng)滿足特定條件時(如類路徑中有某個類、沒有用戶自定義的同類型Bean等),才會實例化并注冊相應(yīng)的自動配置Bean。
覆蓋默認(rèn)配置:
- 如果開發(fā)者想要自定義某部分配置或者完全禁用某個自動配置,可以通過創(chuàng)建自己的配置類或在application.properties/application.yml中設(shè)置屬性值來實現(xiàn)。
- Spring Boot會優(yōu)先考慮用戶自己定義的Bean或?qū)傩耘渲?,從而實現(xiàn)了靈活的擴(kuò)展和配置覆蓋。
自定義自動配置:
- 開發(fā)者還可以編寫自己的自動配置類,通過添加@Configuration注解,并利用條件裝配注解來自定義特定場景下的自動化配置邏輯。
另:META-INF下另一個文件MANIFEST.MF的作用
MANIFEST.MF是Java應(yīng)用程序和JAR文件中用于存儲元數(shù)據(jù)(metadata)的一個重要文件。它位于JAR文件的META-INF目錄下,包含了關(guān)于該JAR包及其組件的重要信息。MANIFEST.MF是Java應(yīng)用打包和部署過程中不可或缺的一部分,為Java虛擬機(jī)(JVM)提供了有關(guān)如何加載和執(zhí)行JAR內(nèi)容的關(guān)鍵指導(dǎo)信息。
在JAR文件中,MANIFEST.MF 文件常見的用途包括:
Main-Class聲明:對于可執(zhí)行的JAR文件(也稱為Runnable JAR或Self-executable JAR),需要在MANIFEST.MF文件中指定一個主類(Main-Class)。例如:
Main-Class: com.example.Main
這樣,用戶可以直接通過命令行 java -jar myapp.jar 來運行這個JAR程序。
Class-Path聲明:用于定義當(dāng)前JAR文件依賴的其他外部JAR文件的路徑。例如:
Class-Path: lib/library.jar lib/anotherlibrary.jar
這樣,當(dāng)運行此JAR時,Java會自動加載指定路徑下的庫。
Sealed指示:可以用來表示JAR是否被密封(sealed),即所有包內(nèi)的類都必須來自同一個代碼源,以增強(qiáng)安全性。
簽名信息:如果JAR文件被數(shù)字簽名,那么相關(guān)的簽名證書、簽名者信息等也會記錄在MANIFEST.MF中。
服務(wù)提供者信息:在實現(xiàn)Java SPI(Service Provider Interface)時,MANIFEST.MF也可以包含ServiceProvider-Impl條目,列出實現(xiàn)了某個接口的服務(wù)提供者的全限定類名。
OSGi Bundle信息:在OSGi框架中,每個Bundle都有自己的MANIFEST.MF文件,其中包含了Bundle的標(biāo)識符、版本、導(dǎo)入導(dǎo)出的包、激活器等重要信息。

5. Spring Boot Starter


- Spring Boot Starter 是 Spring Boot 框架中的一種模塊化設(shè)計,旨在簡化項目的初始化和依賴管理。它們是一組預(yù)配置的依賴項集合,每個Starter都針對特定的功能場景,例如數(shù)據(jù)訪問、Web服務(wù)、安全控制等。
- Spring Boot Starter通過預(yù)先集成并配置好了一系列功能組件,為開發(fā)者快速搭建應(yīng)用提供了極大便利。
使用Spring Boot Starter的主要優(yōu)點包括:
- 統(tǒng)一入口:只需要在Maven或Gradle構(gòu)建文件中引入一個Starter依賴,就可以自動獲得一組相關(guān)的庫以及對應(yīng)的默認(rèn)配置。
- 約定優(yōu)于配置:Starter包含了基于最佳實踐的默認(rèn)配置,極大地減少了開發(fā)者在項目啟動階段需要編寫的配置代碼量。
- 易用性與一致性:無論開發(fā)何種功能的應(yīng)用程序,都可以遵循相同的模式來引入依賴,使得團(tuán)隊成員易于理解和操作。
- 減少版本沖突:由于Starter中的所有依賴版本都已經(jīng)協(xié)調(diào)一致,因此可以避免不同組件之間的版本不兼容問題。
例如,spring-boot-starter-web 包含了開發(fā)Web應(yīng)用程序所需的所有基本依賴,如Spring MVC、Tomcat服務(wù)器等;而 spring-boot-starter-data-jpa 則包含了使用JPA進(jìn)行數(shù)據(jù)庫持久化的相關(guān)依賴。
6. 只有Spring Boot Starter有spring.factories文件嗎
并非只有Spring Boot Starter才有spring.factories文件。spring.factories 文件是Spring Boot框架為了實現(xiàn)自動配置(Auto-Configuration)和SPI(Service Provider Interface)機(jī)制而使用的一種約定。雖然它通常與Spring Boot Starter一起使用,但并非Spring Boot Starter特有的。
在任何Java項目中,只要符合Spring Boot的自動裝配規(guī)則,都可以創(chuàng)建自己的META-INF/spring.factories文件來聲明自定義的自動配置類或者其他擴(kuò)展點。這意味著即使不是官方提供的Spring Boot Starter模塊,開發(fā)者也可以編寫自己的模塊,并在其jar包中包含spring.factories文件來提供額外的自動配置服務(wù)。
所以,spring.factories文件不僅限于Spring Boot Starter,而是所有遵循Spring Boot規(guī)范并希望通過這種方式進(jìn)行擴(kuò)展的模塊都可能包含的一個核心配置文件。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java Bean與Map之間相互轉(zhuǎn)化的實現(xiàn)方法
這篇文章主要介紹了Java Bean與Map之間相互轉(zhuǎn)化的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
Java求兩個正整數(shù)的最大公約數(shù)和最小公倍數(shù)
這篇文章主要介紹了輸入兩個正整數(shù)m和n,求其最大公約數(shù)和最小公倍數(shù),需要的朋友可以參考下2017-02-02
Java中將File轉(zhuǎn)化為MultipartFile的操作
這篇文章主要介紹了Java中將File轉(zhuǎn)化為MultipartFile的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
從dubbo zookeeper注冊地址提取出zookeeper地址的方法
今天小編就為大家分享一篇關(guān)于從dubbo zookeeper注冊地址提取出zookeeper地址的方法,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12

