前言
我們在往期文章中,曾經深入分析過Java的SPI機制,它是一種服務發現機制。具體詳見:深入理解JDK的SPI機制
在繼續深入Dubbo之前,我們必須先要明白Dubbo中的SPI機制。因為有位大神(佚名)曾這樣說過:
要想理解Dubbo,必須要先搞明白Dubbo SPI機制,不然會非常暈。
一、背景
1、來源
Dubbo 的擴充套件點載入從 JDK 標準的 SPI (Service Provider Interface) 擴充套件點發現機制加強而來。但還有所不同,它改進了JDK標準的 SPI的以下問題:
-
JDK 標準的 SPI 會一次性例項化擴充套件點所有實現,如果有擴充套件實現初始化很耗時,但如果沒用上也載入,會很浪費資源。
-
如果擴充套件點載入失敗,連擴充套件點的名稱都拿不到了。比如:JDK 標準的 ScriptEngine,通過 getName() 獲取指令碼型別的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導致 RubyScriptEngine 類載入失敗,這個失敗原因被吃掉了,和 ruby 對應不起來,當使用者執行 ruby 指令碼時,會報不支援 ruby,而不是真正失敗的原因。
-
增加了對擴充套件點 IoC 和 AOP 的支援,一個擴充套件點可以直接 setter 注入其它擴充套件點。
2、約定
在擴充套件類的 jar 包內,放置擴充套件點配置檔案 META-INF/dubbo/介面全限定名
,內容為:配置名=擴充套件實現類全限定名
,多個實現類用換行符分隔。
3、配置檔案
Dubbo SPI 所需的配置檔案需放置在 META-INF/dubbo 路徑下,幾乎所有的功能都有擴充套件點實現。
我們以Protocol介面為例,它裡面有很多實現。
二、Dubbo SPI
通過上圖我們可以看到,Dubbo SPI和JDK SPI配置的不同,在Dubbo SPI中可以通過鍵值對的方式進行配置,這樣就可以按需載入指定的實現類。
Dubbo SPI的相關邏輯都被封裝到ExtensionLoader
類中,通過ExtensionLoader
我們可以載入指定的實現類,一個擴充套件介面就對應一個ExtensionLoader
物件,在這裡我們把它親切的稱為:擴充套件點載入器。
我們先看下它的屬性:
public class ExtensionLoader<T> {
//擴充套件點配置檔案的路徑,可以從3個地方載入到擴充套件點配置檔案
private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
//擴充套件點載入器的集合
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
//擴充套件點實現的集合
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
//擴充套件點名稱和實現的對映快取
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<Class<?>, String>();
//擴充點實現類集合快取
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>();
//擴充套件點名稱和@Activate的對映快取
private final Map<String, Activate> cachedActivates = new ConcurrentHashMap<String, Activate>();
//擴充套件點實現的快取
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();
}
複製程式碼
ExtensionLoader會把不同的擴充套件點配置和實現都快取起來。同時,Dubbo在官網上也給了我們提醒:擴充套件點使用單一例項載入(請確保擴充套件實現的執行緒安全性),快取在 ExtensionLoader
中。下面我們看幾個重點方法。
1、獲取擴充套件點載入器
我們首先通過ExtensionLoader.getExtensionLoader()
方法獲取一個 ExtensionLoader 例項,它就是擴充套件點載入器。然後再通過 ExtensionLoader 的 getExtension 方法獲取擴充類物件。這其中,getExtensionLoader 方法用於從快取中獲取與擴充類對應的 ExtensionLoader,若快取未命中,則建立一個新的例項。
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null)
throw new IllegalArgumentException("Extension type == null");
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
複製程式碼
比如你可以通過下面這樣,來獲取Protocol介面的ExtensionLoader例項:
ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
就可以拿到擴充套件點載入器的物件例項:
com.alibaba.dubbo.common.extension.ExtensionLoader[com.alibaba.dubbo.rpc.Protocol]
2、獲取擴充套件類物件
上一步我們已經拿到載入器,然後可以根據載入器例項,通過擴充套件點的名稱獲取擴充套件類物件。
public T getExtension(String name) {
//校驗擴充套件點名稱的合法性
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
// 獲取預設的擴充實現類
if ("true".equals(name)) {
return getDefaultExtension();
}
//用於持有目標物件
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
複製程式碼
它先嚐試從快取中獲取,未命中則建立擴充套件物件。那麼它的建立過程是怎樣的呢?
private T createExtension(String name) {
//從配置檔案中獲取所有的擴充套件類,Map資料結構
//然後根據名稱獲取對應的擴充套件類
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);
}
//注入依賴
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 包裝為Wrapper例項
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
type + ") could not be instantiated: " + t.getMessage(), t);
}
}
複製程式碼
這裡的重點有兩個,依賴注入和Wrapper包裝類,它們是Dubbo中IOC 與 AOP 的具體實現。
2.1、依賴注入
向擴充物件中注入依賴,它會獲取類的所有方法。判斷方法是否以 set 開頭,且方法僅有一個引數,且方法訪問級別為 public,就通過反射設定屬性值。所以說,Dubbo中的IOC僅支援以setter方式注入。
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
Class<?> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
複製程式碼
2.2、Wrapper
它會將當前 instance 作為引數傳給 Wrapper 的構造方法,並通過反射建立 Wrapper 例項。 然後向 Wrapper 例項中注入依賴,最後將 Wrapper 例項再次賦值給 instance 變數。說起來可能比較繞,我們直接看下它最後生成的物件就明白了。 我們以DubboProtocol為例,它包裝後的物件為:
綜上所述,如果我們獲取一個擴充套件類物件,最後拿到的就是這個Wrapper類的例項。 就像這樣:
ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol extension = extensionLoader.getExtension("dubbo");
System.out.println(extension);
複製程式碼
輸出為:com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper@4cdf35a9
3、獲取所有的擴充套件類
在我們通過名稱獲取擴充套件類物件之前,首先需要根據配置檔案解析出所有的擴充套件類。
它是一個擴充套件點名稱和擴充套件類的對映表Map<String, Class<?>>
首先,還是從快取中cachedClasses
獲取,如果沒有就呼叫loadExtensionClasses
從配置檔案中載入。配置檔案有三個路徑:
META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/
先嚐試從快取中獲取。
private Map<String, Class<?>> getExtensionClasses() {
//從快取中獲取
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
//載入擴充套件類
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
複製程式碼
如果沒有,就呼叫loadExtensionClasses
從配置檔案中讀取。
private Map<String, Class<?>> loadExtensionClasses() {
//獲取 SPI 註解,這裡的 type 變數是在呼叫 getExtensionLoader 方法時傳入的
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension
name on extension " + type.getName()+ ": " + Arrays.toString(names));
}
//設定預設的副檔名稱,參考getDefaultExtension 方法
//如果名稱為true,就是呼叫預設擴贊類
if (names.length == 1) cachedDefaultName = names[0];
}
}
//載入指定路徑的配置檔案
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
複製程式碼
以Protocol介面為例,獲取到的實現類集合如下,我們就可以根據名稱載入具體的擴充套件類物件。
{
registry=class com.alibaba.dubbo.registry.integration.RegistryProtocol
injvm=class com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
thrift=class com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
mock=class com.alibaba.dubbo.rpc.support.MockProtocol
dubbo=class com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=class com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
redis=class com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rmi=class com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
}
複製程式碼
三、自適應擴充套件機制
在Dubbo中,很多擴充都是通過 SPI 機制進行載入的,比如 Protocol、Cluster、LoadBalance 等。這些擴充套件並非在框架啟動階段就被載入,而是在擴充套件方法被呼叫的時候,根據URL物件引數進行載入。 那麼,Dubbo就是通過自適應擴充套件機制來解決這個問題。
自適應擴充機制的實現邏輯是這樣的: 首先 Dubbo 會為擴充介面生成具有代理功能的程式碼。然後通過 javassist 或 jdk 編譯這段程式碼,得到 Class 類。最後再通過反射建立代理類,在代理類中,就可以通過URL物件的引數來確定到底呼叫哪個實現類。
1、Adaptive註解
在開始之前,我們有必要先看一下與自適應擴充息息相關的一個註解,即 Adaptive 註解。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
String[] value() default {};
}
複製程式碼
從上面的程式碼中可知,Adaptive 可註解在類或方法上。
- 標註在類上 Dubbo 不會為該類生成代理類。
- 標註在方法上 Dubbo 則會為該方法生成代理邏輯,表示當前方法需要根據 引數URL 呼叫對應的擴充套件點實現。
2、獲取自適應擴充類
getAdaptiveExtension 方法是獲取自適應擴充的入口方法。
public T getAdaptiveExtension() {
// 從快取中獲取自適應擴充
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if (createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
//未命中快取,則建立自適應擴充,然後放入快取
if (instance == null) {
try {
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create
adaptive instance: " + t.toString(), t);
}
}
}
}
}
return (T) instance;
}
複製程式碼
getAdaptiveExtension
方法首先會檢查快取,快取未命中,則呼叫 createAdaptiveExtension
方法建立自適應擴充。
private T createAdaptiveExtension() {
try {
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("
Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}
}
複製程式碼
這裡的程式碼較少,呼叫 getAdaptiveExtensionClass
方法獲取自適應擴充 Class 物件,然後通過反射例項化,最後呼叫injectExtension
方法向擴充例項中注入依賴。
獲取自適應擴充套件類過程如下:
private Class<?> getAdaptiveExtensionClass() {
//獲取當前介面的所有實現類
//如果某個實現類標註了@Adaptive,此時cachedAdaptiveClass不為空
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
//以上條件不成立,就建立自適應擴充類
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
複製程式碼
在上面方法中,它會先獲取當前介面的所有實現類,如果某個實現類標註了@Adaptive
,那麼該類就被賦值給cachedAdaptiveClass
變數並返回。如果沒有,就呼叫createAdaptiveExtensionClass
建立自適應擴充類。
3、建立自適應擴充類
createAdaptiveExtensionClass
方法用於生成自適應擴充類,該方法首先會生成自適應擴充類的原始碼,然後通過 Compiler 例項(Dubbo 預設使用 javassist 作為編譯器)編譯原始碼,得到代理類 Class 例項。
private Class<?> createAdaptiveExtensionClass() {
//構建自適應擴充程式碼
String code = createAdaptiveExtensionClassCode();
ClassLoader classLoader = findClassLoader();
// 獲取編譯器實現類 這個Dubbo預設是採用javassist
Compiler compiler =ExtensionLoader.getExtensionLoader(Compiler.class).getAdaptiveExtension();
//編譯程式碼,返回類例項的物件
return compiler.compile(code, classLoader);
}
複製程式碼
在生成自適應擴充套件類之前,Dubbo會檢查介面方法是否包含@Adaptive
。如果方法上都沒有此註解,就要丟擲異常。
if (!hasAdaptiveAnnotation){
throw new IllegalStateException(
"No adaptive method on extension " + type.getName() + ",
refuse to create the adaptive class!");
}
複製程式碼
我們還是以Protocol介面為例,它的export()
和refer()
方法,都標註為@Adaptive
。destroy
和 getDefaultPort
未標註 @Adaptive
註解。Dubbo 不會為沒有標註 Adaptive 註解的方法生成代理邏輯,對於該種型別的方法,僅會生成一句丟擲異常的程式碼。
package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.extension.Adaptive;
import com.alibaba.dubbo.common.extension.SPI;
@SPI("dubbo")
public interface Protocol {
int getDefaultPort();
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
}
複製程式碼
所以說當我們呼叫這兩個方法的時候,會先拿到URL物件中的協議名稱,再根據名稱找到具體的擴充套件點實現類,然後去呼叫。下面是生成自適應擴充套件類例項的原始碼:
package com.viewscenes.netsupervisor.adaptive;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
import com.alibaba.dubbo.rpc.Exporter;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Protocol;
import com.alibaba.dubbo.rpc.RpcException;
public class Protocol$Adaptive implements Protocol {
public void destroy() {
throw new UnsupportedOperationException(
"method public abstract void Protocol.destroy() of interface Protocol is not adaptive method!");
}
public int getDefaultPort() {
throw new UnsupportedOperationException(
"method public abstract int Protocol.getDefaultPort() of interface Protocol is not adaptive method!");
}
public Exporter export(Invoker invoker)throws RpcException {
if (invoker == null) {
throw new IllegalArgumentException("Invoker argument == null");
}
if (invoker.getUrl() == null) {
throw new IllegalArgumentException("Invoker argument getUrl() == null");
}
URL url = invoker.getUrl();
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null) {
throw new IllegalStateException("Fail to get extension(Protocol) name from url("
+ url.toString() + ") use keys([protocol])");
}
Protocol extension = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);
return extension.export(invoker);
}
public Invoker refer(Class clazz,URL ur)throws RpcException {
if (ur == null) {
throw new IllegalArgumentException("url == null");
}
URL url = ur;
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null) {
throw new IllegalStateException("Fail to get extension(Protocol) name from url("+ url.toString() + ") use keys([protocol])");
}
Protocol extension = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);
return extension.refer(clazz, url);
}
}
複製程式碼
綜上所述,當我們獲取某個介面的自適應擴充套件類,實際就是一個Adaptive
類例項。
ExtensionLoader<Protocol> extensionLoader =
ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol adaptiveExtension = extensionLoader.getAdaptiveExtension();
System.out.println(adaptiveExtension);
複製程式碼
輸出為:
com.alibaba.dubbo.rpc.Protocol$Adaptive@47f6473
四、例項
我們看完以上流程之後,如果想寫一套自己的邏輯替換Dubbo中的流程,就變得很簡單。
Dubbo預設使用dubbo
協議來暴露服務。我們可以搞一個自定義的協議來替換它。
1、實現類
首先,我們建立一個MyProtocol
類,它實現Protocol介面。
package com.viewscenes.netsupervisor.protocol;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.rpc.Exporter;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Protocol;
import com.alibaba.dubbo.rpc.RpcException;
import com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol;
public class MyProtocol extends DubboProtocol implements Protocol{
public int getDefaultPort() {
return 28080;
}
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
URL url = invoker.getUrl();
System.out.println("自定義協議,進行服務暴露:"+url);
return super.export(invoker);
}
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
return super.refer(type, url);
}
public void destroy() {}
}
複製程式碼
2、擴充套件點配置檔案
然後,在自己的專案中META-INF/services
建立com.alibaba.dubbo.rpc.Protocol
檔案,檔案內容為:
myProtocol=com.viewscenes.netsupervisor.protocol.MyProtocol
3、修改Dubbo配置檔案
最後修改生產者端的配置檔案:
<!-- 用自定義協議在20880埠暴露服務 -->
<dubbo:protocol name="myProtocol" port="20880"/>
複製程式碼
這樣在我們啟動生產者端專案的時候,Dubbo在進行服務暴露的過程中,就會呼叫到我們自定義的MyProtocol
類,完成相應的邏輯處理。