Dubbo原始碼分析(三)Dubbo中的SPI和自適應擴充套件機制

清幽之地發表於2019-03-19

前言

我們在往期文章中,曾經深入分析過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 路徑下,幾乎所有的功能都有擴充套件點實現。

Dubbo擴充套件點配置檔案

我們以Protocol介面為例,它裡面有很多實現。

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包裝物件

綜上所述,如果我們獲取一個擴充套件類物件,最後拿到的就是這個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()方法,都標註為@AdaptivedestroygetDefaultPort未標註 @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類,完成相應的邏輯處理。

相關文章