dubbo源碼分析系列(1)擴展機制的實現

#1 系列目錄java

#2 SPI擴展機制git

站在一個框架做者的角度來講,定義一個接口,本身默認給出幾個接口的實現類,同時容許框架的使用者也可以自定義接口的實現。如今一個簡單的問題就是:如何優雅的根據一個接口來獲取該接口的全部實現類呢?github

這就須要引出java的SPI機制了緩存

##2.1 SPI介紹與demo服務器

這些內容就再也不多說了,網上搜一下,一大堆,具體能夠參考這篇博客Java SPI機制簡介;app

我這裏給出一個簡單的demo:框架

定義一個接口:com.demo.dubbo.demo.spi.service.HelloServicejvm

接口的實現類:ide

com.demo.dubbo.demo.spi.service.impl.DefaultHelloService
com.demo.dubbo.demo.spi.service.impl.CustomHelloService

而後在類路徑下,建立META-INF/services/com.demo.dubbo.demo.spi.service.HelloService文件,內容以下:函數

com.demo.dubbo.demo.spi.service.impl.DefaultHelloService
com.demo.dubbo.demo.spi.service.impl.CustomHelloService

總體結構以下圖所示:

SPI結構示例

使用方式以下:

ServiceLoader<HelloService> helloServiceLoader=ServiceLoader.load(HelloService.class);
for(HelloService item:helloServiceLoader){
	item.hello();
}

##2.2 ServiceLoader的源碼分析

從上面能夠看到,先根據ServiceLoader的load靜態方法根據目標接口加載出一個ServiceLoader實例,而後能夠遍歷這個實例(實現了Iterable接口),獲取到接口的全部實現類

來看下ServiceLoader的幾個重要屬性:

要加載的接口
private Class<S> service;

// The class loader used to locate, load, and instantiate providers
private ClassLoader loader;

// 用於緩存已經加載的接口實現類,其中key爲實現類的完整類名
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 用於延遲加載接口的實現類
private LazyIterator lookupIterator;

首先第一步:獲取一個ServiceLoader<HelloService> helloServiceLoader實例,此時尚未進行任何接口實現類的加載操做,屬於延遲加載類型的。只是建立了LazyIterator lookupIterator對象而已。

第二步:ServiceLoader實現了Iterable接口,即實現了該接口的iterator()方法,實現內容以下:

public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

for循環遍歷ServiceLoader的過程其實就是調用上述hasNext()和next()方法的過程

第一次循環遍歷會使用lookupIterator去查找,以後就緩存到providers中。LazyIterator會去加載類路徑下/META-INF/services/接口全稱 文件的url地址,使用以下代碼來加載:

String fullName = "META-INF/services/" + service.getName();
loader.getResources(fullName)

文件加載並解析完成以後,獲得一系列的接口實現類的完整類名,調用next()方法時纔回去真正執行接口實現類的加載操做,並根據無參構造器建立出一個實例,存到providers中;

以後再次遍歷ServiceLoader,就直接遍歷providers中的數據

##2.3 ServiceLoader缺點分析

  • 雖然ServiceLoader也算是使用的延遲加載,可是基本只能經過遍歷所有獲取,也就是接口的實現類所有加載並實例化一遍。若是你並不想用某些實現類,它也被加載並實例化了,這就形成了浪費。

  • 獲取某個實現類的方式不夠靈活,只能經過Iterator形式獲取,不能根據某個參數來獲取對應的實現類

#3 dubbo的擴展機制

##3.1 簡單功能介紹

dubbo的擴展機制和java的SPI機制很是類似,可是又增長了以下功能:

  • 1 能夠方便的獲取某一個想要的擴展實現,java的SPI機制就沒有提供這樣的功能

  • 2 對於擴展實現IOC依賴注入功能:

    舉例來講:接口A,實現者A一、A2。接口B,實現者B一、B2。

    如今實現者A1含有setB()方法,會自動注入一個接口B的實現者,此時注入B1仍是B2呢?都不是,而是注入一個動態生成的接口B的實現者B$Adpative,該實現者可以根據參數的不一樣,自動引用B1或者B2來完成相應的功能

  • 3 對擴展采用裝飾器模式進行功能加強,相似AOP實現的功能

    仍是以上面的例子,接口A的另外一個實現者AWrapper1。大致內容以下:

    private A a;
    AWrapper1(A a){
    	this.a=a;
    }

    所以,咱們在獲取某一個接口A的實現者A1的時候,已經自動被AWrapper1包裝了。

##3.2 dubbo的ExtensionLoader解析擴展過程

如下面的例子爲例來分析下:

ExtensionLoader<Protocol> protocolLoader=ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol  protocol=protocolLoader.getAdaptiveExtension();

其中Protocol接口定義以下:

@Extension("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();

}

對應的實現者以下:

Protocol實現者

第一步:根據要加載的接口建立出一個ExtensionLoader實例

ExtensionLoader中含有一個靜態屬性:

ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

用於緩存全部的擴展加載實例,這裏加載Protocol.class,就以Protocol.class爲key,建立的ExtensionLoader爲value存儲到上述EXTENSION_LOADERS中

這裏沒有進行任何的加載操做。

咱們先來看下,ExtensionLoader實例是如何來加載Protocol的實現類的:

  • 1 先解析Protocol上的Extension註解的name,存至String cachedDefaultName屬性中,做爲默認的實現

  • 2 到類路徑下的加載 META-INF/services/com.alibaba.dubbo.rpc.Protocol文件

    加載Protocol擴展

    該文件的內容以下:

    com.alibaba.dubbo.registry.support.RegistryProtocol
    com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
    com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
    com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
    com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
    com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
    com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol

    而後就是讀取每一行內容,加載對應的class。

  • 3 對於上述class分紅三種狀況來處理

    對於一個接口的實現者,ExtensionLoader分三種狀況來分別存儲對應的實現者,屬性分別以下:

    Class<?> cachedAdaptiveClass;
    Set<Class<?>> cachedWrapperClasses;
    Reference<Map<String, Class<?>>> cachedClasses;

    狀況1: 若是這個class含有Adaptive註解,則將這個class設置爲Class<?> cachedAdaptiveClass。

    狀況2: 嘗試獲取帶對應接口參數的構造器,若是可以獲取到,則說明這個class是一個裝飾類即,須要存到Set<Class<?>> cachedWrapperClasses中

    狀況3: 若是沒有上述構造器。則獲取class上的Extension註解,根據該註解的定義的name做爲key,存至Reference<Map<String, Class<?>>> cachedClasses結構中

至此,解析文件過程結束。

以Protocol爲例來詳細介紹下整個過程:

  • 1 解析Protocol上的Extension註解的name

    @Extension("dubbo")
    public interface Protocol{
    	//略
    }

    因此cachedDefaultName值爲dubbo。

  • 2 解析類路徑下的加載 META-INF/services/com.alibaba.dubbo.rpc.Protocol文件

    如DubboProtocol:

    @Extension(DubboProtocol.NAME)
    public class DubboProtocol extends AbstractProtocol {
    	//略
    }

    沒有Adaptive註解,同時只有無參構造器,因此只能存放到Reference<Map<String, Class<?>>> cachedClasses中,key就是上述DubboProtocol.NAME即dubbo。

    如ProtocolFilterWrapper:

    public class ProtocolFilterWrapper implements Protocol {
    
        private final Protocol protocol;
    
        public ProtocolFilterWrapper(Protocol protocol){
            if (protocol == null) {
                throw new IllegalArgumentException("protocol == null");
            }
            this.protocol = protocol;
        }
    }

    含有Protocol參數的構造器,做爲一個裝飾類,存放至Set<Class<?>> cachedWrapperClasses中

    同理ProtocolListenerWrapper:

    public class ProtocolListenerWrapper implements Protocol {
    
        private final Protocol protocol;
    
        public ProtocolListenerWrapper(Protocol protocol){
            if (protocol == null) {
                throw new IllegalArgumentException("protocol == null");
            }
            this.protocol = protocol;
        }
    }

    含有Protocol參數的構造器,做爲一個裝飾類,存放至Set<Class<?>> cachedWrapperClasses中。

##3.3 dubbo的ExtensionLoader獲取擴展的過程

以獲取DubboProtocol爲例

ExtensionLoader<Protocol> protocolLoader=ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol dubboProtocol=protocolLoader.getExtension(DubboProtocol.NAME);

獲取過程以下:

private T createExtension(String name) {
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = injectExtension((T) clazz.newInstance());
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && wrapperClasses.size() > 0) {
            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);
    }
}

大體分紅4步:

  • 1 根據name獲取對應的class

    首先獲取ExtensionLoader<Protocol>對象的Reference<Map<String, Class<?>>> cachedClasses屬性,若是爲空則表示尚未進行解析,則開始進行上面的解析。解析完成以後,根據name獲取對應的class,這裏便獲取到了DubboProtocol.class

  • 2 根據獲取到的class建立一個實例

  • 3 對獲取到的實例,進行依賴注入

  • 4 對於上述通過依賴注入的實例,再次進行包裝。即遍歷Set<Class<?>> cachedWrapperClasses中每個包裝類,分別調用帶Protocol參數的構造函數建立出實例,而後一樣進行依賴注入

    以Protocol爲例,cachedWrapperClasses中存着上述提到過的ProtocolFilterWrapper、ProtocolListenerWrapper。分別會對DubboProtocol實例進行包裝,這個比較好理解的

下面對於這個依賴注入的過程就要詳細的說明下,來看下這個過程:

private T injectExtension(T instance) {
    try {
        for (Method method : instance.getClass().getMethods()) {
            if (method.getName().startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())) {
                Class<?> pt = method.getParameterTypes()[0];
                if (pt.isInterface() && getExtensionLoader(pt).getSupportedExtensions().size() > 0) {
                    try {
                        Object adaptive = getExtensionLoader(pt).getAdaptiveExtension();
                        method.invoke(instance, adaptive);
                    } 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;
}

從上面能夠看到,進行注入的條件以下:

  • set開頭的方法
  • 方法的參數只有一個
  • 方法必須是public
  • 方法的參數必須是接口,而且是ExtensionLoader可以獲取其擴展類

咱們知道一個接口的實現者可能有多個,此時到底注入哪個呢?

此時採起的策略是,並不去注入一個具體的實現者,而是注入一個動態生成的實現者,這個動態生成的實現者的邏輯是肯定的,可以根據不一樣的參數來使用不一樣的實現者實現相應的方法。這個動態生成的實現者的class就是ExtensionLoader的Class<?> cachedAdaptiveClass

以Protocol爲例,動態生成的Protocol實現者大概以下:

class Protocol$Adpative implements Protocol{
	public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.RpcException{
	    if (arg0 == null)  { 
	        throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null"); 
	    }
	    if (arg0.getUrl() == null) { 
	        throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null"); 
	    }
	    com.alibaba.dubbo.common.URL url = arg0.getUrl();
	    String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
	    if(extName == null) {
	        throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])"); 
	    }
	    com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)com.alibaba.dubbo.common.ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
	    return extension.export(arg0);
	}

	public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0,com.alibaba.dubbo.common.URL arg1) throws com.alibaba.dubbo.rpc.RpcException{
	    if (arg1 == null)  { 
	        throw new IllegalArgumentException("url == null"); 
	    }
	    com.alibaba.dubbo.common.URL url = arg1;
	    String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
	    if(extName == null) {
	        throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])"); 
	    }
	    com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)com.alibaba.dubbo.common.ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
	    return extension.refer(arg0, arg1);
	}

	public void destroy(){
	    throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
	}
}

從上面的代碼中能夠看到,Protocol$Adpative是根據URL參數中protocol屬性的值來選擇具體的實現類的。

如值爲dubbo,則從ExtensionLoader<Protocol>中獲取dubbo對應的實例,即DubboProtocol實例

如值爲hessian,則從ExtensionLoader<Protocol>中獲取hessian對應的實例,即HessianProtocol實例

也就是說Protocol$Adpative可以根據url中的protocol屬性值動態的採用對應的實現。

對於上述獲取動態實現者即Protocol$Adpative的過程還須要補充一些細節內容:

  • 1 要求對應的接口中的某些方法必須含有Adaptive註解,沒有Adaptive註解,則表示不須要生成動態類
  • 2 對於接口的方法中不含Adaptive註解的,所有是不可調用的,如上述的destroy()方法
  • 3 含有Adaptive註解的方法必須含有URL類型的參數,或者可以獲取到URL,分別如上述的refer方法和export方法
  • 4 從URL中根據什麼參數來獲取實現者信息呢?以Protocol爲例,參數就爲"protocol",默認是接口簡單名稱首字母小寫或者接口中指定的默認實現,對於別的接口,咱們從url的哪一個參數中獲取對應的實現者呢?這就能夠從Adpative註解中給出,下面給出一個Transporter例子

Transporter接口內容以下:

@Extension("netty")
public interface Transporter {

    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
    Server bind(URL url, ChannelHandler handler) throws RemotingException;

    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;

}

接口Transporter指定的默認實現是"netty",同時@Adaptive註解中又給出了"client"和"transporter"。

因此獲取實現的過程以下:

public com.alibaba.dubbo.remoting.Client connect(com.alibaba.dubbo.common.URL arg0,com.alibaba.dubbo.remoting.ChannelHandler arg1) throws com.alibaba.dubbo.remoting.RemotingException{
    if (arg0 == null)  { 
        throw new IllegalArgumentException("url == null"); 
    }
    com.alibaba.dubbo.common.URL url = arg0;
    String extName = url.getParameter("client", url.getParameter("transporter", "netty"));
    if(extName == null) {
        throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString() + ") use keys([client, transporter])"); 
    }
    com.alibaba.dubbo.remoting.Transporter extension = (com.alibaba.dubbo.remoting.Transporter)com.alibaba.dubbo.common.ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.remoting.Transporter.class).getExtension(extName);
    return extension.connect(arg0, arg1);
}

String extName = url.getParameter("client", url.getParameter("transporter", "netty"));

先根據client來獲取,若是獲取不到再根據transporter來獲取,若是還獲取不到,則直接使用Transporter默認指定的netty。

至此,dubbo的ExtensionLoader的內容大概就說完了。

#4 結束語

下一篇文章就開始介紹下,服務器端暴漏服務和向註冊中心註冊服務的過程

相關文章
相關標籤/搜索