文章目錄
上篇文章《【SPI】 — java spi 機制簡介》中, 可以看到,java spi 機制非常簡單, 就是讀取指定的配置文件, 將所有的類都加載到程序中。 而這種機制, 存在很多缺陷, 比如:
- 所有實現類無論是否使用, 直接被加載, 可能存在浪費
- 不能夠靈活控制什麼時候什麼時機, 匹配什麼實現, 功能太弱
Dubbo 基於自己的需要,對SPI 機制進行了增強, 本篇文章將簡單介紹一下 Dubbo 中的 SPI用法。
1 @SPI 標籤 及其使用簡介
與 Java SPI 實現類配置不同, Dubbo SPI 是通過鍵值對的方式進行配置, 這樣我們就可以按需加載指定的實現類。
@SPI註解的玩法如下:
- (1)在某個接口上標註@SPI註解,表明此接口是 SPI 的擴展點
@SPI("b") //指定缺省實現類的別名
public interface InfoService {
Object sayHello(String name);
}
- (2)在META-INF下的dubbo文件夾下指定接口與實現類之間的映射關係,具體配置如下:
-(3)簡單看一下某一個實現類的源代碼:
public class InfoServiceAImpl implements InfoService {
@Override
public Object sayHello(String name) {
System.out.println(name + ",你好,調通了A實現!");
return name + ",你好,調通了A實現!";
}
}
- (4)通過(2)中指定的實現類的key獲取具體的實現類
/**
* dubbo spi類加載驗證
* extensionLoader.getExtension("a") ---------- 取到對應的擴展類
* extensionLoader.getDefaultExtension() ------ 取得SPI("b")指定的實現類(默認實現類)
*/
@Test
public void dubboSPI() {
//獲取InfoService的 Loader 實例
ExtensionLoader<InfoService> extensionLoader = ExtensionLoader.getExtensionLoader(InfoService.class);
//取得a拓展類
InfoService infoServiceA = extensionLoader.getExtension("a");
infoServiceA.sayHello("yoyo");
//取得b拓展類
InfoService infoServiceB = extensionLoader.getDefaultExtension();
infoServiceB.sayHello("nrsc");
}
- (5)測試結果如下
2 @Adaptive標籤 及其使用簡介
2.1 @Adaptive標註在類上的具體含義詳解 — 將該類作爲最佳適配類
2.1.1 場景介紹
試想假如我寫了一個接口,並想將其作爲dubbo的SPI擴展點,但卻並沒在該接口上標註一個
【缺省實現類的別名】
----> 也就是說沒有指定默認的實現類。
但是我又想讓該擴展點有一個【不得不走的默認實現】
,並且在該默認實現裏我可以拿到其他非默認實現,這該怎麼搞呢???
有了以上需求後,回顧一下1中的用法,做個簡圖如下:
回顧完之後你應該會有這樣的思路:
我可以先通過extensionLoader.getExtension(【不得不走的默認實現】的別名
),獲取到該默認實現類,然後在該默認實現裏再遍歷獲取到所有的非默認實現!!!
是的,可以做到,但是有點不夠優雅:
- 首先,按照這種思路這個
【不得不走的默認實現】的別名
應該得寫死在調用的代碼裏 - 其次,在這個不得不走的默認實現類裏,找其他非默認實現類的的方法還得自己去寫
而@Adaptive註解標註在類上的使用姿勢就優雅地解決了上訴問題。
2.1.2 @Adaptive標註在類上使用姿勢及其意義詳解
使用姿勢介紹
(1)在某個接口上標註@SPI註解,表明此接口是 SPI 的擴展點
(2)在某個具體實現類上標註上@Adaptive 表明該實現類是【不得不走的默認實現類】
,如下:
@Adaptive
public class InfoServiceAImpl implements InfoService {
@Override
public Object sayHello(String name) {
System.out.println(name + ",你好,調通了A實現!");
return name + ",你好,調通了A實現!";
}
}
(3)當然要像1中的(2)一樣,在META-INF下的dubbo文件夾下指定接口與實現類之間的映射關係
(4)測試類如下:
@Test
public void test1() {
//獲取InfoService的ExtensionLoader
ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
InfoService adaptiveExtension = loader.getAdaptiveExtension(); //獲取到@Adaptive註解標註的實現類
System.out.println(adaptiveExtension.sayHello("yoyo"));
InfoService defaultExtension = loader.getDefaultExtension();//獲取接口上@SPI("別名")中別名指定的實現類
System.out.println(defaultExtension.sayHello("nrsc"));
}
(5)通過打斷點的方式來看一下,@Adaptive註解標註在類上如何優雅的解決了我在上面說的問題
由上圖可知,其解決思路如下:
這裏需要注意
的是:
- (1)生成的InfoService的ExtensionLoader會加入到ExtensionLoader中的一個ConcurrentMap緩存起來,因此我們假若在標有的@Adapter註解的實現類裏再獲取InfoService的ExtensionLoader,可以直接從ConcurrentMap緩存裏取
- (2)遍歷並執行所有非@Adapter註解標註的實現類的姿勢如下:
//獲取所有未標註@Adapter註解的實現類的別名,並通過別名獲取到實現類
Set<String> supportedExtensions = loader.getSupportedExtensions();
for (String name : supportedExtensions) {
System.out.println(loader.getExtension(name).sayHello("MM"));
}
2.2 @Adaptive標註在方法上具體含義詳解 — 靜態代理該方法
2.2.1 場景介紹
現在有這樣一個需求,一個接口下里有多個方法,但是我只想將某個方法暴漏給別人,該怎麼搞???
相信大家肯定都知道,解決方式主要有兩種:
- 動態代理
- 靜態代理
而將@Adaptive註解標註在方法上就可以完成對該方法進行靜態代理的工作 —》 據說因爲靜態代理比動態代理效率更高
,所以dubbo採用了靜態代理 —> 有興趣的可以進行考證!!!
2.2.2 @Adaptive標註在方法上的使用姿勢
(1)
在某個接口上標註@SPI註解,表明此接口是 SPI 的擴展點
;
在該接口的某個方法上標註@Adapter註解,表明該方法需要被靜態代理
,
並且注意這個方法必須要有一個參數爲URL
(2)如1中的(2)一樣,在META-INF下的dubbo文件夾下指定接口與實現類之間的映射關係
(3)某一個實現類的源碼:
public class InfoServiceAImpl implements InfoService {
@Override
public Object sayHello(String name) {
System.out.println(name + ",你好,調通了A實現!");
return name + ",你好,調通了A實現!";
}
@Override
public Object passInfo(String msg, URL url) {
System.out.println("恭喜你,調通了A實現:" + url);
return msg;
}
}
(4)測試
【情況1】
各個實現類上都沒有標註@Adaptive註解, 接口的SPI註解爲@SPI(“b”),url無參數的情況
/**
* 各個實現類上都沒有@Adaptive
* SPI上有註解@SPI("b"),
* url無參數
* 則以@SPI("b")爲準,選擇InfoServiceBImpl 實現中的方法進行靜態代理
*/
@Test
public void test1() {
ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
InfoService adaptiveExtension = loader.getAdaptiveExtension();
URL url = URL.valueOf("test://localhost/test");
System.out.println(adaptiveExtension.passInfo("yoyo", url));
}
測試結果如下:
【情況2】
各個實現類上都沒有標註@Adaptive註解, 接口的SPI註解爲@SPI(“b”),URL中有具體的值info.service=a
注意:
url中的info.service是按照接口名稱InfoService拆解的結果,dubbo會按此規則找到InfoService接口別名爲a的實現類,並對此實現類中的方法進行靜態代理
/**
* 各個實現類上都沒有@Adaptive
* SPI上有註解@SPI("b")
* URL中有具體的值info.service=a,
* 則以URL爲準,選擇InfoServiceAImpl 實現中的方法進行靜態代理
*/
@Test
public void test2() {
ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
InfoService adaptiveExtension = loader.getAdaptiveExtension();
URL url = URL.valueOf("test://localhost/test?info.service=a");
System.out.println(adaptiveExtension.passInfo("james", url));
}
測試結果如下:
【情況3】
各個實現類上都沒有標註@Adaptive註解, 接口方法中加上的註解爲@Adaptive({“NRSC”}),URL中有具體的值爲NRSC=c
- 此時InfoService接口應爲:
@SPI("b") //指定缺省實現類的別名
public interface InfoService {
Object sayHello(String name);
@Adaptive({"NRSC"})
//@Adaptive
Object passInfo(String msg, URL url);
}
- 測試類爲:
/**
* 各個實現類上面沒有@Adaptive
* 接口方法中加上的註解爲@Adaptive({"NRSC"}),
* URL中有具體的值NRSC=c,
* 則以URL中的NRSC參數爲準,選擇InfoServiceCImpl 實現中的方法進行靜態代理
*/
@Test
public void test3() {
ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
InfoService adaptiveExtension = loader.getAdaptiveExtension();
URL url = URL.valueOf("test://localhost/test?info.service=a&NRSC=c");
System.out.println(adaptiveExtension.passInfo("james", url));
}
- 測試結果如下:
由此可知這種方式其實是對 【情況2】
的一種更優雅的實現!!!
【情況4】
InfoServiceCImpl實現類上有@Adaptive, 接口方法中加上的註解爲@Adaptive({“NRSC”}),URL隨意傳值
/**
* InfoServiceCImpl實現類上有@Adaptive,URL隨意傳值
* 直接會拿到InfoServiceCImpl的實現類 ---》 並未做靜態代理
*/
@Test
public void test4() {
ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
InfoService adaptiveExtension = loader.getAdaptiveExtension();
URL url = URL.valueOf("test://localhost/test");
System.out.println(adaptiveExtension.passInfo("james", url));
}
測試結果:
2.3 簡單擼一下源碼,看看爲啥 @Adaptive標註在方法上會靜態代理該方法,標註在類上卻不會走靜態代理
打着斷點可以看到源碼中ExtensionLoader中創建具體實現類的關鍵代碼如下:
接下來我們看一下接口的任何實現類都沒有標註@Adapter註解時,dubbo爲我們拼接的代理類,即上圖中code對應的字符串,將其格式化後如下:
package com.nrsc.service;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class InfoService$Adaptive implements com.nrsc.service.InfoService {
public java.lang.Object passInfo(java.lang.String arg0,
org.apache.dubbo.common.URL arg1) {
if (arg1 == null) {
throw new IllegalArgumentException("url == null");
}
org.apache.dubbo.common.URL url = arg1;
String extName = url.getParameter("NRSC", "b");
if (extName == null) {
throw new IllegalStateException(
"Failed to get extension (com.nrsc.service.InfoService) name from url (" +
url.toString() + ") use keys([NRSC])");
}
//通過別名找到具體的實現類
com.nrsc.service.InfoService extension = (com.nrsc.service.InfoService) ExtensionLoader.getExtensionLoader(com.nrsc.service.InfoService.class)
.getExtension(extName);
//拿着實現類調用方法
return extension.passInfo(arg0, arg1);
}
//未標註@Adapter註解的方法無法被調用
public java.lang.Object sayHello(java.lang.String arg0) {
throw new UnsupportedOperationException(
"The method public abstract java.lang.Object com.nrsc.service.InfoService.sayHello(java.lang.String) of interface com.nrsc.service.InfoService is not adaptive method!");
}
}
看到這段字符串,再聯繫一下上篇文章《【dubbo源碼解析】— 通過javassist/JavassistCompiler動態生成一個實例對象》,是不是就可以很清楚的明白下面的結論了:
- @Adapter標籤標註在方法上可以對該方法進行靜態代理
- 如果接口的某個實現類上標註了@Adapter註解,將直接調用該實現類,而不會進行靜態代理
end!!!