Dubbo原始碼分析之服務引用

土豆肉絲蓋澆飯發表於2018-04-23

流程圖

Dubbo原始碼分析之服務引用

這個流程對應我們這次原始碼分析主要內容,不得不說dubbo的文件寫的太好了

時序圖

Dubbo原始碼分析之服務引用

引用服務兩種方式

  1. 直連引用服務

    Dubbo原始碼分析之服務引用

  2. 從註冊中心發現服務

    Dubbo原始碼分析之服務引用

經過debug,這邊refer帶的引數和實際有出入,具體看下面的解析

一些概念

Directory

主要用於獲取Invoker

public interface Directory<T> extends Node {
    //獲取當前Directory對應的介面
    Class<T> getInterface();
    //根據invocaiton獲取對應的Invoker
    List<Invoker<T>> list(Invocation invocation) throws RpcException;
    
}
複製程式碼

這個介面不是擴充套件點,具體實現有StaticDirectory,RegistryDirectory StaticDirectory從名字看出來是靜態的,就是說需要手動對裡面invoker進行增減 而RegistryDirectory對註冊中心目錄增加了監聽,裡面的invoker會隨著提供者的改變而變化

LoadBlance

用於選擇呼叫的invoker


@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {
    //根據不同LoadBalance演算法,從invokers中選擇出一個合適的invoker
    @Adaptive("loadbalance")
	<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;

}
複製程式碼

實現有

random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
leastactive=com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
consistenthash=com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
複製程式碼

分別對應帶權隨機,帶權輪詢,最少活躍數,一致性hash演算法

Cluster

叢集功能,當有多個Invoker時,會把它們偽裝成一個Invoker,提供一些叢集呼叫方式

@SPI(FailoverCluster.NAME)
public interface Cluster {

    //將多個Invoker偽裝成一個Invoker,Invoker從directory獲取
    @Adaptive
    <T> Invoker<T> join(Directory<T> directory) throws RpcException;

}
複製程式碼

實現有

//在配置mock引數配置之後生效,用於服務降級
mock=com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterWrapper
//失敗自動切換,當出現失敗,重試其它伺服器 。通常用於讀操作,但重試會帶來更長延遲。
可通過 retries="2" 來設定重試次數(不含第一次)。
failover=com.alibaba.dubbo.rpc.cluster.support.FailoverCluster
//失敗安全,出現異常時,直接忽略。通常用於寫入審計日誌等操作。
failfast=com.alibaba.dubbo.rpc.cluster.support.FailfastCluster
//失敗自動恢復,後臺記錄失敗請求,定時重發。通常用於訊息通知操作。
failsafe=com.alibaba.dubbo.rpc.cluster.support.FailsafeCluster
//失敗自動恢復,後臺記錄失敗請求,定時重發。通常用於訊息通知操作。
failback=com.alibaba.dubbo.rpc.cluster.support.FailbackCluster
//並行呼叫多個伺服器,只要一個成功即返回。通常用於實時性要求較高的讀操作,但需要浪
費更多服務資源。可通過 forks="2" 來設定最大並行數。
forking=com.alibaba.dubbo.rpc.cluster.support.ForkingCluster
//可用性呼叫,呼叫最先可用的invoker
available=com.alibaba.dubbo.rpc.cluster.support.AvailableCluster
//合併多個呼叫結果的cluster
mergeable=com.alibaba.dubbo.rpc.cluster.support.MergeableCluster
//廣播呼叫所有提供者,逐個呼叫,任意一臺報錯則報錯 。通常用於通知所有提供者更新快取
或日誌等本地資源資訊。
broadcast=com.alibaba.dubbo.rpc.cluster.support.BroadcastCluster
複製程式碼

原始碼分析

解析配置

我們一般引用服務的時候,會配置

    <dubbo:reference id="bidService" interface="com.alibaba.dubbo.demo.bid.BidService"/>
複製程式碼

這個標籤會被DubboNamespaceHandler解析為ReferenceBean

        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));

複製程式碼

那麼ReferenceBean又是怎麼生成com.alibaba.dubbo.demo.bid.BidService型別的代理放到spring容器中的呢?答案是用到了FactoryBean 看下FactoryBean的定義

public interface FactoryBean<T> {
    //返回getObjectType方法對應的物件
    T getObject() throws Exception;
    //返回FactoryBean的型別
    Class<?> getObjectType();

    boolean isSingleton();
}
複製程式碼

實現FactoryBean的介面,可以生產一些其他型別的Bean到Spring容器,型別由getObjectType方法控制,返回物件由getObject方法得到 ReferenceConfig實現了這個介面,那麼在獲取BidService型別bean的時候,會呼叫ReferenceConfig的getObject方法來獲得

在getObject方法中我們會返回ReferenceConfig中的ref屬性,在返回之前會通過init方法先對ref進行初始化,ref其實就是一個代理物件,內部封裝了invoker的呼叫。這個init方法的作用主要是獲取invokers,通過cluster偽裝成一個invoker,並且把invoker轉換為代理物件ref

獲取invoker

獲取invoker以及建立代理對邏輯全在ReferenceConfig的createProxy中 首先我們會判斷我們需要的服務在InjvmProtocol是否存在以及可呼叫

//根據之前解析的引數,構造一個本地jvm呼叫的url
URL tmpUrl = new URL("temp", "localhost", 0, map);
		final boolean isJvmRefer;
        //是否配置injvm引數
        if (isInjvm() == null) {
            if (url != null && url.length() > 0) { //配置直連URL的情況下,不做本地引用
                isJvmRefer = false;
            } else if (InjvmProtocol.getInjvmProtocol().isInjvmRefer(tmpUrl)) {
                //預設情況下如果本地有服務暴露,則引用本地服務.
                isJvmRefer = true;
            } else {
                isJvmRefer = false;
            }
        } else {
            isJvmRefer = isInjvm().booleanValue();
        }
		
		if (isJvmRefer) {
			URL url = new URL(Constants.LOCAL_PROTOCOL, NetUtils.LOCALHOST, 0, interfaceClass.getName()).addParameters(map);
			invoker = refprotocol.refer(interfaceClass, url);
            if (logger.isInfoEnabled()) {
                logger.info("Using injvm service " + interfaceClass.getName());
            }
		}
複製程式碼

如果我們不強制指定injvm引數等於false,如果InjvmProtocol暴露了這個服務,消費者預設會使用本地的

如果不呼叫InjvmProtocol,那麼通過遠端協議得到invoker 首先會對url進行處理

if (url != null && url.length() > 0) { // 使用者指定URL,指定的URL可能是對點對直連地址,也可能是註冊中心URL
                String[] us = Constants.SEMICOLON_SPLIT_PATTERN.split(url);
                if (us != null && us.length > 0) {
                    for (String u : us) {
                        URL url = URL.valueOf(u);
                        if (url.getPath() == null || url.getPath().length() == 0) {
                            url = url.setPath(interfaceName);
                        }
                        if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
                            urls.add(url.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                        } else {
                            urls.add(ClusterUtils.mergeUrl(url, map));
                        }
                    }
                }
            } else { // 通過註冊中心配置拼裝URL
            	List<URL> us = loadRegistries(false);
            	if (us != null && us.size() > 0) {
                	for (URL u : us) {
                	    URL monitorUrl = loadMonitor(u);
                        if (monitorUrl != null) {
                            map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
                        }
                	    urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));
                    }
            	}
            	if (urls == null || urls.size() == 0) {
                    throw new IllegalStateException("No such any registry to reference " + interfaceName  + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
                }
            }
複製程式碼

如果配置了直連url,因為可以配置多個,用分割符分成多個後,可能會存在registry協議的url,會對registry協議url做一些特殊處理,會在refer引數內加上之前儲存的一些呼叫介面的配置鍵值對

registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-consumer&dubbo=2.0.0&organization=dubbox&owner=programmer&pid=34788&refer=application%3Ddemo-consumer%26dubbo%3D2.0.0%26interface%3Dcom.alibaba.dubbo.demo.bid.BidService%26methods%3DthrowNPE%2Cbid%26organization%3Ddubbox%26owner%3Dprogrammer%26pid%3D34788%26side%3Dconsumer%26timestamp%3D1522551195255&registry=zookeeper&timestamp=1522551197254
複製程式碼

而不是registry協議的直連url,通過ClusterUtils.mergeUrl進行引數合併後,生成的url就直接對應到服務提供者,如

127.0.0.1:20880/com.alibaba.dubbo.demo.bid.BidService?application=demo-consumer&dubbo=2.0.0&interface=com.alibaba.dubbo.demo.bid.BidService&methods=throwNPE,bid&organization=dubbox&owner=programmer&pid=34828&side=consumer&timestamp=1522552227828
複製程式碼

這邊因為我配置的直連url="127.0.0.1:20880",沒有配置協議,所以產生的提供者url沒有協議,但是protcol的適配類會自動使用dubbo協議,如果提供者不是用dubbo協議暴露的,那麼就存在問題了。

如果沒有配置直連url,那麼獲取註冊中心的url,並且在refer引數的放入呼叫介面的配置鍵值對,和上面第一個url一致

完成解析url之後,就可以通過protocol的refer方法,把url轉換成invoker

if (urls.size() == 1) {
    invoker = refprotocol.refer(interfaceClass, urls.get(0));
} else {
    List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
    URL registryURL = null;
    for (URL url : urls) {
        invokers.add(refprotocol.refer(interfaceClass, url));
        if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
            registryURL = url; // 用了最後一個registry url
        }
    }
    if (registryURL != null) { // 有 註冊中心協議的URL
        // 對有註冊中心的Cluster 只用 AvailableCluster
        URL u = registryURL.addParameter(Constants.CLUSTER_KEY, AvailableCluster.NAME); 
        invoker = cluster.join(new StaticDirectory(u, invokers));
    }  else { // 不是 註冊中心的URL
        invoker = cluster.join(new StaticDirectory(invokers));
    }
}
複製程式碼

如果url只存在一個,那麼直接用protocol進行轉換 如果存在多個,會先通過urls獲取所有invoker,然後根據urls中是否存在registry協議的url,做不同的叢集呼叫

  1. urls中存在註冊中心url 強制會使用AvailableCluster呼叫,因為一部分是直連的invoker,一部分是registry協議生成的invoker,registry協議生成的invoker內部也是多個invoker的cluster呼叫,如果在外層還允許使用其他複雜的cluster模式,我認為會加大呼叫複雜度,所以這個外層的cluster呼叫,是哪個invoker優先可用就用誰
  2. urls中不存在註冊中心url 對於urls全是直連的url,那麼直接使用配置的cluster模式把多個invoker偽裝成一個即可

下面看下RegistryProtocol和DubboProtocol如何通過refer方法把url轉換為invoker

RegistryProtocol的refer

public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
    url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
    //通過url獲取註冊中心物件
    Registry registry = registryFactory.getRegistry(url);
    //如果遠端呼叫的介面就是RegistryService,直接返回,暫時不知道這個被什麼功能呼叫
    if (RegistryService.class.equals(type)) {
        return proxyFactory.getInvoker((T) registry, type, url);
    }
    //提取refer內的引數
    // group="a,b" or group="*"
    Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
    String group = qs.get(Constants.GROUP_KEY);
    //如果配置了group,呼叫對應group的提供者
    if (group != null && group.length() > 0 ) {
        if ( ( Constants.COMMA_SPLIT_PATTERN.split( group ) ).length > 1
                || "*".equals( group ) ) {
            //MergeableCluster會根據merge引數是否配置,進行結果合併
            return doRefer( getMergeableCluster(), registry, type, url );
        }
    }
    //這邊的cluster是適配類,會根據url內配置的cluster引數選擇叢集策略
    return doRefer(cluster, registry, type, url);
}
複製程式碼

在doRefer方法裡,通過type和url初始化RegistryDirectory,RegistryDirectory內部會通過url從Registry獲取所有提供者url並通過對應protocol建立invoker 同時把RegistryDirectory設定到cluster,cluster會呼叫RegistryDirectory的doList方法獲取對應invoker,偽裝成一個invoker,然後根據不同叢集實現進行特定的呼叫

private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
    //配置Directory
    RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
    directory.setRegistry(registry);
    directory.setProtocol(protocol);
    URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters());
    if (! Constants.ANY_VALUE.equals(url.getServiceInterface())
            && url.getParameter(Constants.REGISTER_KEY, true)) {
        registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
                Constants.CHECK_KEY, String.valueOf(false)));
    }
//註冊監聽回撥,用於invoker動態更新    directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY, 
            Constants.PROVIDERS_CATEGORY 
            + "," + Constants.CONFIGURATORS_CATEGORY 
            + "," + Constants.ROUTERS_CATEGORY));
    //從directory獲取invokers,對外封裝成一個invoker
    return cluster.join(directory);
}
複製程式碼

DubboProtocol的refer

 public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {

    // modified by lishen
    optimizeSerialization(url);

    // create rpc invoker.
    DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
    invokers.add(invoker);
    return invoker;
}
複製程式碼

在DubboProtocol的refer方法根據url生成對應的DubboInvoker,DubboInvoker初始化的時候,會把netty客戶端物件陣列ExchangeClient傳入,ExchangeClient根據url生成,會連線到對應到遠端暴露伺服器監聽的埠,DubboInvoker會輪詢client對server進行遠端呼叫。呼叫邏輯在doInvoke方法

@Override
    protected Result doInvoke(final Invocation invocation) throws Throwable {
        RpcInvocation inv = (RpcInvocation) invocation;
        final String methodName = RpcUtils.getMethodName(invocation);
        inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
        inv.setAttachment(Constants.VERSION_KEY, version);
        
        ExchangeClient currentClient;
        if (clients.length == 1) {
            currentClient = clients[0];
        } else {
            currentClient = clients[index.getAndIncrement() % clients.length];
        }
        try {
            boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
            boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
            int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY,Constants.DEFAULT_TIMEOUT);
            //直接呼叫,忽略返回資訊,通過設定return=false來實現
            if (isOneway) {
                //sent引數用來設定是否需要等待訊息發出再返回,在非同步呼叫總是不等待返回
            	boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
                currentClient.send(inv, isSent);
                RpcContext.getContext().setFuture(null);
                return new RpcResult();
            } else if (isAsync) {//非同步呼叫,通過配置async=true開啟,同時可以配置onreturn回撥
            	ResponseFuture future = currentClient.request(inv, timeout) ;
                RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));//將future繫結到上下文,這個非同步回撥會在FutureFilter裡面處理,同步呼叫設定的callback也會在FutureFilter處理
                return new RpcResult();
            } else {//同步呼叫
            	RpcContext.getContext().setFuture(null);
                return (Result) currentClient.request(inv, timeout).get();
            }
        } catch (TimeoutException e) {
            throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        } catch (RemotingException e) {
            throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
        }
    }
複製程式碼

這邊呼叫模式由三種,oneway,async,sync oneway直接呼叫忽略結果可以配置sent async非同步呼叫 sync同步呼叫,阻塞返回結果 這邊的集中呼叫方式都可以配置回撥方法,回撥的邏輯在FutureFIiter裡面

public Result invoke(final Invoker<?> invoker, final Invocation invocation) throws RpcException {
        final boolean isAsync = RpcUtils.isAsync(invoker.getUrl(), invocation);
        //oninvoke回撥
        fireInvokeCallback(invoker, invocation);
        // need to configure if there's return value before the invocation in order to help invoker to judge if it's
        // necessary to return future.
        Result result = invoker.invoke(invocation);
        if (isAsync) {
           //onthrow和onreturn回撥
            asyncCallback(invoker, invocation);
        } else {
            //
            syncCallback(invoker, invocation, result);
        }onthrow和onreturn回撥
        return result;
    }
複製程式碼

可以通過對方法配置onthrow,oninvoke,onreturn來設定回撥

image.png

ExchangeClient呼叫遠端提供者的邏輯單獨再講,和Server一起

建立代理物件

再拿到invoker之後,通過

proxyFactory.getProxy(invoker);
複製程式碼

建立代理,和服務暴露都是用proxyFactory擴充套件點,但是服務引用用getProxy把invoker轉換為代理ref,而在服務暴露中是把代理ref轉換為Invoker invoker轉換為代理ref的邏輯有兩部分,一部分在AbstractProxyFactory,另一部分通過模版方法讓子類實現 先看AbstractProxyFactory中的邏輯

public <T> T getProxy(Invoker<T> invoker) throws RpcException {
        Class<?>[] interfaces = null;
        String config = invoker.getUrl().getParameter("interfaces");
        if (config != null && config.length() > 0) {
            String[] types = Constants.COMMA_SPLIT_PATTERN.split(config);
            if (types != null && types.length > 0) {
                interfaces = new Class<?>[types.length + 2];
                interfaces[0] = invoker.getInterface();
                interfaces[1] = EchoService.class;
                for (int i = 0; i < types.length; i ++) {
                    interfaces[i + 1] = ReflectUtils.forName(types[i]);
                }
            }
        }
        if (interfaces == null) {
            interfaces = new Class<?>[] {invoker.getInterface(), EchoService.class};
        }
        return getProxy(invoker, interfaces);
    }
複製程式碼

在AbstractProxyFactory的getProxy會在代理的介面中加入EchoService介面,也就是回聲服務,使用方式如下

Dubbo原始碼分析之服務引用
具體原理是在呼叫服務暴露的invoker時會有EchoFilter攔截這個呼叫

@Activate(group = Constants.PROVIDER, order = -110000)
public class EchoFilter implements Filter {

	public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
		if(inv.getMethodName().equals(Constants.$ECHO) && inv.getArguments() != null && inv.getArguments().length == 1 )
			return new RpcResult(inv.getArguments()[0]);
		return invoker.invoke(inv);
	}

}
複製程式碼

如果invoker可用,會把傳過去的值原封不動返回過來 在增加EchoService介面後,通過子類的模版方法getProxy來建立代理

public abstract <T> T getProxy(Invoker<T> invoker, Class<?>[] types);
複製程式碼

我們看下JdkProxyFactory的實現

public <T> T getProxy(Invoker<T> invoker, Class<?>[] interfaces) {
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), interfaces, new InvokerInvocationHandler(invoker));
    }
複製程式碼

具體代理如何通過invoker實現呼叫封裝在InvokerInvocationHandler裡

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        //處理Object方法的呼叫,跟Object有關的方法都不需要遠端呼叫
        if (method.getDeclaringClass() == Object.class) {
            return method.invoke(invoker, args);
        }
        if ("toString".equals(methodName) && parameterTypes.length == 0) {
            return invoker.toString();
        }
        if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
            return invoker.hashCode();
        }
        if ("equals".equals(methodName) && parameterTypes.length == 1) {
            return invoker.equals(args[0]);
        }
        //執行遠端呼叫
        return invoker.invoke(new RpcInvocation(method, args)).recreate();
    }
複製程式碼

其中recreate方法用來將result轉換為介面實際需要的型別,如果有異常丟擲

public Object recreate() throws Throwable {
        if (exception != null) {
            throw exception;
        }
        return result;
    }
複製程式碼

現在把代理放到spring容器,用起來就想本地呼叫一樣,其實也不是主動放,依賴注入的時候才主動初始化

接下去

Dubbo可以說複雜又簡單,在引用和暴露中存在很多其他功能點,接下來需要一個個解析

  1. remoting模組解析
  2. 註冊中心解析
  3. Protocol解析
  4. Cluster,Directory,LoadBalance解析

最後

希望大家關注下我的公眾號

image

相關文章