一位前輩在一次技術分享中指出咱們目前的包管理不規範,模塊間職責有重疊,理解成本高不易維護,提出在開發過程當中應當明確按照職責將服務劃分到對應的模塊中。java
好比咱們把全部服務都放在service層,但其實服務也是分爲基礎服務和業務邏輯服務的,或許把相似業務數據查詢組裝服務放在service層,把具體業務邏輯服務統一放在business層會更好,更利於基礎服務的複用。mysql
但當服務拆離到不一樣模塊進行復用時,可能在開發過程當中出現服務依賴的問題,這部分依賴問題的解耦能夠用到Java的SPI機制。顯然,我歷來沒有據說過SPI是什麼,也不明白這有什麼好處。sql
翻遍各類網上資料,來來回回都是車軲轆話,互相抄來抄去講得並不通俗易懂,這裏就用我本身的理解來解釋。shell
SPI(Service Provider Interface)
,大意是「服務提供者接口」,是指在服務使用方角度提出的「接口要求」,是對「服務提供方」提出的約定,簡單說就是:「我須要這樣的服務,如今大家來知足」。編程
API(Application Programming Interface)
與之相對,是站在服務提供方角度提供的無需瞭解底層細節的操做入口,即「我有這樣的服務能夠給你使用」。微信
SPI與API的出發點大相徑庭,但做用與目的是相同的,即面向接口編程,也就是解耦。同時SPI使用的是一種「插件思惟」,即服務提供者負責全部的使用維護,當替換服務提供方時不要說調用方不修改代碼,連配置文件都不須要修改(不過可能要修改依賴的jar)。併發
模塊化插件框架
舉個例子,隔壁部門以爲咱們的一個現有服務很棒,但願咱們在其專用環境部署一份,同時但願之後的全部迭代可以給他們也更新。可是使用的自研中間件咱們使用的內網版本他們使用公網版本,支付上咱們對接支付寶他們對接微信......在業務邏輯不變但切換基礎服務時應該如何維護使成本最小?ide
方案 | 優勢 | 缺點 |
---|---|---|
維護兩套代碼 | 邏輯一致 | 實現簡單但維護成本高 |
同一套代碼,在業務邏輯中區分環境 | 維護成本低,統一管理 | 邏輯複雜,須要硬編碼,當再出現新環境時還得折騰 |
SPI「插件」方式 | 維護成本低,無需針對實現方硬編碼,更多新環境或服務提供方變化時修改簡單且不影響原有邏輯 | 理解成本提升 |
這也許就是一些框架在發展過程當中經歷過的階段,能夠發現使用「插件」能更好知足這個需求。模塊化
試想一下,若是要實現這樣的解耦方式,理想狀況下應該如何作?不外乎就是如下幾點:
這是一種與IOC相同的思路,將裝配控制權轉移至程序外,由配置決定,切換成本低。
java.util.ServiceLoader提供的SPI加載方式
這個類很是簡單,是原生支持的SPI加載方式,實際代碼量也就200行左右。
關鍵點:
public static <S> ServiceLoader<S> load(Class<S> service)
ServiceLoader<SomeService> loader = ServiceLoader.load(SomeService.class);
可設置接入對應的接口private static final String PREFIX = "META-INF/services/";
META-INF/services/
目錄META-INF/services/
下,文件名應爲接口全限定名,內容每行爲一個實現類全限定名public final class ServiceLoader<S> implements Iterable<S>
private class LazyIterator implements Iterator<S>
,實現了懶加載迭代,即迭代到對應的類才加載對應的類private boolean hasNextService()
和private S nextService()
hasNext()
方法和next()
方法Class.forName
加載類,使用newInstance
初始化實例,cast
進行強制類型轉換最終獲得實例,所以實現類必須提供無參構造方法清楚原理後,使用方式就很好理解。
step.1 調用方定義接口
package com.xxx;
public interface IHelloWorld {
void sayHello();
}
複製代碼
step.? 使用API方式實現接口
非必選,對照看一下非SPI的方式。
package com.xxx;
public class HelloWorldApi implements IHelloWorld {
@Override
public void sayHello() {
System.out.println("Hello API!");
}
}
複製代碼
step.2 調用方在業務代碼中使用ServiceLoader
package com.xxx;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
// 使用API
IHelloWorld helloWorldApi = new HelloWorldApi();
helloWorldApi.sayHello();
// 使用SPI
ServiceLoader<IHelloWorld> loader = ServiceLoader.load(IHelloWorld.class);
for (IHelloWorld helloWorldSpi : loader) {
helloWorldSpi.sayHello();
}
}
}
複製代碼
主要區別在於SPI方式並不須要知道實現類是誰,徹底面向接口使用,相似RPC調用的狀況;而API要求在業務方代碼/配置中指明實現類。
step.3 提供方實現接口
這裏提供兩個實現類。
package com.xxx;
public class HelloWorldSpi1 implements IHelloWorld {
@Override
public void sayHello() {
System.out.println("Hello SPI 1!");
}
}
複製代碼
package com.xxx;
public class HelloWorldSpi2 implements IHelloWorld {
@Override
public void sayHello() {
System.out.println("Hello SPI 2!");
}
}
複製代碼
能夠看出,實現方式與API方式徹底一致。
step.4 提供方提供配置
文件位於/resources/META-INF/services
,文件名爲com.xxx.IHelloWorld
即接口全限定名稱。
/resources/META-INF/services/com.xxx.IHelloWorld
的內容爲兩個實現類的全限定名稱:
com.xxx.HelloWorldSpi1
com.xxx.HelloWorldSpi2
複製代碼
ps. 一般調用方與提供方不在同一個jar中
輸出結果
Hello API!
Hello SPI 1!
Hello SPI 2!
複製代碼
參考咱們經常使用的JDBC,咱們在同一套代碼中可能須要利用相同接口但不一樣實現的狀況下,能夠在代碼中利用SPI接入面向接口編程,在業務中不考慮具體的底層實現。
具體的底層實現能夠分離出來,將每組實現和SPI配置文件打包成不一樣的jar,在具體使用時根據須要使用不一樣的jar便可。
具體實現可隨時替換,不修改業務代碼或配置
在mysql-connector-java:5.1.47
包的META-INF/services/
目錄下有個java.sql.Driver
文件,內容爲:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
複製代碼
這是JDBC 4.0以後使用SPI機制直接獲取實現,避免以前使用Class.forName("com.mysql.jdbc.Driver")
方式加載MySQL驅動時的硬編碼。詳情可見java.sql.DriverManager
類中的靜態代碼塊:
static {
loadInitialDrivers(); // 這裏使用ServiceLoader獲取具體的Driver接口實現
println("JDBC DriverManager initialized");
}
複製代碼
本文搬自個人博客,歡迎參觀!