SPI機制

Erosion2020發表於2024-11-26

概述

Java SPI(Service Provider Interface)是一種 服務發現機制,用於實現模組化、可插拔式的設計。在 Java 中,它允許程式在執行時動態地載入和呼叫實現類,而不是在編譯時硬編碼依賴。這種機制在 JDK 內建庫第三方庫 中被廣泛使用,例如 JDBC 驅動載入、日誌框架繫結(如 SLF4J 和 Logback)、序列化機制擴充套件等。

SPI 的核心概念

  1. 服務介面(Service Interface)
    定義服務的規範,提供一個介面或抽象類。
  2. 服務提供者(Service Provider)
    一個實現了服務介面的具體類。
  3. 服務載入器(Service Loader)
    用於動態載入實現服務介面的服務提供者類。

SPI 的工作機制

Java SPI 的實現依賴於 resources/META-INF/services 資料夾中的描述檔案。主要過程如下:

  1. 定義服務介面: 建立一個服務介面,定義公共方法。
  2. 建立服務提供者: 編寫實現服務介面的具體類。
  3. 配置服務提供者:META-INF/services 資料夾中,建立一個檔案,檔名是服務介面的全限定類名,內容是服務提供者的全限定類名。
  4. 透過 ServiceLoader 載入服務: 使用 java.util.ServiceLoader 動態載入實現類。

Java SPI 示例

我的檔案結構定義如下:

/src/
    ├── test/
    	├── java/
    		├── spi/
    			├── example/
    				├── MyService		# SPI介面
    				├── SericeA			# SPI介面A實現
    				├── SericeB			# SPI介面B實現
    				├── SPIServiceLoader # SPI載入器
    ├── resources/
    	├── META-INF/
    		├── services/
    			├── spi.example.MyService	# 資原始檔

定義服務介面

建立一個服務介面 MyService

package spi.example;

public interface MyService {
    void execute();
}

建立服務提供者

建立ServiceA、SeriviceB兩個類,然後重寫excute程式碼

package spi.example;

public class ServiceA implements MyService {
    @Override
    public void execute() {
        System.out.println("ServiceA is executing...");
    }
}
package spi.example;

public class ServiceB implements MyService {
    @Override
    public void execute() {
        System.out.println("ServiceB is executing...");
    }
}

配置服務提供者

resources/META-INF/services 目錄下,建立一個檔案,檔名為 spi.example.MyService,內容為:

spi.example.ServiceA
spi.example.ServiceB

使用 ServiceLoader 載入服務

在主程式中,透過 ServiceLoader 動態載入實現類:

package spi.example;

import java.util.ServiceLoader;
public class SPIServiceLoader {
    public static void main(String[] args) {
        ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);

        for (MyService service : loader) {
            service.execute();
        }
    }
}

執行結果

image-20241126224757183

SPI惡意程式碼執行

比如我們在spi.example包中新增一份惡意程式碼的MyService實現,如下:

package spi.example;

public class EvilService implements MyService{
    public EvilService(){
        try {
            System.out.println("EvilService constructor is executing...");
            Runtime.getRuntime().exec("calc");
        }catch (Exception ignore) { }
    }
    @Override
    public void execute() {
        System.out.println("EvilService is executing...");
    }
}

執行結果如下,可以看到惡意程式碼被執行:

image-20241126225345244

SPI 的缺點

  • 效能問題: 每次呼叫 ServiceLoader 都需要掃描 META-INF/services 下的檔案,可能影響效能。
  • 缺乏優先順序支援: 多個服務提供者時,SPI 無法原生支援載入優先順序。
  • 安全性問題: 攻擊者可能透過篡改 META-INF/services 檔案載入惡意類。

增強版 SPI

為了解決上述缺點,現代框架(如 Spring)提供了增強的服務發現機制。例如:

  • Spring 使用 @Component 和 @Autowired 自動注入服務。
  • Google Guice 和 Apache Dubbo 也擴充套件了類似的機制,支援更加靈活的依賴注入和服務載入。
  • SPI 是 Java 生態中非常重要的機制,在理解其原理的基礎上,可以結合實際場景選擇更適合的方案。

相關文章