面試回答
Java 中區分 API 和 SPI,通俗的講:API 和 SPI 都是相對的概念,他們的差別只在語義上,API 直接被應用開發人員使用,SPI 被框架擴展人員使用。
API Application Programming Interface
大多數情況下,都是實現方來制定接口并完成對接口的不同實現,調用方僅僅依賴卻無權選擇不同實現。
SPI Service Provider Interface
而如果是調用方來制定接口,實現方來針對接口實現不同的實現。調用方來選擇自己需要的實現方。
知識擴展
如何定義一個 SPI
步驟1、定義一組接口(假設是 com.chiyi.test.IShout
),并寫出接口的一個或多個實現,(假設是 com.chiyi.test.Dog
、com.chiyi.test.Cat
)。
public interface IShout {void shout();
}
public class Dog implements IShout{@Overridepublic void shout() {System.out.println("wang wang");}
}
public class Cat implements IShout{@Overridepublic void shout() {System.out.println("miao miao");}
}
步驟2、在 src/main/resources/
下建立 /META-INF/services
目錄,新增一個以接口命名的文件(com.chiyi.test.IShout
文件),內容是要應用的實現類(這里是 com.chiyi.test.Dog
和com.chiyi.test.Cat
,每行一個類)。
com.chiyi.test.Dog
com.chiyi.test.Cat
步驟3、使用 ServiceLoader 來加載配置文件中指定的實現。
public class Main {public static void main(String[] args) {ServiceLoader<IShout> shouts=ServiceLoader.load(IShout.class);for(IShout s:shouts){s.shout();}}
}
代碼輸出:
wang wang
miao miao
SPI 的實現原理
看 ServiceLoader 類的簽名類的成員變量:
public final class ServiceLoader<S>implements Iterable<S>
{private static final String PREFIX = "META-INF/services/";// 代表被加載的類或者接口private final Class<S> service;// 用于定位,加載和實例化 providers 的類加載器private final ClassLoader loader;// 創建 ServiceLoader 時采用的訪問控制上下文private final AccessControlContext acc;// 緩存 providers,按實例化的順序排列private LinkedHashMap<String,S> providers = new LinkedHashMap<>();// 懶查找迭代器private LazyIterator lookupIterator;······
}
參考具體源碼,梳理了一下,實現的流程如下:
- 應用程序調用
ServiceLoader.load
方法,ServiceLoader.load
方法內先創建一個新的ServiceLoader
,并實例化該類中的成員變量,包括:
-
- loader(ClassLoader 類型,類加載器)
- acc(AccessControlContext 類型,訪問控制器)
- providers(LinkedHashMap 類型,用于緩存加載成功的類)
- lookupIterator(實現迭代器功能)
- 應用程序通過迭代器接口獲取對象實例
-
- ServiceLoader 先判斷成員變量 providers 對象中(LinkedHashMap 類型)是否有緩存實例對象,如果有緩存,直接返回。
- 如果沒有緩存,執行類的裝載:
-
-
- 讀取 META-INF/services/ 下的配置文件,獲得所有能被實例化的類的名稱
- 通過反射方法 Class.forName() 加載類對象,并用 instance() 方法將類實例化
- 把實例化的類緩存到 providers 對象中(LinkedHashMap 類型)
- 然后返回實例對象
-
SPI 的應用場景
概括地說,適用于:調用者根據實際使用需要,啟用、擴展、或者替換框架的實現策略。
比如常見的例子:
- 數據庫驅動加載接口實現類的加載
- JDBC 加載不同類型數據庫的驅動
- 日志門面接口實現類加載
- SLF4J 加載不同提供商的日志實現類
Spring
Spring 中大量使用了 SPI,比如:對 servlet3.0 規范對 ServletContainerInitializer 的實現、自動類型轉換 Type Conversion SPI(Converter SPI、Formatter SPI)等
Dubbo
Dubbo 中也大量使用 SPI的方式實現框架的擴展,不過它對 java 提供的原生 SPI 做了封裝,允許用戶擴展實現 Filter 接口。