layout: post
categories: Java
title: Java 中經常被提到的 SPI 到底是什麼?
tagline: by 子悠
tags:
- 子悠
Java
程式設計師在日常工作中經常會聽到 SPI
,而且很多框架都使用了 SPI
的技術,那麼問題來了,到底什麼是 SPI
呢?今天阿粉就帶大家好好了解一下 SPI。
SPI 概念
SPI
全稱是 Service Provider Interface
,是一種 JDK
內建的動態載入實現擴充套件點的機制,透過 SPI
技術我們可以動態獲取介面的實現類,不用自己來建立。
這裡提到了介面和實現類,那麼 SPI
技術上具體有哪些技術細節呢?
- 介面:需要有一個功能介面;
- 實現類:介面只是規範,具體的執行需要有實現類才行,所以不可缺少的需要有實現類;
- 配置檔案:要實現
SPI
機制,必須有一個與介面同名的檔案存放於類路徑下面的META-INF/services
資料夾中,並且檔案中的每一行的內容都是一個實現類的全路徑; - 類載入器
ServiceLoader
:JDK
內建的一個類載入器,用於載入配置檔案中的實現類;
舉個例子
上面說了 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.java
和 WinRarCompresser.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】免費領取。希望能在這寒冷的日子裡,幫助到大家。