一、前言
在之前的dubbo源碼分析我們以及了解了dubbo相關的架構、用法和原理,但是提到dubbo我們就不得不提其中spi機制,dubbo源碼中使用了大量的spi機制,其所有的核心元件都做成了基于spi的實作方式,比如Protocol層=>DubboProtocol,Cluster層=>FailoverCluster等,spi這種機制可以實作這個元件的插拔式使用(一個接口多種實作,可以随時調整實作方式)同時支援我們進行定制化擴充。
下圖為dubbo使用spi形式引入的内部元件

下面就讓我們一起來探究一下dubbo的spi相關機制。
二、java SPI
在介dubbo的spi之前我們需要先了解一下什麼是spi以及jdk預設支援的spi機制。
<h3>2.1、什麼是SPI</h3>
SPI的全名為Service Provider Interface,子產品之間互相調用基于接口程式設計,為了能夠實作不同接口實作的可插拔需要為某個接口尋找服務實作将裝配的控制權移到程式之外的機制。
要使用Java SPI,需要遵循如下約定:
- 1、當服務提供者提供了接口的一種具體實作後,在jar包的META-INF/services目錄下建立一個以“接口全限定名”為命名的檔案,内容為實作類的全限定名;
- 2、接口實作類所在的jar包放在主程式的classpath中;
- 3、主程式通過java.util.ServiceLoder動态裝載實作子產品,它通過掃描META-INF/services目錄下的配置檔案找到實作類的全限定名,把類加載到JVM;
- 4、SPI的實作類必須攜帶一個不帶參數的構造方法;
像我們熟悉的java的jdbc,jdk定義了一套接口規範,不同的資料庫廠商提供不同的實作。
mysql資料廠商實作:
2.2、java SPI 示範例
項目結構
spi-api: 提供接口規範
public interface SpiService {
void say();
}
**spi-impl1、spi-impl2: 提供不同實作 **
public class SpiServiceImplOne implements SpiService {
public void say() {
System.out.println("i am SpiServiceImplOne");
}
}
其他實作類類似
spi配置 不同實作都有相似配置
@Test
public void testSpi(){
ServiceLoader<SpiService> serviceLoader = ServiceLoader.load(SpiService.class);
for (SpiService o : serviceLoader) {
o.say();
}
}
測試結果
介紹java提供的spi機制、執行個體、源碼分析,優缺點 引申出dubbo的spi機制
2.3、java spi源碼解析
public static <S> ServiceLoader<S> load(Class<S> service) {
//擷取classLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
//擷取接口類
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//擷取類記載器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
//安全管理
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
//重新家在
reload();
}
public void reload() {
//清空緩存中的接口對應的執行個體對象
providers.clear();
//建立LazyIterator 該對象是用來循環周遊執行個體化接口實作類的
lookupIterator = new LazyIterator(service, loader);
}
從測試類ServiceLoader的load()開始溯源,最終其使用兩個參數對象 服務接口的class和類加載器建立LazyIterator對象,該對象是實際進行服務執行個體化的,其實作了Iterator疊代器hasNext()=>hasNextService()和next()=>nextService().
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//擷取spi配置檔案路徑 classPath /META-INF/service/${接口全限定類名}
String fullName = PREFIX + service.getName();
//根據配置檔案路徑記載配置檔案到configs
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//讀取并解析配置檔案擷取其中的配置資訊(配置資訊可以是多個實作類是一個清單)則pending為接口實作名的清單疊代對象
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
//從疊代器擷取一個接口實作 使用nextName接收
nextName = pending.next();
return true;
}
hasNextSerive實作判斷是否還有需要執行個體化的接口實作,其中有三個熟悉關注
- nextName: 需要進行執行個體化的接口實作類全限定類名 從pending疊代器中擷取到
- pending:從配置檔案中解析出來的所有實作接口全限定類名清單集合(疊代器形式)
- configs: 根據classPath /META-INF/service/${接口全限定類名}夾在的URL對象
//擷取服務執行個體對象
private S nextService() {
//省略相關校驗
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
}
該方法簡單主要是使用接口實作類的空構造器(是以java SPI接口實作類需要遵循提供一個空構造器限制)
2.4、java SPI的優劣
在使用和源碼分析後,我們能大概總結出其優缺點
優點:
使用Java SPI機制的優勢是實作解耦,使得第三方服務子產品的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用架構擴充或替換架構元件,進而很好的支援可插拔和變更擴充。
缺點:
ServiceLoader隻能完全周遊并全部進行執行個體化,不能很好的按需加載(遇到接口實作類執行個體化特别耗費資源情況)會有性能損失和不靈活
多個并發多線程使用ServiceLoader類的執行個體是不安全的。
是以dubbo并沒有完全采用javaSPI(隻是做了相容),而是自己實作類一套SPI機制,下面我們來看看Dubbo的spi機制。
三、dubbo SPI
dubbo在原有的spi基礎上主要有以下的改變,①配置檔案采用鍵值對配置的方式,使用起來更加靈活和簡單通過@SPI實作按需加載 ② 增強了原本SPI的功能,使得SPI具備ioc和aop的功能,這在原本的java中spi是不支援的。dubbo的spi是通過ExtensionLoader來解析的,通過ExtensionLoader來加載指定的實作類,
配置檔案的路徑在META-INF/dubbo路徑下
我們先來看一下 Dubbo 對配置檔案目錄的約定,不同于 Java SPI ,Dubbo 分為了三類目錄。
- META-INF/services/ 目錄:該目錄下的 SPI 配置檔案是為了用來相容 Java SPI 。
- META-INF/dubbo/ 目錄:該目錄存放使用者自定義的 SPI 配置檔案。
- META-INF/dubbo/internal/ 目錄:該目錄存放 Dubbo 内部使用的 SPI 配置檔案。
3.1、示例
Dubbo的SPI例子的和Java的項目架構一直,接口以及實作類似隻是需要在接口中使用dubbo的SPI相關注解
- 接口
@SPI("two")
public interface SpiDemo {
//動态自适應擴充注解
@Adaptive
void getSpi(URL url);
}
- 配置
dubbo源碼分析之八-dubbo的spi機制 - 測試
@Test
public void testSpi(){
ExtensionLoader<SpiDemo> extensionLoader = ExtensionLoader.getExtensionLoader(SpiDemo.class);
URL url = URL.valueOf("test://123.5.5.5/test");
//普通擴充
//從全部的實作類中根據spi名字擷取一個
SpiDemo extension = extensionLoader.getExtension("two");
extension.getSpi(url); //輸出SpiDemoImpl two
//自适應擴充
//預設擷取 @SPI中的value屬性作為參數
SpiDemo adaptiveExtension = extensionLoader.getAdaptiveExtension();
adaptiveExtension.getSpi(url); //輸出SpiDemoImpl two
//@SPI中參數為two,url中設定的參數為one 則使用one對應的SpiDemoImplOne
url = URL.valueOf("test://123.5.5.5/test?spi.demo=one");
adaptiveExtension = extensionLoader.getAdaptiveExtension();
adaptiveExtension.getSpi(url); //輸出SpiDemoImpl one
//@Activate 會擷取滿足條件注解中條件的一組接口實作 一般在Dubbo的過濾器中使用較多
List<SpiDemo> myWorks = extensionLoader.getActivateExtension(url, "", "myWork");
for(SpiDemo spiDemo:myWorks){
spiDemo.getSpi(url);
}
}
@Adaptive注解根據Dubbo 的URL相關參數動态的選擇具體的實作 通過該getAdaptiveExtension擷取
該項目預設參數設定 spi.demo(接口名SpiDemo的駝峰逆轉為spi.demo)
如果沒有設定則預設參數為@Spi注解的value值即 spi.demo=two 即使用SpiDemoImplOne實作類
如果設定protocol://host:port/name?spi.demo=one 即使用SpiDemoImplTwo實作類
Activate注解表示一個擴充是否被激活(使用),可以放在類定義和方法(本文不講)上,dubbo将它标注在spi的擴充類上,表示這個擴充實作激活條件和時機。它有兩個設定過濾條件的字段,group,value 都是字元數組。 用來指定這個擴充類在什麼條件下激活
有關DUBBO相關SPI使用可參考:https://www.jianshu.com/p/dc616814ce98
3.2、dubbo原了解析
dubbo擴充的核心代碼如下:
1.ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name);
2.ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension();
3.ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);
**getExtensionLoader方法 **
Dubbo SPI為每一個SPI接口都建立一個ExtensionLoader并放入對應的緩存中,每次擷取都先從緩存中擷取對應的Loader 沒有則建立一個新的并放入緩存中,其中擷取擴充有三種類型
- 一種是普通類型的擴充實作類擷取,直接通過class執行個體化
- 一種是自适應的擴充實作類擷取,主要是通過DubboURL中的參數動态擷取實作類,對應的類或者方法會使用@Adaptive注解修飾,使用該注解修飾的類dubbo編譯過程中會生成XXXx$adaptive代理類。
- 一種是選擇符合dub boUrl的某種條件的一組接口實作類,這些類使用@Adative注解修飾,主要在dubbo的過濾器中使用較多。
3.2.1、getExtension方法
和getExtensionLoader類似 先從緩存中擷取@SPI注解value對應的接口實作,沒有則調用createExtension()建立
createExtension方法
private T createExtension(String name) {
//從我們上面說的三個目錄中加載接口實作類的Class 并按照名字擷取
//擷取class屬性比較複雜此處額外講解
Class<?> clazz = getExtensionClasses().get(name);
//找不到抛出異常
if (clazz == null) {
throw findException(name);
}
try {
//從緩存中擷取執行個體對象 沒有反射建立并放入緩存中
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
//使用IOC的形式為該是執行個體通過setXXX()形式進行依賴注入
injectExtension(instance);
//如果有包裝類(以Wrapper結尾)擷取進行對該實力進行包裝(一些通用的邏輯可以放在包裝類中 因為包裝類都會持有
// 原始對象執行完後會繼續調用原始對象)
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
//對包裝類進行執行個體化和依賴注入
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
//異常處理
}
}
該方法中包含了擷取接口執行個體的主要流程。包括class執行個體擷取緩存擷取,反射執行個體化,依賴注入,包裝類包裝整體流程會在源碼分析完成後整體屬性。下面我們來關注一下如何擷取到符合條件的class的getExtensionClasses,這個是我們進行接口執行個體化的基礎。
WrapperClass - AOP
包裝類是因為一個擴充接口可能有多個擴充實作類,而這些擴充實作類會有一個相同的或者公共的邏輯,如果每個實作類都寫一遍代碼就重複了,此處dubbo設定出來了包裝類統一的通用邏輯在此處實作類似于spring的AOP思想。
**injectExtension -IOC **
該方法主要用于将執行個體化的接口實作類的相關依賴給注入進來,類似于spring的IOC。
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
//針對實作類的使用公有的setXXX(xxx)方法注入相關依賴
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
Class<?> pt = method.getParameterTypes()[0];
try {
//通過setXXX 擷取對應的屬性名xxx
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
//調用Objecttfactory去擷取屬性值(objectFactory擷取也使用了SPI機制)
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
//反射調用填充資料
method.invoke(instance, object);
}
} catch (Exception e) {
//省略錯誤異常處理
}
}
}
}
} catch (Exception e) {
//省略錯誤異常處理
}
return instance;
}
injectExtension 方法将屬性通過set方法注入,擷取屬性值的方式是使用Objectfactory.getExtension() 如果和spring整合則會使用實作類SpringExtensionFactory對象從spring容器中擷取對象
public <T> T getExtension(Class<T> type, String name) {
for (ApplicationContext context : contexts) {
if (context.containsBean(name)) {
Object bean = context.getBean(name);
if (type.isInstance(bean)) {
return (T) bean;
}
}
}
return null;
}
getExtensionClasses方法
套路一樣緩存中擷取沒有則調用loadExtensionClasses()方法加載,該方法加載會調用loadDirectory()方法從我們上面說的三個目錄分别加載對應目錄下的配置資訊并通過loadResource()方法進行全部配置檔案加載最終通過loadClass()方法加載具體的class
loadClass()
/**
* 加載所有使用@SPI注解修飾在配置檔案中聲明的接口實作class
* @param extensionClasses 解析出來的擴充類緩存集合
* @param resourceURL 對應某個spi配置檔案(在該方法中沒作用隻是用來日志列印)
* @param clazz 擴充類實作
* @param name 擴充類的名字 key(name)=value(clazz的名字)
*/
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + "is not subtype of interface.");
}
//如果該類被Adaptive注解修飾,則将該類存放cachedAdaptiveClass中
// 這種機制應該是@Adaptive注解隻能修飾一個接口類型實作類
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
//如果是包裝類則wrappers中加入
} else if (isWrapperClass(clazz)) {
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} else {
//普通類
clazz.getConstructor();
//擷取名字,沒有生成(怎麼可能沒有名字,不都是鍵值對嗎?)
if (name == null || name.length() == 0) {
name = findAnnotationName(clazz);
if (name == null || name.length() == 0) {
if (clazz.getSimpleName().length() > type.getSimpleName().length()
&& clazz.getSimpleName().endsWith(type.getSimpleName())) {
name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase();
} else {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
}
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
//如果類上使用@Activate注解修飾則将該類頁也放入cachedActivates緩存中
//之後将name 和class作為key-value存放到extensionClasses集合中
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
} else if (c != clazz) {
//省略異常
}
}
}
}
}
private boolean isWrapperClass(Class<?> clazz) {
try {
//type 是對應的接口類對象class clazz的屬性中包含接口對象則說明該類是一個包裝類
//在dubbo中所有的Wrapper類都會持有一個接口類對象
clazz.getConstructor(type);
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
該方法中主要針對class的不同類型将其放入不同的緩存對象中,有四種緩存class類型。
- 對于使用@Adaptive注解修飾的類放入cachedAdaptiveClass對象中,每一個接口類隻能有一個實作類使用@Adaptive注解修飾,使用該注解修飾getAdaptiveExtensions則不會根據dubbo URL中參數選擇實作類,而是使用該注解修飾的類實作。
- 對于包裝類将其放入cachedWrapperClasses清單中
- 普通類存放到extensionClasses中,如果該類也被@Activate注解修飾也會将其放入cachedActivates map集合中
3.2.2、getAdaptiveExtension()方法
與getExtension()方法類似先從其對應的緩存中擷取,沒有調用createAdaptiveExtension()方法
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
private Class<?> getAdaptiveExtensionClass() {
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
getAdaptiveExtensionClass() 擷取對應的class類,擷取的類有兩種方式一種是之前使用@Adaptive注解修飾在類上 通過getExtensionClasses()方法存放到cachedAdaptiveClass中的class,另一種是使用使用@Adaptive注解修飾在方法上,這裡Dubbo架構會自動生成XXXX#Adaptive代理類 如下:
public class SpiDemo$Adaptive implements com.xiu.dubbo.service.SpiDemo {
public void getSpi(com.alibaba.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
//根據dubboURL中的參數test://123.5.5.5/test?spi.demo=two 生成一個名字 根據參數類型動态擷取自适應的擴充實作
String extName = url.getParameter("spi.demo", "two");
if(extName == null) {
throw
new IllegalStateException("Fail to get extension(com.xiu.dubbo.service.SpiDemo)" +
" name from url(" + url.toString() + ") use keys([spi.demo])");
}
com.xiu.dubbo.service.SpiDemo extension = (com.xiu.dubbo.service.SpiDemo)ExtensionLoader.getExtensionLoader(
com.xiu.dubbo.service.SpiDemo.class).getExtension(extName);
extension.getSpi(arg0);
}
}
這裡就展現出了dubbo提供的自适應擴充,它擷取實作類可以通過Dubbo 的URL中的參數動态選擇實作類,進而更加靈活。後面的流程也和getExtension類似,根據class空構造器建立執行個體對象,injectExtension()spring形式注入相關依賴對象。
3.2.2、getActivateExtension()方法
擷取使用@Activate注解修飾的符合條件的一組接口實作。
/**
* 根據dubboURl的參數和分組資訊 篩選使用@Activate注解修飾的一組接口實作
* @param url dubboURL
* @param values 參數對應的值清單
* @param group 分組資訊
* @return 符合條件的一組接口實作清單
*/
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<T>();
//将DUBBO的URL中查詢條件的值作為names清單用于查找符合的實作
List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
//先根據分組擷取符合條件的(且不在nams中的防止重複擷取)
if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
//所有使用注解@Activate修飾的類會緩存到cachedActivates中
getExtensionClasses();
for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Activate activate = entry.getValue();
//符合分組且不在names中
if (isMatchGroup(group, activate.group())) {
T ext = getExtension(name);
if (!names.contains(name)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
&& isActive(activate, url)) {
exts.add(ext);
}
}
}
//排序
Collections.sort(exts, ActivateComparator.COMPARATOR);
}
List<T> usrs = new ArrayList<T>();
//查找符合names屬性的實作
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
if (Constants.DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
T ext = getExtension(name);
usrs.add(ext);
}
}
}
//彙總到一起傳回
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}
3.3、DUBBO spi執行流程
