Java SPI機制初探
一、概述
1. 什么是SPI
2. 對比API有什么區別
3. SPI有什么用
4. SPI工作機制
二、ServiceLoader
1. 源碼解析
2. 小結
三、SPI實際應用場景
1. JDBC
2. Spring
3. Dubbo
四、總結
一、概述
什么是SPI
SPI 即 Service Provider Interface ,也就是“服務提供者的接口”。
SPI 將服務接口和具體的服務實現分離開來,將服務調用方和服務實現者解耦,能夠提升程序的擴展性、可維護性。同時,修改或者替換服務的實現不需要修改調用方。
Java中有許多地方都使用到了SPI機制,比如數據庫加載驅動JDBC、Spring、以及
Dubbo的擴展實現等。
對比API有什么區別

API:接口實現方同時負責接口定義和接口實現,接口控制權在服務提供方。
SPI:服務調用方負責接口定義,不同的接口實現方根據接口定義可以有不同的實現,能夠在運行時動態的加載不用實現類,接口控制權在服務調用方。
SPI有什么用
解耦
在框架開發中,通常需要依賴一些可插拔的功能,但不希望實現具體的適配(能夠保持靈活性)。SPI機制通過定義接口和動態加載實現 ,可以讓框架與服務實現解耦。
※ 場景
- 一個數據庫連接池庫需要支持多個數據庫實現(如 MySQL、PostgreSQL),可以通過 SPI 機制動態加載這些數據庫驅動。
- 日志框架(比如 SLF4J)的具體實現,可以通過 SPI 機制加載不同的日志庫(如 Log4j、Logback)。
可擴展
SPI機制提供了動態發現和加載服務的能力,可以讓應用程序非常方便地實現擴展,而不需要修改現有代碼。
※ 場景
- 一個文件處理系統需要支持不同的文件格式(如 JSON 或 XML)。通過 SPI 機制可以動態發現不同的文件解析器插件,無需提前硬編碼支持的格式。
動態加載
SPI 可以用來實現插件化架構,通過動態加載具體的服務實現增減模塊,而無需重新發布整個系統。
※ 場景
- Web服務器(如 Tomcat)可以通過 SPI 機制動態加載不同的 HTTP 處理器或過濾器。
- 數據分析系統可以通過 SPI 機制動態加載新的分析算法。
SPI工作機制

二、ServiceLoader
ServiceLoader是JDK中提供的服務加載類,位于 java.util 包下,final修飾不可被繼承,是實現SPI機制的核心。
源碼解析
public final class ServiceLoader<S>
implements Iterable<S>
{
// 默認加載路徑前綴
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
// 被加載的實例或接口
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
// 類加載器
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
// 本地緩存,key: 類名 value;類
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
// 迭代器
private LazyIterator lookupIterator;
}※ load方法
// 暴露給外部使用的加載方法
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
// 構造方法私有化
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 如果指定了claseLoader,則使用該classLoader,如果沒有指定,則使用默認classLoader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}ClassLoader cl = Thread.currentThread().getContextClassLoader();通過獲取線程上下文類加載器(Thread Context ClassLoader)。
※ reload方法
public void reload() {
// 清空緩存,將linkedHashMap的頭和尾置為null
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}ServiceLoader 實現了 Iterable 接口的方法后,擁有了迭代的能力,在這個 iterator 方法被調用時,首先會在 ServiceLoader 的 Provider 緩存中進行查找,如果緩存中沒有命中,則在 LazyIterator 中進行查找。
public Iterator<S> iterator() {
return new Iterator<S>() {
// 本地緩存providers
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 優先查本地緩存
if (knownProviders.hasNext())
return true;
// 沒有則在LazyInterator中進行查找
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}LazyIterator 使ServiceLoader擁有了懶加載的能力,只有調用iterator方法或遍歷的時候才會去加載對應的實現類,核心代碼如下:
// Private inner class implementing fully-lazy provider lookup
//
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 配置文件路徑,默認 /META-INF/services/ 開頭
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else // 讀取實現類 類名
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析配置文件中每行類名
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 加載對應的實現類
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 創建實現類
S p = service.cast(c.newInstance());
// 放到緩存中
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}小結
ServiceLoader本質上就是讀取約定目錄(/META-INF/services/ )下對應接口全限定命名的文件,然后通過反射全量加載文件中定義的所有接口實現類,從而將接口與實現進行解耦。
※ 優點
- 解耦 :接口與實現分離,無需在代碼中硬編碼實現類。
- 擴展性 :新增實現只需添加配置,無需修改已有代碼。
※ 缺點
- 線程不安全 : ServiceLoader 非線程安全,不能保證單例。
- 性能開銷 :每次迭代都重新加載文件(可通過緩存解決),并且會全量配置文件中指定的所有實現類。
- 無健壯性 :配置錯誤(如類未找到)會拋出異常而非優雅降級。
三、SPI實際應用場景
JDBC
JDK中定義了Driver接口,用于連接數據庫。
package java.sql;
import java.util.logging.Logger;
public interface Driver {
Connection connect(String url, java.util.Properties info)
throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}不同的數據庫廠商實現這個接口,從而實現與數據庫的連接,以我們最熟悉的MySQL為例,獲取數據庫連接的示例代碼如下:
// 獲取數據庫連接
DriverManager connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123");
// 創建statement
Statement statement = connection.createStatement();
// 執行sql
ResultSet resultSet = statement.executeQuery("select * from student");其中DriverManager加載時會執行靜態代碼塊去加載driver,部分核心代碼如下:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 如果驅動程序被打包為服務提供者(Service Provider),則加載它。
// 通過類加載器獲取所有驅動程序
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 這里就使用ServiceLoader去加載接口實現類
// 以mysql-connector-java為例,加載的是 com.mysql.jdbc.Driver 和 com.mysql.fabric.jdbc.FabricMySQLDriver
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 加載數據庫驅動Driver, 以mysql-connector-java為例,這里加載的是 com.mysql.jdbc.Driver 和 com.mysql.fabric.jdbc.FabricMySQLDrive
// 并注冊到registeredDrivers中
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}隨后在getConnection方法中遍歷已經加載的driver,執行connect方法進行連接。
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
// 遍歷已經注冊的driver,調用connect方法進行連接
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}Spring
Spring 中沒有直接使用Java SPI機制,不過 Spring的spring.factories 機制類似于SPI機制并擁有更強大的擴展機制,通過讀取 META-INF/spring.factories 文件實現自動裝配、上下文初始化等功能。
// SpringFactoriesLoader 源碼核心邏輯
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
// 從所有jar包的spring.factories文件加載配置
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<>();
while (urls.hasMoreElements()) {
Properties properties = PropertiesLoaderUtils.loadProperties(
new UrlResource(urls.nextElement()));
String factoryClassNames = properties.getProperty(factoryType.getName());
// 支持逗號分隔的多個實現
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
// 1. 加載所有實現類名
List<String> names = loadFactoryNames(factoryType, classLoader);
// 2. 實例化所有實現
List<T> instances = new ArrayList<>(names.size());
for (String name : names) {
Class<?> instanceClass = ClassUtils.forName(name, classLoader);
instances.add((T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance());
}
// 3. 按@Order排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
MultiValueMap<String, String> result = new LinkedMultiValueMap();
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry)var6.next();
String factoryClassName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryName = var9[var11];
result.add(factoryClassName, factoryName.trim());
}
}
}
// 讀取META-INF/spring.factories文件夾下文件,解析文件內容并緩存到Map中
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
IOException ex = var13;
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", ex);
}
}
}
}spring.factories文件的格式為 xxx=xxxx,=號前面的key為接口全限定名,=號后面的value為接口實現類全限定名 ,多個實現類之間用逗號分割,下圖中指定的key為EnableAutoConfiguration,這樣spring就會為=號后面的類注冊為bean。spring自動裝配原理這里暫不展開。
org.springframework.boot.autoconfigure.EnableAutoCnotallow=\
org.apache.hadoop.hbase.client.MonitorAlicloudHBaseAutoConfigurationDubbo
Dubbo的可擴展機制也使用到了SPI,不過不是原生的SPI,而是經過優化的。
由上文ServiceLoader源碼解析可知,SPI會讀取配置文件,遍歷所有類并實例化,如果某些類并不會使用,此時還是會加載進來,造成了資源的浪費;Java SPI配置文件中只是簡單列出了所有擴展實現,沒有給他們命名,無法在程序中準確引用;無法做到自動注入和裝配等等…… Dubbo擴展點機制優化了上述問題,提供了動態擴展的能力。
Demo
定義一個接口,并使用 @SPI 注解標注,這表明這個接口是一個擴展點,可被Dubbo的ExtensionLoader加載。
@SPI
public interface DemoSpi {
void say();
}接口實現:
public class DemoSpiImpl implements DemoSpi {
public void say() {
}
}將實現類放在特定目錄下,Dubbo在加載擴展類的時候,會從 META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/ 這幾個目錄下讀取。這里在 META-INF/dubbo 目錄下新建一個以 DemoSpi 接口名為文件名的文件,內容如下:
demoSpiImpl = com.xxx.xxx.DemoSpiImpl(為 DemoSpi 接口實現類的全類名)使用如下:
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<DemoSpi> extensionLoader =
ExtensionLoader.getExtensionLoader(DemoSpi.class);
DemoSpi dmeoSpi = extensionLoader.getExtension("demoSpiImpl");
dmeoSpi.sayHello();
}
}源碼解析
由上面的例子可以看出,Dubbo主要通過ExtensionLoader加載配置文件中指定的實現類,整體流程上和ServiceLoader加載流程類似,同時做了相關的優化擴展。
※ duboo加載擴展主要步驟
- 讀取并解析配置文件
- 緩存所有擴展實現
- 基于用戶執行的擴展名,實例化對應的擴展實現
- 進行擴展實例化屬性的IOC注入及實例化擴展的包裝類,實現AOP特性
SPI 加載固定擴展類的入口是 ExtensionLoader 的 getExtension 方法,該方法主要調用createExtession方法獲取擴展實例:
private T createExtension(String name, boolean wrap) {
// 從配置文件中加載所有的拓展類,可得到“配置項名稱”到“配置類”的映射關系表
Class<?> clazz = getExtensionClasses().get(name);
// 如果沒有該接口的擴展,或者該接口的實現類不允許重復但實際上重復了,直接拋出異常
if (clazz == null || unacceptableExceptions.contains(name)) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
// 這段代碼保證了擴展類只會被構造一次,也就是單例的.
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 向實例中注入依賴
injectExtension(instance);
// 如果啟用包裝的話,則自動為進行包裝.
// 比如我基于 Protocol 定義了 DubboProtocol 的擴展,但實際上在 Dubbo 中不是直接使用的 DubboProtocol, 而是其包裝類 ProtocolListenerWrapper
if (wrap) {
List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
wrapperClassesList.addAll(cachedWrapperClasses);
wrapperClassesList.sort(WrapperComparator.COMPARATOR);
Collections.reverse(wrapperClassesList);
}
// 循環創建 Wrapper 實例
if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
for (Class<?> wrapperClass : wrapperClassesList) {
Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
if (wrapper == null || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
// 將當前 instance 作為參數傳給 Wrapper 的構造方法,并通過反射創建 Wrapper 實例。 // 然后向 Wrapper 實例中注入依賴,最后將 Wrapper 實例再次賦值給 instance 變量
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
}
}
// 初始化
initExtension(instance);
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " + type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}※ 上述代碼全流程
- 通過getExtensionClasses獲取所有的拓展類
- 通過反射創建拓展對象
- 向拓展對象中注入依賴
- 將拓展對象包裹在相應的Wrapper對象中
- 初始化擴展對象
獲取所有拓展類getExtensionClasses和JDK SPI類似,也是先從緩存中拿,拿不到加鎖再次查緩存,還拿不到則通過loadExtensionClasses加載拓展類。
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = (Map)this.cachedClasses.get();
if (classes == null) {
synchronized(this.cachedClasses) {
classes = (Map)this.cachedClasses.get();
if (classes == null) {
classes = this.loadExtensionClasses();
this.cachedClasses.set(classes);
}
}
}
return classes;
}loadExtensionClasses總共做了兩件事情,一是解析@SPI注解,二是調用loadDirectory方法加載指定文件夾配置文件。
private Map<String, Class<?>> loadExtensionClasses() {
// 緩存默認的 SPI 擴展名
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
// 基于策略來加載指定文件夾下的文件
// 分別讀取 META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/ 這幾個目錄下的配置文件
for (LoadingStrategy strategy : strategies) {
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
}
return extensionClasses;
}其中loadDirectory方法則是拿到指定文件夾下文件的全路徑名,調用loadResource去加載資源。
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8));
Throwable var7 = null;
try {
String line;
try {
// 讀取配置文件中每一行
while((line = reader.readLine()) != null) {
// 只讀取#前面的內容,#后面的為注釋
int ci = line.indexOf(35);
if (ci >= 0) {
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf(61);
if (i > 0) {
// 分別讀取=前的作為name,=后面的為具體的實現類全路徑名
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0 && !this.isExcluded(line, excludedPackages)) {
this.loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden);
}
} catch (Throwable var21) {
Throwable t = var21;
IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + this.type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
this.exceptions.put(line, e);
}
}
}
} catch (Throwable var22) {
var7 = var22;
throw var22;
}
} finally {
//close ...
}
} catch (Throwable var24) {
Throwable t = var24;
logger.error("Exception occurred when loading extension class (interface: " + this.type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}loadResource則主要讀取配置文件中每一行,分為key和value兩部分,key為=前面的實現類別名,value為=后面的實現類實例,加載實例的過程為loadClass方法,如下:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
boolean overridden) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error occurred when loading extension class (interface: " + type + ", class line: " + clazz.getName() + "), class " + clazz.getName() + " is not subtype of interface.");
}
// 檢測目標類上是否有 Adaptive 注解
if (clazz.isAnnotationPresent(Adaptive.class)) {
cacheAdaptiveClass(clazz, overridden);
} else if (isWrapperClass(clazz)) {
// 緩存包裝類
cacheWrapperClass(clazz);
} else {
// 進入到這里,表明只是該類只是一個普通的拓展類
// 檢測 clazz 是否有默認的構造方法,如果沒有,則拋出異常
clazz.getConstructor();
if (StringUtils.isEmpty(name)) {
// 如果 name 為空,則嘗試從 Extension 注解中獲取 name,或使用小寫的類名作為 name
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
String[] names = NAME_SEPARATOR.split(name);
if (ArrayUtils.isNotEmpty(names)) {
// 如果類上有 Activate 注解,則使用 names 數組的第一個元素作為鍵,
// 存儲 name 到 Activate 注解對象的映射關系
cacheActivateClass(clazz, names[0]);
for (String n : names) {
// 存儲 Class 到名稱的映射關系
cacheName(clazz, n);
// 存儲 name 到 Class 的映射關系.
// 如果存在同一個擴展名對應多個實現類,基于 override 參數是否允許覆蓋,如果不允許,則拋出異常.
saveInExtensionClass(extensionClasses, clazz, n, overridden);
}
}
}
}Dubbo SPI應用場景
※ 協議擴展
# META-INF/dubbo/org.apache.dubbo.rpc.Protocol
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
http=org.apache.dubbo.rpc.protocol.http.HttpProtocol
grpc=org.apache.dubbo.rpc.protocol.grpc.GrpcProtocol※ 集群容錯
@SPI(FailoverCluster.NAME)
public interface Cluster {
@Adaptive
<T> Invoker<T> join(Directory<T> directory) throws RpcException;
}
// 使用示例
<dubbo:reference cluster="failfast"/>※ 服務治理 filter過濾器
@SPI
public interface Filter {
Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;
public interface Listener {
void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation);
void onError(Throwable t, Invoker<?> invoker, Invocation invocation);
}
}四、總結
值得一提的是,SPI在實際使用過程中存在一些問題:
※ 資源浪費預性能問題
由上文源碼解析可知,ServiceLoader在加載時會掃描 META-INF/services 目錄并解析文件,會通過反射實例化所有實現類,如果實現類很多,或者初始化很耗時,會造成一定程度上的性能開銷和資源浪費,所以Dubbo的ExtensionLoader在此之上進行了優化,通過緩存以及在配置文件中指定key實現只加載部分實現類。
※ 多個實現類加載順序問題
ServiceLoader加載服務提供者實現的順序由classpath中jar包的順序決定,如果你的邏輯依賴于獲取到的第一個實現,在不同環境下可能會出現加載順序不一致導致的異常問題,所以盡可能避免依賴加載順序。
※ 類重復加載問題
如果classpath中存在多個jar包都申明了同一個接口的相同實現類,或者同一個jar包被不同類加載器加載多次,會導致同一個實現類被多次加載和實例化,可能導致單例失效或資源沖突。
總的來說,SPI機制的應用場景還是很廣的,其核心在于通過讀取配置文件動態加載接口實現,解耦了接口定義與實現 ,實現了框架可擴展性,在各大框架中都能看到其身影。




























