深入淺出:SPI機制在JDK與Spring Boot中的應用

2023-09-15 12:00:47

本文分享自華為雲社群《Spring高手之路14——深入淺出:SPI機制在JDK與Spring Boot中的應用》,作者:磚業洋__ 。

Spring Boot不僅是簡化Spring應用開發的工具,它還融合了許多先進的機制。本文深入探討了Spring Boot中與Java的標準SPI相似的機制,揭示了它的工作原理、應用場景及與標準SPI的異同。文章通過實際程式碼範例為你展示瞭如何在Spring Boot中使用這一機制,並以形象的比喻幫助你理解其背後的思想。

1. SPI解讀:什麼是SPI?

SPI (Service Provider Interface) 是一種服務發現機制,它允許第三方提供者為核心庫或主框架提供實現或擴充套件。這種設計允許核心庫/框架在不修改自身程式碼的情況下,通過第三方實現來增強功能。

JDK原生的SPI:
  • 定義和發現:JDKSPI主要通過在META-INF/services/目錄下放置特定的檔案來指定哪些類實現了給定的服務介面。這些檔案的名稱應為介面的全限定名,內容為實現該介面的全限定類名。

  • 載入機制:ServiceLoader類使用Java的類載入器機制從META-INF/services/目錄下載入和範例化服務提供者。例如,ServiceLoader.load(MyServiceInterface.class)會返回一個實現了MyServiceInterface的範例迭代器。

  • 缺點:JDK原生的SPI每次通過ServiceLoader載入時都會初始化一個新的範例,沒有實現類的快取,也沒有考慮單例等高階功能。

Spring的SPI:
  • 更加靈活:SpringSPI不僅僅是服務發現,它提供了一套完整的外掛機制。例如,可以為Spring定義新的PropertySourceApplicationContextInitializer等。

  • 與IoC整合:與JDKSPI不同,SpringSPI與其IoC (Inversion of Control) 容器整合,使得在SPI實現中可以利用Spring的全部功能,如依賴注入。

  • 條件匹配:Spring提供了基於條件的匹配機制,這允許在某些條件下只載入特定的SPI實現,例如,可以基於當前執行環境的不同來選擇載入哪個資料庫驅動。

  • 設定:Spring允許通過spring.factories檔案在META-INF目錄下進行設定,這與JDKSPI很相似,但它提供了更多的功能和靈活性。

舉個類比的例子:

想象我們正在建造一個電視機,SPI就像電視機上的一個USB插口。這個插口可以插入各種裝置(例如U盤、遊戲手柄、電視棒等),但我們並不關心這些裝置的內部工作方式。這樣只需要提供一個標準的介面,其他公司(例如U盤製造商)可以為此介面提供實現。這樣,電視機可以在不更改自己內部程式碼的情況下使用各種新裝置,而裝置製造商也可以為各種電視機制造相容的裝置。

總之,SPI是一種將介面定義與實現分離的設計模式,它鼓勵第三方為一個核心產品或框架提供外掛或實現,從而使核心產品能夠輕鬆地擴充套件功能。

2. SPI在JDK中的應用範例

Java的生態系統中,SPI 是一個核心概念,允許開發者提供擴充套件和替代的實現,而核心庫或應用不必更改,下面舉出一個例子來說明。

全部程式碼和步驟如下:

步驟1:定義一個服務介面,檔名: MessageService.java

package com.example.demo.service;

public interface MessageService {
    String getMessage();
}

步驟2:為服務介面提供實現,這裡會提供兩個簡單的實現類。

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!";
    }
}

這些實現就像不同品牌或型號的U盤或其他USB裝置。每個裝置都有自己的功能和特性,但都遵循相同的USB標準。

步驟3:註冊服務提供者

在資源目錄(通常是src/main/resources/)下建立一個名為META-INF/services/的資料夾。在這個資料夾中,建立一個名為com.example.demo.service.MessageService的檔案(這是我們介面的全限定名),這個檔案沒有任何副檔名,所以不要加上.txt這樣的字尾。檔案的內容應為我們的兩個實現類的全限定名,每個名字佔一行:

com.example.demo.service.HelloMessageService
com.example.demo.service.HiMessageService

META-INF/services/ 是 Java SPI (Service Provider Interface) 機制中約定俗成的特定目錄。它不是隨意選擇的,而是 SPI 規範中明確定義的。因此,當使用 JDK 的 ServiceLoader 類來載入服務提供者時,它會特意去查詢這個路徑下的檔案。

請確保檔案的每一行只有一個名稱,並且沒有額外的空格或隱藏的字元,檔案使用UTF-8編碼。

步驟4:使用ServiceLoader載入和使用服務

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());
        }
    }
}

執行結果如下:

這說明ServiceLoader成功地載入了我們為MessageService介面提供的兩個實現,並且我們可以在不修改Main類的程式碼的情況下,通過新增更多的實現類和更新META-INF/services/com.example.MessageService檔案來擴充套件我們的服務。

想象一下買了一臺高階的智慧電視,這臺電視上有一個或多個HDMI埠,這就是它與外部裝置連線的介面。

  • 定義服務介面:這就像電視定義了HDMI埠的標準。在上面的程式碼中,MessageService介面就是這個「HDMI埠」,定義瞭如何與外部裝置交流。
  • 為服務介面提供實現:這類似於製造商為HDMI介面生產各種裝置,如遊戲機、藍光播放器或串流媒體棒。在程式碼中,HelloMessageServiceHiMessageService就是這些「HDMI裝置」。每個裝置/實現都有其獨特的輸出,但都遵循了統一的HDMI標準(MessageService介面)。

  • 註冊服務提供者:當我們購買了一個HDMI裝置,它通常都會在包裝盒上明確標明「適用於HDMI」。這就像一個標識,告訴使用者它可以連線到任何帶有HDMI介面的電視。在SPI的例子中,META-INF/services/目錄和其中的檔案就像這個「標籤」,告訴JDK哪些類是MessageService的實現。

  • 使用ServiceLoader載入和使用服務:當插入一個HDMI裝置到電視上,並切換到正確的輸入頻道,電視就會顯示該裝置的內容。類似地,在程式碼的這個步驟中,ServiceLoader就像電視的輸入選擇功能,能夠發現和使用所有已連線的HDMI裝置(即MessageService的所有實現)。

3. SPI在Spring框架中的應用

Spring官方在其檔案和原始碼中多次提到了SPIService Provider Interface)的概念。但是,當我們說「SpringSPI」時,通常指的是Spring框架為開發者提供的一套可延伸的介面和抽象類,開發者可以基於這些介面和抽象類實現自己的版本。

Spring中,SPI的概念與Spring Boot使用的spring.factories檔案的機制不完全一樣,但是它們都體現了可插拔、可延伸的思想。

Spring的SPI:
  • Spring的核心框架提供了很多介面和抽象類,如BeanPostProcessorPropertySourceApplicationContextInitializer等,這些都可以看作是SpringSPI。開發者可以實現這些介面來擴充套件Spring的功能。這些介面允許開發者在Spring容器的生命週期的不同階段介入,實現自己的邏輯。
Spring Boot的spring.factories機制:
  • spring.factoriesSpring Boot的一個特性,允許開發者自定義自動設定。通過spring.factories檔案,開發者可以定義自己的自動設定類,這些類在Spring Boot啟動時會被自動載入。

  • 在這種情況下,SpringFactoriesLoader的使用,尤其是通過spring.factories檔案來載入和範例化定義的類,可以看作是一種特定的SPI實現方式,但它特定於Spring Boot

3.1 傳統Spring框架中的SPI思想

在傳統的Spring框架中,雖然沒有直接使用名為"SPI"的術語,但其核心思想仍然存在。Spring提供了多個擴充套件點,其中最具代表性的就是BeanPostProcessor。在本節中,我們將通過一個簡單的MessageService介面及其實現來探討如何利用SpringBeanPostProcessor擴充套件點體現SPI的思想。

提供兩個簡單的實現類。

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();
    }
}

執行程式

使用之前提供的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());
    }
}

執行結果:

現在,每一個MessageService實現都被BeanPostProcessor處理了,新增了額外的訊息「[Processed by Spring SPI]」。這演示了SpringSPI概念,通過BeanPostProcessor來擴充套件或修改Spring容器中的bean

有人可能留意到這裡紅色的警告,這個之前在講BeanPostProcessor的時候也提到過,當BeanPostProcessor自身被一個或多個BeanPostProcessor處理時,就會出現這種情況。簡單地說,由於BeanPostProcessor需要在其他bean之前初始化,所以某些BeanPostProcessor無法處理早期初始化的bean,包括設定類和其他BeanPostProcessor。解決辦法就是不要把MessageServicePostProcessor放在設定類初始化,在設定類刪掉,再把MessageServicePostProcessor加上@Component註解。

類比文章開頭的電視機的例子:

  • 電視機與USB插口: 在這個新的範例中,電視機仍然是核心的Spring應用程式,具體來說是DemoApplication類。這個核心應用程式需要從某個服務(即MessageService)獲取並列印一條訊息。
  • USB插口: 與之前一樣,MessageService介面就是這個"USB插口"。它為電視機提供了一個標準化的介面,即getMessage()方法,但沒有規定具體怎麼實現。
  • 裝置製造商與他們的產品: 在這裡,我們有兩種裝置製造商或第三方提供者:HelloMessageServiceHiMessageService。它們為"USB插口"(即MessageService介面)提供了不同的裝置或實現。一個顯示「Hello from HelloMessageService!」,另一個顯示「Hi from HiMessageService!」
  • BeanPostProcessor: 這是一個特殊的「魔法盒子」,可以將其視為一個能夠攔截並修改電視機顯示內容的智慧裝置。當插入USB裝置(即MessageService的實現)並嘗試從中獲取訊息時,這個「魔法盒子」會介入,併為每條訊息新增「[Processed by Spring SPI]」
  • Spring上下文設定: 這依然是電視機的使用說明書,但現在是使用了基於Java的設定方式,即MessageServiceConfig類。這個「使用說明書」指導Spring容器如何建立並管理MessageService的範例,並且還指導它如何使用「魔法盒子」(即MessageServicePostProcessor)來處理訊息。

總的來說,與之前的例子相比,這個新範例提供了一個更加動態的場景,其中SpringBeanPostProcessor擴充套件點允許我們攔截並修改bean的行為,就像一個能夠干預並改變電視機顯示內容的智慧裝置。

3.2 Spring Boot中的SPI思想

Spring Boot有一個與SPI相似的機制,但它並不完全等同於Java的標準SPI

Spring Boot的自動設定機制主要依賴於spring.factories檔案。這個檔案可以在多個jar中存在,並且Spring Boot會載入所有可見的spring.factories檔案。我們可以在這個檔案中宣告一系列的自動設定類,這樣當滿足某些條件時,這些設定類會自動被Spring Boot應用。

接下來會展示Spring SPI思想的好例子,但是它與Spring Boot緊密相關。

定義介面

package com.example.demo.service;

public interface MessageService {
    String getMessage();
}

這裡會提供兩個簡單的實現類。

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!";
    }
}

註冊服務

resources/META-INF下建立一個檔名為spring.factories。這個檔案裡,可以註冊MessageService實現類。

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是實現類的全路徑。如果有多個實現類,它們應當用逗號分隔。

spring.factories檔案中的條目鍵和值之間不能有換行,即key=value形式的結構必須在同一行開始。但是,如果有多個值需要列出(如多個實現類),並且這些值是逗號分隔的,那麼可以使用反斜槓(\)來換行。spring.factories 的名稱是約定俗成的。如果試圖使用一個不同的檔名,那麼 Spring Boot 的自動設定機制將不會識別它。

這裡spring.factories又可以寫為

com.example.demo.service.MessageService=com.example.demo.service.HelloMessageService,\
  com.example.demo.service.HiMessageService

直接在逗號後面回車IDEA會自動補全反斜槓,保證鍵和值之間不能有換行即可。

使用SpringFactoriesLoader來載入服務

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的第二個引數是類載入器,此處我們使用預設的類載入器,所以傳遞null

執行結果:

這種方式利用了SpringSpringFactoriesLoader,它允許開發者提供介面的多種實現,並通過spring.factories檔案來註冊它們。這與JDKSPI思想非常相似,只是在實現細節上有所不同。這也是Spring Boot如何自動設定的基礎,它會查詢各種spring.factories檔案,根據其中定義的類來初始化和設定bean

我們繼續使用電視機的例子來解釋:

  • 電視機: 這是我們的Spring應用,就像DemoApplication。電視機是檢視不同訊號源或通道的裝置,我們的應用程式是為了執行並使用不同的服務實現。
  • USB插口: 這代表我們的MessageService介面。USB插口是一個標準的介面,它允許連線各種裝置,就像MessageService介面允許有多種實現方式。
  • USB裝置(如U盤或行動硬碟): 這代表我們的服務實現,例如HelloMessageServiceHiMessageService。每個USB裝置在插入電視機後都有特定的內容或功能,這就像我們的每個服務實現返回不同的訊息。
  • 電視機的USB裝置目錄: 這是spring.factories檔案。當我們將USB裝置插入電視機時,電視機會檢查裝置的資訊或內容,spring.factories檔案告訴Spring Boot哪些服務實現是可用的,就像電視機知道有哪些USB裝置被插入。
  • 電視機的USB掃描功能: 這就是SpringFactoriesLoader。當我們要從電視機上檢視USB內容時,電視機會掃描並顯示內容。同樣,當DemoApplication執行時,SpringFactoriesLoader會查詢並載入在spring.factories檔案中列出的服務實現。

簡化解釋:

  • 當插入USB裝置到電視機,期望電視機能夠識別並顯示該裝置的內容。

  • 在我們的例子中,USB裝置的內容就是從MessageService實現類返回的訊息。

  • spring.factories檔案就像電視機的內建目錄,告訴電視機哪些USB裝置是已知的和可以使用的。

  • 當我們的DemoApplication(電視機)執行時,它使用SpringFactoriesLoaderUSB掃描功能)來檢查哪些服務(USB裝置)是可用的,並輸出相應的訊息(顯示USB內容)。

  總結:在這個Spring BootSPI例子中,我們展示了核心Spring應用如何自動地識別和使用spring.factories檔案中註冊的實現,這與電視機自動地識別和使用所有插入的USB裝置有相似之處。

4. SPI在JDBC驅動載入中的應用

資料庫驅動的SPI主要體現在JDBC驅動的自動發現機制中。JDBC 4.0 引入了一個特性,允許驅動自動註冊到DriverManager。這是通過使用JavaSPI來實現的。驅動jar包內會有一個META-INF/services/java.sql.Driver檔案,此檔案中包含了該驅動的Driver實現類的全類名。這樣,當類路徑中有JDBC驅動的jar檔案時,Java應用程式可以自動發現並載入JDBC驅動,而無需明確地載入驅動類。

這意味著任何資料庫供應商都可以編寫其自己的JDBC驅動程式,只要它遵循JDBC驅動程式的SPI,它就可以被任何使用JDBCJava應用程式所使用。

當我們使用DriverManager.getConnection()獲取資料庫連線時,背後正是利用SPI機制載入合適的驅動程式。

以下是SPI機制的具體工作方式:

定義服務介面:

在這裡,介面已經由Java平臺定義,即java.sql.Driver

為介面提供實現:

各巨量資料庫廠商(如OracleMySQLPostgreSQL等)為其資料庫提供了JDBC驅動程式,它們都實現了java.sql.Driver介面。例如,MySQL的驅動程式中有一個類似於以下的類:

public class com.mysql.cj.jdbc.Driver implements java.sql.Driver {
    // 實現介面方法...
}

直接上圖:

註冊服務提供者:

對於MySQL的驅動程式,可以在其JAR檔案的META-INF/services目錄下找到一個名為java.sql.Driver的檔案,檔案內容如下:

com.mysql.cj.jdbc.Driver

直接上圖:

看到這裡是不是發現和第2節舉的JDK SPI的例子一樣?體會一下。

使用SPI來載入和使用服務:

當我們呼叫DriverManager.getConnection(jdbcUrl, username, password)時,DriverManager會使用ServiceLoader來查詢所有已註冊的java.sql.Driver實現。然後,它會嘗試每一個驅動程式,直到找到一個可以處理給定jdbcUrl的驅動程式。

以下是一個簡單的範例,展示如何使用JDBC SPI獲取資料庫連線:

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驅動程式,因為DriverManager會自動為我們選擇合適的驅動程式。

這種模組化和外掛化的機制使得我們可以輕鬆地為不同的資料庫切換驅動程式,只需要更改JDBC URL並確保相應的驅動程式JAR在類路徑上即可。

Spring Boot中,開發者通常不會直接與JDBCSPI機制互動來獲取資料庫連線。Spring Boot的自動設定機制隱藏了許多底層細節,使得設定和使用資料庫變得更加簡單。

一般會在application.propertiesapplication.yml中設定資料庫連線資訊。

例如:

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的自動設定機制會根據提供的依賴和設定資訊來初始化和設定DataSource物件,這個物件管理資料庫連線。實際上,新增JDBC驅動依賴時,Spring Boot會使用JDKSPI機制(在JDBC規範中應用)來找到並載入相應的資料庫驅動。開發者雖然不直接與JDKSPI互動,但在背後Spring Boot確實利用了JDK SPI機制來獲取資料庫連線。

5. 如何通過Spring Boot自動設定理解SPI思想

這種機制有點類似於JavaSPI,因為它允許第三方庫提供一些預設的設定。但它比JavaSPI更為強大和靈活,因為Spring Boot提供了大量的註解(如@ConditionalOnClass@ConditionalOnProperty@ConditionalOnMissingBean等)來控制自動設定類是否應該被載入和應用。

總的來說,Spring Bootspring.factories機制和JavaSPI在概念上是相似的,但它們在實現細節和用途上有所不同。

讓我們建立一個簡化的實際例子,假設我們要為不同的訊息服務(如SMSEmail)建立自動設定。

MessageService介面:

package com.example.demo.service;

public interface MessageService {
    void send(String message);
}

SMS服務實現:

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服務實現:

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(元件),分別是SmsServiceEmailService。這些beans的建立取決於application.properties檔案中特定的屬性值。

  • @ConditionalOnProperty(name = 「message.type」, havingValue = 「sms」)

application.propertiesapplication.yml中定義的屬性message.type的值為sms時,此條件為true。此時,smsService()方法將被呼叫,從而建立一個SmsServicebean

  • @ConditionalOnProperty(name = 「message.type」, havingValue = 「email」)

application.propertiesapplication.yml中定義的屬性message.type的值為email時,此條件為true。此時,emailService()方法將被呼叫,從而建立一個EmailServicebean

spring.factories檔案:

src/main/resources/META-INF目錄下建立一個spring.factories檔案,內容如下:

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);
    }
}

執行結果:

在上述例子中,我們建立了一個MessageService介面和兩個實現(SmsServiceEmailService)。然後,我們建立了一個自動設定類,其中包含兩個bean定義,這兩個bean定義分別基於application.properties中的屬性值條件性地建立。在spring.factories檔案中,我們宣告了這個自動設定類,以便Spring Boot在啟動時能夠自動載入它。

在此,繼續用電視機的例子昇華理解下

電視機類比

總體概念:
  • 假設電視機(TV)是一個Java應用。

  • 電視機的各種插槽,如HDMIUSBVGA等,可以視為應用中的SPI介面。

  • 插入這些插槽的裝置(如DVD播放器、遊戲機、USB驅動器等)可以視為SPI的實現。

Java的SPI:
  • 當我們購買電視機時,不知道將會連線哪種裝置,可能是DVD播放器,也可能是遊戲機。

  • 但是,只要這些裝置遵循了插槽的標準(例如,HDMI標準),就可以將其插入電視機並使其工作。

  • 這就像JavaSPI機制:為了能讓多個供應商提供實現,Java定義了一個介面,供應商提供具體的實現。

Spring Boot的自動設定:
  • 現在,想象一下現代的智慧電視。當插入一個裝置,電視機不僅可以識別它,還可能根據所連線的裝置型別自動調整設定,例如選擇正確的輸入源、優化影象質量等。

  • 這就像Spring Boot的自動設定:當Spring Boot應用啟動時,它會檢查classpath上的庫,並根據存在的庫自動設定應用。

  • 電視機的自動設定可以類比為Spring Boot中的spring.factories和各種@Conditional…註解。它們決定在什麼條件下進行哪種設定。

擴充套件性:
  • 如果電視製造商想為新型的插槽或連線技術開發電視,它可以很容易地在其電視機型中新增新的插槽。

  • 同樣地,使用Spring Boot,如果要為應用新增新功能或庫,只需新增相關的依賴,然後Spring Boot會自動識別並設定這些新功能。

通過這種類比,電視機的插槽和自動設定功能為我們提供了一個直觀的方式來理解JavaSPI機制和Spring Boot的自動設定如何工作,以及它們如何為應用開發者提供便利。

6. SPI(Service Provider Interface)總結

SPI,即服務提供者介面,是一種特定的設計模式。它允許框架或核心庫為第三方開發者提供一個預定義的介面,從而使他們能夠為框架提供自定義的實現或擴充套件。

核心目標:

  • 解耦:SPI機制讓框架的核心與其擴充套件部分保持解耦,使核心程式碼不依賴於具體的實現。
  • 動態載入:系統能夠通過特定的機制(如JavaServiceLoader)動態地發現和載入所需的實現。
  • 靈活性:框架使用者可以根據自己的需求選擇、更換或增加新的實現,而無需修改核心程式碼。
  • 可插拔:第三方提供的服務或實現可以輕鬆地新增到或從系統中移除,無需更改現有的程式碼結構。

價值:

  • 為框架或庫的使用者提供更多的自定義選項和靈活性。

  • 允許框架的核心部分保持穩定,同時能夠容納新的功能和擴充套件。

SPI與「開閉原則」:

「開閉原則」提倡軟體實體應該對擴充套件開放,但對修改封閉。即在不改變現有程式碼的前提下,通過擴充套件來增加新的功能。

SPI如何體現「開閉原則」:

  • 對擴充套件開放:SPI提供了一種標準化的方式,使第三方開發者可以為現有系統提供新的實現或功能。
  • 對修改封閉:新增新的功能或特性時,原始框架或庫的程式碼不需要進行修改。
  • 獨立發展:框架與其SPI實現可以獨立地進化和發展,互不影響。

總之,SPI是一種使軟體框架或庫更加模組化、可延伸和可維護的有效方法。通過遵循「開閉原則」,SPI確保了系統的穩定性和靈活性,從而滿足了不斷變化的業務需求。

點選關注,第一時間瞭解華為雲新鮮技術~