流程圖
這個流程對應我們這次原始碼分析主要內容,不得不說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®istry=zookeeper×tamp=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×tamp=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,做不同的叢集呼叫
- urls中存在註冊中心url 強制會使用AvailableCluster呼叫,因為一部分是直連的invoker,一部分是registry協議生成的invoker,registry協議生成的invoker內部也是多個invoker的cluster呼叫,如果在外層還允許使用其他複雜的cluster模式,我認為會加大呼叫複雜度,所以這個外層的cluster呼叫,是哪個invoker優先可用就用誰
- 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來設定回撥
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介面,也就是回聲服務,使用方式如下
具體原理是在呼叫服務暴露的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可以說複雜又簡單,在引用和暴露中存在很多其他功能點,接下來需要一個個解析
- remoting模組解析
- 註冊中心解析
- Protocol解析
- Cluster,Directory,LoadBalance解析
最後
希望大家關注下我的公眾號