Dubbo進階(十四)- Dubbo中參數回調 Callback 實現深究

參數回調 Callback是Dubbo中一種機制,與調用本地callback相同,將基於長連接生成反向代理在服務端執行客戶端的邏輯,本文將以以下內容展開。
以下幾個方面:

  1. callback例子
  2. callback中,關鍵配置,服務端對於 配置了 <dubbo:argument index="1" callback="true"/> ,是怎麼處理的。
  3. 爲什麼客戶端雖然沒有對callback指定,但是如果服務端不指定callback,而客戶端爲什麼會報錯?
  4. 服務端收到消息後,對callback有特殊處理嗎?
  5. 服務端是通過怎樣方式調用客戶端邏輯?通過zk上面節點信息call,還是怎樣呢?
  6. 客戶端中,傳遞過去的 爲一個類,有什麼處理?
  7. 客戶端又是如何相應 服務端發起的回調呢?
  8. 客戶端回調,是通過新的new 對象調用,還是基於舊實例調用呢?

以上問題看完文章後相信大家就可以清楚,若有疑問,關注博主公衆號:六點A君,回覆標題獲取最新答案><

例子

何爲在服務器執行客戶端邏輯?簡單點說,就是客戶端可以定義一個方法,而此方法調用方爲服務端。
要使用參數回調這個功能,有兩個要素:

  1. 服務端對該Service 需要配置需要回調的參數。
  2. 客戶端調用對應服務端 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:methoddubbo: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
        }

以上代碼包括幾步:

  1. 判斷 methods 是否爲空
  2. 判斷arguments 是否爲空
    如果通過判斷,則進入以下設置parameter 過程
  3. 以 <dubbo:argument type="com.demo.CallbackListener" callback="true" /> 方式配置的適配
  4. <dubbo:argument index="1" callback="true" /> 方式配置的適配。

服務端對Dubbo 配置方面就是這些,最終,在註冊中心留下的ProviderService 則會多個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 支持主要 體現在以下兩個方面:

  1. 從註冊中心獲取配置,參數寫道url中
  2. 代理將callback方法暴露出去,但是不註冊到zk上
  3. 將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
首先進入 RegistryProtocoldoRefer 方法:

    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);
    }

而後在 FailbackRegistrysubscribe 方法:

    @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);
        }
    }

代碼較深,就單以文字加部分代碼分析。

  1. 如果使用Zookeeper 協議,則會進入 ZookeeperRegistry 執行 doSubscribe ,這個方法則主要是從zk上獲取各種類型的節點以及數量,以及將自己 的信息 在zookeeper 上的 service/consumers{service}/consumers 下面創建一個節點。並且會獲取{service} 下面所有類型的節點。
  2. 執行 notify 方法,主要目的 註冊後第一次通過註冊中心信息去更改 對應配置。
    上述 notify 方法主要是 執行 FailbackRegistrynotify 方法。
  3. 執行FailbackRegistrynotify 方法後,會執行其doNotify方法,這個方法最終會執行到 AbstractRegistrynotify 方法。
  4. 最終的 notify 會執行到 RegistryDirectory 中。對 對應接口暴露下 三種類型(providersconsumersrouters) 節點進行讀取 。

當服務端url 有值是,會執行 RegistryDirectoryrefreshOverrideAndInvoker 。將其進行配置更新,最終將服務端配置addListener.1.callback=true 加入到自己請求參數中,當進行序列化時繞過Hessian。

Consumer 構造Request數據

當Consumer 端這邊已經搞好配置,也拿到了 負載均衡後的 Invoker,一步一步穿過Protocol,Transporter,轉由Netty發送,而Netty 發送,則會對其進行編碼,從而使用 到Dubbo 自實現的編碼方式,具體邏輯如下:

  1. 進入到 NettyCodecAdapter 的 內部類 InternalEncoderencode 方法。
  2. 進入 DubboCountCodecencode
  3. 依次進入 ExchangeCodecencodeencodeRequestencodeReguqestData 方法,
    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);
    }

上面方法有以下幾個過程:

  1. 構造URL 參數,設置Callback 應有的參數
  2. 通過類實例,類,以及url 獲取一個Invoker,PROXY_FACTORY.getInvoker(inst, clazz, exportUrl);
  3. 執行 protocol.export(invoker); 使用DubboProtocol 暴露服務
  4. Exporter 放入channelattribute 中。

下面看看 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 也有着特定的適配:

  1. 需要配置 callback類型參數用於告訴 客戶端 參數類型
  2. 在 數據解碼階段,將callback參數的解碼

當 Provider 獲取而來的 具體 Callback 對象時,參數有了封裝,看截圖:
在這裏插入圖片描述
此處相信認真讀的同學有個疑問:

  1. 從 上面分析來看,客戶端傳遞過來時候,對callback處理就是傳null,以及設置 attachments。那爲啥到服務端會變爲 有 InvokerInvoocationHandler 以及 AsyncToSyncInvoker 的包裝類型呢?

往下看。

當在客戶端傳遞過來是,在Netty 的encode 邏輯處,對callback進行了適配,所以其實在 provider 的 decode 邏輯進行了封裝,用 InvokerInvoocationHandler 以及 AsyncToSyncInvoker 的包裝類型。
下面看看具體邏輯:

  1. NettyCodecAdapter 的 內部類的 InternalDecoderdecode 方法開始。
  2. 進入DubboCountCodec 的 decode方法
  3. 再到 ExchangeCodec 的 兩個重載的 decode方法,再到DubboCodecdecodeBody
  4. 通過使用 DecodeableRpcInvocation 來解碼 RpcInvocation 類型數據
  5. 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;
        }
    }
  1. 而最終有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&register=true&release=2.7.2&side=provider&timestamp=1571061967992

但是此 url 好像並沒有體現出任何作用。
另一方面,這次從服務端發起的回調也是 twoWay類型,即服務端會等待客戶端執行結果。
但是在服務端並沒有設置超時控制的 task 回調,但是會記錄下調用的異常。

Dubbo 客戶端接受回調

當服務端調用之後,則是直接以 Netty 調用方式調用 dubbo接口,而不再去詢問註冊中心是否有對應服務。
客戶端拿到 回調用,通過 對事件進行判斷是 以下事件某一種:CONNECTEDDISCONNECTEDSENTRECEIVEDCAUGHT的某種,而此次回調屬於 RECEIVED 事件,從而 使用一個 ChannelEventRunnable 用於執行其邏輯。

在Netty 傳輸時,Invoker 並沒有拿回來,而只是 拿到 serviceKey 去解析,從而獲取到 客戶端 所緩存的 DubboExporter,最終由 DubboExporter 返回對應的Invoker 對象,這個Invoker 對象就是dubbo 爲callback 方法創建的一個代理對象,而由該Invoker 對象則最終負責由服務端傳遞過來的 參數類型調用對應的方法。

流程

簡單梳理下參數回調Callback 整體實現原理:

  1. Provider 端配置Service,以及Service 的callback 參數
  2. Consumer 端通過註冊中心的url進行參數適配,將callback 參數形式加入自己的url中
  3. 在TCP層發送時,Consumer 對Callback類型參數進行特殊處理,從而避免匿名內部類沒有實現Serializable 接口的報錯
  4. Provider 在收到TCP 消息時,進行解碼,如果是callback類型,則封裝爲新的面向客戶端的 Invoker 代理
  5. 當在Provider 執行完,轉而通過上一步封裝號的Invoker代理想Consumer發起通信調用請求
  6. Consumer 收到RECEIVE 消息類型是,通過特定serviceKey 獲取 已有的Exporter以及Invoker 進行本地調用

整個參數回調Callback 實現就研究完成。

相信讀完全篇文章,大家就會對博主開篇幾個問題有自己見解了,如果對問題仍然不太懂,可以結合源碼進行多次調試。
當然各位同學也可以關注博主公衆號,通過後臺獲取博主私人微信進行交流,一起探討其中疑問

覺得博主寫的有用,不妨關注博主公衆號: 六點A君。
哈哈哈,Dubbo小吃街不迷路:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章