Java 中經常被提到的 SPI 到底是什麼?

2022-11-25 21:00:24
layout: post
categories: Java
title: Java 中經常被提到的 SPI 到底是什麼?
tagline: by 子悠
tags: 
  - 子悠

Java 程式設計師在日常工作中經常會聽到 SPI,而且很多框架都使用了 SPI 的技術,那麼問題來了,到底什麼是 SPI 呢?今天阿粉就帶大家好好了解一下 SPI。

SPI 概念

SPI 全稱是 Service Provider Interface,是一種 JDK 內建的動態載入實現擴充套件點的機制,通過 SPI 技術我們可以動態獲取介面的實現類,不用自己來建立。

這裡提到了介面和實現類,那麼 SPI 技術上具體有哪些技術細節呢?

  1. 介面:需要有一個功能介面;
  2. 實現類:介面只是規範,具體的執行需要有實現類才行,所以不可缺少的需要有實現類;
  3. 組態檔:要實現 SPI 機制,必須有一個與介面同名的檔案存放於類路徑下面的 META-INF/services 資料夾中,並且檔案中的每一行的內容都是一個實現類的全路徑;
  4. 類載入器 ServiceLoaderJDK 內建的一個類載入器,用於載入組態檔中的實現類;

舉個栗子

上面說了 SPI 的幾個概念,接下來阿粉就通過一個栗子來帶大家感受一下具體的用法。

第一步

建立一個介面,這裡我們建立一個解壓縮的介面,其中定義了壓縮和解壓的兩個方法。

package com.example.demo.spi;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-10-08 21:31<br>
 * <b>Desc:</b>無<br>
 */
public interface Compresser {
  byte[] compress(byte[] bytes);
  byte[] decompress(byte[] bytes);
}

第二步

再寫兩個對應的實現類,分別是 GzipCompresser.javaWinRarCompresser.java 程式碼如下

package com.example.demo.spi.impl;

import com.example.demo.spi.Compresser;

import java.nio.charset.StandardCharsets;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-10-08 21:33<br>
 * <b>Desc:</b>無<br>
 */
public class GzipCompresser implements Compresser {
  @Override
  public byte[] compress(byte[] bytes) {
    return"compress by Gzip".getBytes(StandardCharsets.UTF_8);
  }
  @Override
  public byte[] decompress(byte[] bytes) {
    return "decompress by Gzip".getBytes(StandardCharsets.UTF_8);
  }
}
package com.example.demo.spi.impl;

import com.example.demo.spi.Compresser;

import java.nio.charset.StandardCharsets;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author ziyou<br>
 * <b>Date:</b>2022-10-08 21:33<br>
 * <b>Desc:</b>無<br>
 */
public class WinRarCompresser implements Compresser {
  @Override
  public byte[] compress(byte[] bytes) {
    return "compress by WinRar".getBytes(StandardCharsets.UTF_8);
  }

  @Override
  public byte[] decompress(byte[] bytes) {
    return "decompress by WinRar".getBytes(StandardCharsets.UTF_8);
  }
}

第三步

建立組態檔,我們接著在 resources 目錄下建立一個名為 META-INF/services 的資料夾,在其中建立一個名為 com.example.demo.spi.Compresser 的檔案,其中的內容如下:

com.example.demo.spi.impl.WinRarCompresser
com.example.demo.spi.impl.GzipCompresser

注意該檔案的名稱必須是介面的全路徑,檔案裡面的內容每一行都是一個實現類的全路徑,多個實現類就寫在多行裡面,效果如下。

第四步

有了上面的介面,實現類和組態檔,接下來我們就可以使用 ServiceLoader 動態載入實現類,來實現 SPI 技術了,如下所示:

package com.example.demo;

import com.example.demo.spi.Compresser;

import java.nio.charset.StandardCharsets;
import java.util.ServiceLoader;

public class TestSPI {
  public static void main(String[] args) {
    ServiceLoader<Compresser> compressers = ServiceLoader.load(Compresser.class);
    for (Compresser compresser : compressers) {
      System.out.println(compresser.getClass());
    }
  }
}

執行的結果如下

可以看到我們正常的獲取到了介面的實現類,並且可以直接使用實現類的解壓縮方法。

原理

知道了如何使用 SPI 接下來我們來研究一下是如何實現的,通過上面的測試我們可以看到,核心的邏輯是 ServiceLoader.load() 方法,這個方法有點類似於 Spring 中的根據介面獲取所有實現類一樣。

點開 ServiceLoader 我們可以看到有一個常數 PREFIX,如下所示,這也是為什麼我們必須在這個路徑下面建立組態檔,因為 JDK 程式碼裡面會從這個路徑裡面去讀取我們的檔案。

同時又因為在讀取檔案的時候使用了 class 的路徑名稱,因為我們使用 load 方法的時候只會傳遞一個 class,所以我們的檔名也必須是介面的全路徑。

通過 load 方法我們可以看到底層構造了一個 java.util.ServiceLoader.LazyIterator 迭代器。

在迭代器中的 parse 方法中,就獲取了組態檔中的實現類名稱集合,然後在通過反射建立出具體的實現類物件存放到 LinkedHashMap<String,S> providers = new LinkedHashMap<>(); 中。

常用的框架

SPI 技術的使用非常廣泛,比如在 Dubble,不過 Dubble 中的 SPI 有經過改造的,還有我們很常見的資料庫的驅動中也使用了 SPI,感興趣的小夥伴可以去翻翻看,還有 SLF4J 用來載入不同提供商的紀錄檔實現類以及 Spring 框架等。

優缺點

前面介紹了 SPI 的原理和使用,那 SPI 有什麼優缺點呢?

優點

優點當然是解耦,服務方只要定義好介面規範就好了,具體的實現可以由不同的 Jar 進行實現,只要按照規範實現功能就可以被直接拿來使用,在某些場合會被進行熱插拔使用,實現瞭解耦的功能。

缺點

一個很明顯的缺點那就是做不到按需載入,通過原始碼我們看到了是會將所有的實現類都進行建立的,這種做法會降低效能,如果某些實現類實現很耗時了話將影響載入時間。同時實現類的命名也沒有規範,讓使用者不方便參照。

總結

阿粉今天給大家介紹了一個 SPI 的原理和實現,感興趣的小夥伴可以自己去嘗試一下,多動手有利於加深記憶哦,如果覺得我們的文章有幫助,歡迎點贊評論分享轉發,讓更多的人看到。


更多優質內容歡迎關注公眾號【Java 極客技術】,我準備了一份面試資料,回覆【bbbb07】免費領取。希望能在這寒冷的日子裡,幫助到大家。