參數回調 Callback是Dubbo中一種機制,與調用本地callback相同,將基於長連接生成反向代理在服務端執行客戶端的邏輯,本文將以以下內容展開。
以下幾個方面:
- callback例子
- callback中,關鍵配置,服務端對於 配置了
<dubbo:argument index="1" callback="true"/>
,是怎麼處理的。 - 爲什麼客戶端雖然沒有對callback指定,但是如果服務端不指定callback,而客戶端爲什麼會報錯?
- 服務端收到消息後,對callback有特殊處理嗎?
- 服務端是通過怎樣方式調用客戶端邏輯?通過zk上面節點信息call,還是怎樣呢?
- 客戶端中,傳遞過去的 爲一個類,有什麼處理?
- 客戶端又是如何相應 服務端發起的回調呢?
- 客戶端回調,是通過新的new 對象調用,還是基於舊實例調用呢?
以上問題看完文章後相信大家就可以清楚,若有疑問,關注博主公衆號:六點A君,回覆標題獲取最新答案><
例子
何爲在服務器執行客戶端邏輯?簡單點說,就是客戶端可以定義一個方法,而此方法調用方爲服務端。
要使用參數回調這個功能,有兩個要素:
- 服務端對該
Service
需要配置需要回調的參數。 - 客戶端調用對應服務端
Service
對象時,傳入對應邏輯。
而 服務端配置例子如下:
<dubbo:service interface="com.anla.rpc.callback.provider.service.CallbackService" ref="callbackService" connections="1" callbacks="1000">
<dubbo:method name="addListener">
<dubbo:argument index="1" callback="true" />
<!--也可以通過指定類型的方式-->
<!--<dubbo:argument type="com.demo.CallbackListener" callback="true" />-->
</dubbo:method>
</dubbo:service>
具體例子地址在這裏:Callback 參數回調
服務端配置
前幾篇文章分析了 服務端配置及初始化過程,有興趣同學可以回看,這裏只拎初對Callback 的單獨配置。
provider的初始化過程B:外部化配置初始化過程
Provider的初始化過程C:服務暴露詳解
當服務暴露是,主要配置以及export 過程 在ServiceConfig
進行的,而在配置讀取時 會首先對 dubbo:method
和 dubbo:argument
進行初始化,對 callback 原理進行標識則是在 doExportUrlsFor1Protocol
方法中進行。
這個方法代碼比較長,邏輯上包括對 配置進行讀取,對dubbo 中的 url進行拼湊,以及 輸出 invoker等邏輯,可以看上面兩篇文章有細分析。
而callbcak 原理在以下邏輯
if (CollectionUtils.isNotEmpty(methods)) {
// 判斷 methods 是否爲空
for (MethodConfig method : methods) {
appendParameters(map, method, method.getName());
String retryKey = method.getName() + ".retry";
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey);
if ("false".equals(retryValue)) {
map.put(method.getName() + ".retries", "0");
}
}
List<ArgumentConfig> arguments = method.getArguments();
if (CollectionUtils.isNotEmpty(arguments)) {
// 判斷arguments 是否爲空
for (ArgumentConfig argument : arguments) {
// convert argument type
if (argument.getType() != null && argument.getType().length() > 0) {
// 以 <dubbo:argument type="com.demo.CallbackListener" callback="true" /> 方式配置
Method[] methods = interfaceClass.getMethods();
// visit all methods
if (methods != null && methods.length > 0) {
for (int i = 0; i < methods.length; i++) {
String methodName = methods[i].getName();
// target the method, and get its signature
if (methodName.equals(method.getName())) {
Class<?>[] argtypes = methods[i].getParameterTypes();
// one callback in the method
if (argument.getIndex() != -1) {
if (argtypes[argument.getIndex()].getName().equals(argument.getType())) {
appendParameters(map, argument, method.getName() + "." + argument.getIndex());
} else {
throw new IllegalArgumentException("Argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
}
} else {
// multiple callbacks in the method
for (int j = 0; j < argtypes.length; j++) {
Class<?> argclazz = argtypes[j];
if (argclazz.getName().equals(argument.getType())) {
appendParameters(map, argument, method.getName() + "." + j);
if (argument.getIndex() != -1 && argument.getIndex() != j) {
throw new IllegalArgumentException("Argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
}
}
}
}
}
}
}
} else if (argument.getIndex() != -1) {
// 以 <dubbo:argument index="1" callback="true" /> 配置
appendParameters(map, argument, method.getName() + "." + argument.getIndex());
} else {
throw new IllegalArgumentException("Argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>");
}
}
}
} // end of methods for
}
以上代碼包括幾步:
- 判斷 methods 是否爲空
- 判斷arguments 是否爲空
如果通過判斷,則進入以下設置parameter 過程 - 以
以 <dubbo:argument type="com.demo.CallbackListener" callback="true" />
方式配置的適配 - 以
<dubbo:argument index="1" callback="true" />
方式配置的適配。
服務端對Dubbo 配置方面就是這些,最終,在註冊中心留下的Provider
的Service
則會多個callback
的配置。
如下形式 addListener.1.callback=true
說明這個接口的 addListener 方法的 第1個參數是callback類型(從0開始)。
Consumer 對 Callback 適配
開篇有個問題,當 服務端對接口沒有配置 callback時候,客戶端 直接啓動,則會報錯,即對象沒有實現 Serializable
。當然呢,Dubbo 默認以 Hessian
作爲序列化框架,而 Hessian
則要求實現 Serializable
。
Caused by: java.lang.IllegalStateException: Serialized class com.anla.rpc.callback.consumer.Consumer$CallBackDemo must implement java.io.Serializable
at com.alibaba.com.caucho.hessian.io.SerializerFactory.getDefaultSerializer(SerializerFactory.java:401)
at com.alibaba.com.caucho.hessian.io.SerializerFactory.getSerializer(SerializerFactory.java:375)
at com.alibaba.com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:389)
at org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput.writeObject(Hessian2ObjectOutput.java:89)
at org.apache.dubbo.rpc.protocol.dubbo.DubboCodec.encodeRequestData(DubboCodec.java:185)
at org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.encodeRequest(ExchangeCodec.java:238)
at org.apache.dubbo.remoting.exchange.codec.ExchangeCodec.encode(ExchangeCodec.java:69)
at org.apache.dubbo.rpc.protocol.dubbo.DubboCountCodec.encode(DubboCountCodec.java:40)
at org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter$InternalEncoder.encode(NettyCodecAdapter.java:70)
所以這裏就引出了 Consumer
端初始化配置的操作,上述明顯是執行dubbo
調用纔會爆出的錯誤。所以肯定在Consumer在配置配置初始化會在註冊中心和 服務端交互。
Consumer 中對callback 支持主要 體現在以下兩個方面:
- 從註冊中心獲取配置,參數寫道url中
- 代理將callback方法暴露出去,但是不註冊到zk上
- 將callback 參數 不直接發送。
另一方面,如果Consumer 端傳入一個實現了Serializable,而客戶端嘗試調用其裏面內部方法,則會報錯,客戶端會報空指針錯誤:
Exception in thread "main" java.lang.NullPointerException
at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)
at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
at com.anla.rpc.callback.provider.impl.CallbackServiceImpl.addListener(CallbackServiceImpl.java:44)
at org.apache.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java)
at org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:47)
at org.apache.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:84)
at org.apache.dubbo.config.invoker.DelegateProviderMetaDataInvoker.invoke(DelegateProviderMetaDataInvoker.java:56)
at org.apache.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:56)
at org.apache.dubbo.rpc.filter.ExceptionFilter.invoke(ExceptionFilter.java:55)
而服務端也會給出給出紅色警告信息:
十月 13, 2019 11:38:17 下午 com.alibaba.com.caucho.hessian.io.SerializerFactory getDeserializer
警告: Hessian/Burlap: 'com.anla.rpc.callback.consumer.Consumer$CallBackDemo' is an unknown class in sun.misc.Launcher$AppClassLoader@18b4aac2:
java.lang.ClassNotFoundException: com.anla.rpc.callback.consumer.Consumer$CallBackDemo
爲什麼會出現這樣信息呢?讀完這篇文章估計大家就會有結論了。
Consumer 增加callback配置
Consumer
端 從 註冊中心獲取 相關配置 在 RegistryProtocol
中進行,而具體管控則是由 RegistryDirectory
進行調用。整個Consumer配置可以看 :
@Reference或ReferenceConfig.get代理對象如何產生(一):SPI模式中 Wrapper和 SPI 類組裝邏輯
和
Dubbo 消費者中 代理對象 初始化詳解
本文同樣只拎出 獲取 callback 配置相關過程。
由Protocl
會執行 refer
方法,去獲取某Invoker
首先進入 RegistryProtocol
的 doRefer
方法:
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
// 構造一個RegistryDirectory
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
// all attributes of REFER_KEY
Map<String, String> parameters = new HashMap<String, String>(directory.getUrl().getParameters());
URL subscribeUrl = new URL(CONSUMER_PROTOCOL, parameters.remove(REGISTER_IP_KEY), 0, type.getName(), parameters);
// 訂閱的url
if (!ANY_VALUE.equals(url.getServiceInterface()) && url.getParameter(REGISTER_KEY, true)) {
directory.setRegisteredConsumerUrl(getRegisteredConsumerUrl(subscribeUrl, url));
registry.register(directory.getRegisteredConsumerUrl());
}
directory.buildRouterChain(subscribeUrl);
// 開始訂閱
directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));
// 返回Invoker
Invoker invoker = cluster.join(directory);
ProviderConsumerRegTable.registerConsumer(invoker, url, subscribeUrl, directory);
return invoker;
}
doRefer
方法中 主要通過 構造一個 RegistryDirectory
進行整個 對外服務相關管理,設置完相關參數,執行
directory.subscribe(subscribeUrl.addParameter(CATEGORY_KEY,
PROVIDERS_CATEGORY + "," + CONFIGURATORS_CATEGORY + "," + ROUTERS_CATEGORY));
RegistryDirectory
方法:
public void subscribe(URL url) {
setConsumerUrl(url);
CONSUMER_CONFIGURATION_LISTENER.addNotifyListener(this);
serviceConfigurationListener = new ReferenceConfigurationListener(this, url);
registry.subscribe(url, this);
}
而後在 FailbackRegistry
的subscribe
方法:
@Override
public void subscribe(URL url, NotifyListener listener) {
super.subscribe(url, listener);
removeFailedSubscribed(url, listener);
try {
// Sending a subscription request to the server side
doSubscribe(url, listener);
} catch (Exception e) {
Throwable t = e;
List<URL> urls = getCacheUrls(url);
if (CollectionUtils.isNotEmpty(urls)) {
notify(url, listener, urls);
logger.error("Failed to subscribe " + url + ", Using cached list: " + urls + " from cache file: " + getUrl().getParameter(FILE_KEY, System.getProperty("user.home") + "/dubbo-registry-" + url.getHost() + ".cache") + ", cause: " + t.getMessage(), t);
} else {
// If the startup detection is opened, the Exception is thrown directly.
boolean check = getUrl().getParameter(Constants.CHECK_KEY, true)
&& url.getParameter(Constants.CHECK_KEY, true);
boolean skipFailback = t instanceof SkipFailbackWrapperException;
if (check || skipFailback) {
if (skipFailback) {
t = t.getCause();
}
throw new IllegalStateException("Failed to subscribe " + url + ", cause: " + t.getMessage(), t);
} else {
logger.error("Failed to subscribe " + url + ", waiting for retry, cause: " + t.getMessage(), t);
}
}
// Record a failed registration request to a failed list, retry regularly
addFailedSubscribed(url, listener);
}
}
代碼較深,就單以文字加部分代碼分析。
- 如果使用
Zookeeper
協議,則會進入ZookeeperRegistry
執行doSubscribe
,這個方法則主要是從zk上獲取各種類型的節點以及數量,以及將自己 的信息 在zookeeper 上的 {service} 下面所有類型的節點。 - 執行
notify
方法,主要目的 註冊後第一次通過註冊中心信息去更改 對應配置。
上述notify
方法主要是 執行FailbackRegistry
的notify
方法。 - 執行
FailbackRegistry
的notify
方法後,會執行其doNotify
方法,這個方法最終會執行到AbstractRegistry
的notify
方法。 - 最終的
notify
會執行到RegistryDirectory
中。對 對應接口暴露下 三種類型(providers
,consumers
,routers
) 節點進行讀取 。
當服務端url 有值是,會執行 RegistryDirectory
的 refreshOverrideAndInvoker
。將其進行配置更新,最終將服務端配置addListener.1.callback=true
加入到自己請求參數中,當進行序列化時繞過Hessian。
Consumer 構造Request數據
當Consumer 端這邊已經搞好配置,也拿到了 負載均衡後的 Invoker
,一步一步穿過Protocol,Transporter,轉由Netty發送,而Netty 發送,則會對其進行編碼,從而使用 到Dubbo 自實現的編碼方式,具體邏輯如下:
- 進入到
NettyCodecAdapter
的 內部類InternalEncoder
的encode
方法。 - 進入
DubboCountCodec
的encode
- 依次進入
ExchangeCodec
的encode
、encodeRequest
、encodeReguqestData
方法,
encodeRequestData
則關乎到 callback 參數設置:
@Override
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
out.writeUTF(version);
out.writeUTF(inv.getAttachment(PATH_KEY));
out.writeUTF(inv.getAttachment(VERSION_KEY));
out.writeUTF(inv.getMethodName());
out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));
Object[] args = inv.getArguments();
// 對參數進行編碼
if (args != null) {
for (int i = 0; i < args.length; i++) {
out.writeObject(encodeInvocationArgument(channel, inv, i));
}
}
out.writeObject(inv.getAttachments());
}
而後進入到 encodeInvocationArgument 進行callback 參數判斷以及填充:
public static Object encodeInvocationArgument(Channel channel, RpcInvocation inv, int paraIndex) throws IOException {
// get URL directly
URL url = inv.getInvoker() == null ? null : inv.getInvoker().getUrl();
byte callbackStatus = isCallBack(url, inv.getMethodName(), paraIndex);
Object[] args = inv.getArguments();
Class<?>[] pts = inv.getParameterTypes();
switch (callbackStatus) {
case CallbackServiceCodec.CALLBACK_NONE:
return args[paraIndex];
case CallbackServiceCodec.CALLBACK_CREATE:
inv.setAttachment(INV_ATT_CALLBACK_KEY + paraIndex, exportOrUnexportCallbackService(channel, url, pts[paraIndex], args[paraIndex], true));
return null;
case CallbackServiceCodec.CALLBACK_DESTROY:
inv.setAttachment(INV_ATT_CALLBACK_KEY + paraIndex, exportOrUnexportCallbackService(channel, url, pts[paraIndex], args[paraIndex], false));
return null;
default:
return args[paraIndex];
}
}
上述 isCallBack(url, inv.getMethodName(), paraIndex);
則使用了從 服務端獲取而來的 addListener.1.callback=true
,從而標明是callback類型,並且如果是callback則直接返回null
而非callback 則直接返回 args[paraIndex];
,由於沒有實現 Serializable,所以會報錯。
Consumer 端 callback 產生以及傳遞
如果說,callback 參數是直接發送null,那麼callback如何傳遞給服務端呢?二者如何交互呢?
先看第一個 , 當判斷爲 CALLBACK_CREATE
事件是,將 exportOrUnexportCallbackService
返回的 String 放入到 需要傳遞的 RpcInvocation 中,並且以 sys_callback_arg
+ paraIndex 作爲key。
而在encodeRequestData
方法最後,會執行 out.writeObject(inv.getAttachments());
將 RpcInvocation
的所有attachments
都交由 Netty
發送。
所以,最終 callback
在客戶端是以 String
類型 通過Netty
發送給 Provider
端。
下面看 exportOrUnexportCallbackService
執行了什麼操作:
private static String exportOrUnexportCallbackService(Channel channel, URL url, Class clazz, Object inst, Boolean export) throws IOException {
// 獲取一個instid
int instid = System.identityHashCode(inst);
// 由於會調用共用的export,所以這個 callback 的服務和 主 服務共享一個 service
// 以下爲構造參數過程
Map<String, String> params = new HashMap<>(3);
params.put(IS_SERVER_KEY, Boolean.FALSE.toString());
params.put(IS_CALLBACK_SERVICE, Boolean.TRUE.toString());
String group = (url == null ? null : url.getParameter(GROUP_KEY));
if (group != null && group.length() > 0) {
params.put(GROUP_KEY, group);
}
// add method, for verifying against method, automatic fallback (see dubbo protocol)
params.put(METHODS_KEY, StringUtils.join(Wrapper.getWrapper(clazz).getDeclaredMethodNames(), ","));
Map<String, String> tmpMap = new HashMap<>(url.getParameters());
tmpMap.putAll(params);
tmpMap.remove(VERSION_KEY);// doesn't need to distinguish version for callback
tmpMap.put(INTERFACE_KEY, clazz.getName());
URL exportUrl = new URL(DubboProtocol.NAME, channel.getLocalAddress().getAddress().getHostAddress(), channel.getLocalAddress().getPort(), clazz.getName() + "." + instid, tmpMap);
// no need to generate multiple exporters for different channel in the same JVM, cache key cannot collide.
String cacheKey = getClientSideCallbackServiceCacheKey(instid);
String countKey = getClientSideCountKey(clazz.getName());
if (export) {
// one channel can have multiple callback instances, no need to re-export for different instance.
if (!channel.hasAttribute(cacheKey)) {
if (!isInstancesOverLimit(channel, url, clazz.getName(), instid, false)) {
// 構造一個callback 的Invoker
Invoker<?> invoker = PROXY_FACTORY.getInvoker(inst, clazz, exportUrl);
// 暴露該服務,獲取一個 Exporter
Exporter<?> exporter = protocol.export(invoker);
// this is used for tracing if instid has published service or not.
// 將Exporter 放入channel 中
channel.setAttribute(cacheKey, exporter);
logger.info("Export a callback service :" + exportUrl + ", on " + channel + ", url is: " + url);
increaseInstanceCount(channel, countKey);
}
}
} else {
// 如果是銷燬callback操作
if (channel.hasAttribute(cacheKey)) {
Exporter<?> exporter = (Exporter<?>) channel.getAttribute(cacheKey);
exporter.unexport();
channel.removeAttribute(cacheKey);
decreaseInstanceCount(channel, countKey);
}
}
return String.valueOf(instid);
}
上面方法有以下幾個過程:
- 構造URL 參數,設置Callback 應有的參數
- 通過類實例,類,以及url 獲取一個Invoker,
PROXY_FACTORY.getInvoker(inst, clazz, exportUrl);
- 執行
protocol.export(invoker);
使用DubboProtocol
暴露服務 - 將
Exporter
放入channel
的attribute
中。
下面看看 callback 的 Invoker 產生邏輯,即 PROXY_FACTORY.getInvoker(inst, clazz, exportUrl);
代碼邏輯:
最終通過 JavaassistProxyFactory
產生一個代理Invoker,用於執行傳入inst 實例的方法。
@Override
public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
// TODO Wrapper cannot handle this scenario correctly: the classname contains '$'
final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
return new AbstractProxyInvoker<T>(proxy, type, url) {
@Override
protected Object doInvoke(T proxy, String methodName,
Class<?>[] parameterTypes,
Object[] arguments) throws Throwable {
return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
}
};
}
所以這個 getInvoker
最終獲取一個 Invoker
,這個Invoker 只是包裝了一層,最終執行 wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
即執行 傳入proxy
的,執行 methodName
的方法。
Provider 對 Callback 適配
服務端對callback 也有着特定的適配:
- 需要配置 callback類型參數用於告訴 客戶端 參數類型
- 在 數據解碼階段,將callback參數的解碼
當 Provider 獲取而來的 具體 Callback 對象時,參數有了封裝,看截圖:
此處相信認真讀的同學有個疑問:
- 從 上面分析來看,客戶端傳遞過來時候,對callback處理就是傳null,以及設置 attachments。那爲啥到服務端會變爲 有
InvokerInvoocationHandler
以及AsyncToSyncInvoker
的包裝類型呢?
往下看。
當在客戶端傳遞過來是,在Netty 的encode 邏輯處,對callback進行了適配,所以其實在 provider 的 decode 邏輯進行了封裝,用 InvokerInvoocationHandler
以及 AsyncToSyncInvoker
的包裝類型。
下面看看具體邏輯:
- 從
NettyCodecAdapter
的 內部類的InternalDecoder
的decode
方法開始。 - 進入
DubboCountCodec
的 decode方法 - 再到
ExchangeCodec
的 兩個重載的decode
方法,再到DubboCodec
的decodeBody
, - 通過使用
DecodeableRpcInvocation
來解碼RpcInvocation
類型數據 - 在
CallbackServiceCodec
中的decodeInvocationArgument
進行 callback 類型判定以及解碼。
public static Object decodeInvocationArgument(Channel channel, RpcInvocation inv, Class<?>[] pts, int paraIndex, Object inObject) throws IOException {
// 如果是callback 類型,則創建client 端的代理,即這個代理對象可以發起對client端的遠程調用
URL url = null;
try {
// 解析初url
url = DubboProtocol.getDubboProtocol().getInvoker(channel, inv).getUrl();
} catch (RemotingException e) {
if (logger.isInfoEnabled()) {
logger.info(e.getMessage(), e);
}
return inObject;
}
// 解析callback類型
byte callbackstatus = isCallBack(url, inv.getMethodName(), paraIndex);
switch (callbackstatus) {
// 普通參數
case CallbackServiceCodec.CALLBACK_NONE:
return inObject;
// 創建callback
case CallbackServiceCodec.CALLBACK_CREATE:
try {
return referOrDestroyCallbackService(channel, url, pts[paraIndex], inv, Integer.parseInt(inv.getAttachment(INV_ATT_CALLBACK_KEY + paraIndex)), true);
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new IOException(StringUtils.toString(e));
}
// 銷燬callback
case CallbackServiceCodec.CALLBACK_DESTROY:
try {
return referOrDestroyCallbackService(channel, url, pts[paraIndex], inv, Integer.parseInt(inv.getAttachment(INV_ATT_CALLBACK_KEY + paraIndex)), false);
} catch (Exception e) {
throw new IOException(StringUtils.toString(e));
}
default:
return inObject;
}
}
- 而最終有consumer 端傳遞的url 有指定 參數回調及類型,所以判定爲callback
看看 referOrDestroyCallbackService
中是如何在服務端構造一個 代理對象的:
private static Object referOrDestroyCallbackService(Channel channel, URL url, Class<?> clazz, Invocation inv, int instid, boolean isRefer) {
Object proxy = null;
// invoker 緩存對象 的key:callback.service.proxy.12503143.com.anla.rpc.callback.provider.service.CallbackListener.654342195.invoker
String invokerCacheKey = getServerSideCallbackInvokerCacheKey(channel, clazz.getName(), instid);
// 代理緩存對象key:callback.service.proxy.12503143.com.anla.rpc.callback.provider.service.CallbackListener.654342195
String proxyCacheKey = getServerSideCallbackServiceCacheKey(channel, clazz.getName(), instid);
// 判斷當前 channel 是否已經產生了代理對象。
proxy = channel.getAttribute(proxyCacheKey);
// count 的key callback.service.proxy.12503143.com.anla.rpc.callback.provider.service.CallbackListener.COUNT
String countkey = getServerSideCountKey(channel, clazz.getName());
if (isRefer) {
if (proxy == null) {
URL referurl = URL.valueOf("callback://" + url.getAddress() + "/" + clazz.getName() + "?" + INTERFACE_KEY + "=" + clazz.getName());
referurl = referurl.addParametersIfAbsent(url.getParameters()).removeParameter(METHODS_KEY);
// 以下判斷是否超出 服務的 callback 參數
if (!isInstancesOverLimit(channel, referurl, clazz.getName(), instid, true)) {
@SuppressWarnings("rawtypes")
// 構造一個Invoker
Invoker<?> invoker = new ChannelWrappedInvoker(clazz, channel, referurl, String.valueOf(instid));
// 使用 JavassistProxyFactory 生成一個由 InvokerInvocationHandler+ AsyncToSyncInvoker 的包裝的invoker
proxy = PROXY_FACTORY.getProxy(new AsyncToSyncInvoker<>(invoker));
// 設置channel屬性
channel.setAttribute(proxyCacheKey, proxy);
channel.setAttribute(invokerCacheKey, invoker);
// 設置計數到channel中
increaseInstanceCount(channel, countkey);
// 忽略併發問題,快速失敗
// 將構造出的invoker 放入channel中
Set<Invoker<?>> callbackInvokers = (Set<Invoker<?>>) channel.getAttribute(CHANNEL_CALLBACK_KEY);
if (callbackInvokers == null) {
callbackInvokers = new ConcurrentHashSet<Invoker<?>>(1);
callbackInvokers.add(invoker);
channel.setAttribute(CHANNEL_CALLBACK_KEY, callbackInvokers);
}
logger.info("method " + inv.getMethodName() + " include a callback service :" + invoker.getUrl() + ", a proxy :" + invoker + " has been created.");
}
}
} else {
if (proxy != null) {
// 從Invoker 中拿出並銷燬
Invoker<?> invoker = (Invoker<?>) channel.getAttribute(invokerCacheKey);
try {
Set<Invoker<?>> callbackInvokers = (Set<Invoker<?>>) channel.getAttribute(CHANNEL_CALLBACK_KEY);
if (callbackInvokers != null) {
callbackInvokers.remove(invoker);
}
invoker.destroy();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
// 直接從map中刪除。
channel.removeAttribute(proxyCacheKey);
channel.removeAttribute(invokerCacheKey);
decreaseInstanceCount(channel, countkey);
}
}
return proxy;
}
此時該回調的Invoker 中的url爲 callback開頭:
callback://192.168.1.107:20880/com.anla.rpc.callback.provider.service.CallbackListener?addListener.1.callback=true&anyhost=true&application=provider&bean.name=com.anla.rpc.callback.provider.service.CallbackService&bind.ip=192.168.1.107&bind.port=20880&callbacks=1000&connections=1&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.anla.rpc.callback.provider.service.CallbackListener&pid=1090®ister=true&release=2.7.2&side=provider×tamp=1571061967992
但是此 url 好像並沒有體現出任何作用。
另一方面,這次從服務端發起的回調也是 twoWay類型,即服務端會等待客戶端執行結果。
但是在服務端並沒有設置超時控制的 task 回調,但是會記錄下調用的異常。
Dubbo 客戶端接受回調
當服務端調用之後,則是直接以 Netty 調用方式調用 dubbo接口,而不再去詢問註冊中心是否有對應服務。
客戶端拿到 回調用,通過 對事件進行判斷是 以下事件某一種:CONNECTED
、DISCONNECTED
、SENT
、RECEIVED
、CAUGHT
的某種,而此次回調屬於 RECEIVED
事件,從而 使用一個 ChannelEventRunnable
用於執行其邏輯。
在Netty 傳輸時,Invoker 並沒有拿回來,而只是 拿到 serviceKey
去解析,從而獲取到 客戶端 所緩存的 DubboExporter
,最終由 DubboExporter
返回對應的Invoker 對象,這個Invoker 對象就是dubbo 爲callback 方法創建的一個代理對象,而由該Invoker 對象則最終負責由服務端傳遞過來的 參數類型調用對應的方法。
流程
簡單梳理下參數回調Callback 整體實現原理:
- Provider 端配置Service,以及Service 的callback 參數
- Consumer 端通過註冊中心的url進行參數適配,將callback 參數形式加入自己的url中
- 在TCP層發送時,Consumer 對Callback類型參數進行特殊處理,從而避免匿名內部類沒有實現Serializable 接口的報錯
- Provider 在收到TCP 消息時,進行解碼,如果是callback類型,則封裝爲新的面向客戶端的 Invoker 代理
- 當在Provider 執行完,轉而通過上一步封裝號的Invoker代理想Consumer發起通信調用請求
- Consumer 收到
RECEIVE
消息類型是,通過特定serviceKey 獲取 已有的Exporter
以及Invoker
進行本地調用
整個參數回調Callback 實現就研究完成。
相信讀完全篇文章,大家就會對博主開篇幾個問題有自己見解了,如果對問題仍然不太懂,可以結合源碼進行多次調試。
當然各位同學也可以關注博主公衆號,通過後臺獲取博主私人微信進行交流,一起探討其中疑問
覺得博主寫的有用,不妨關注博主公衆號: 六點A君。
哈哈哈,Dubbo小吃街不迷路: