1. 程式人生 > >dubbo同名方法的問題及思考

dubbo同名方法的問題及思考

背景

今天小夥伴問我一個問題

分析

我們系統中一直存在該種實踐 似乎從來都穩穩的執行,沒有任何問題呢……

比如

*
 * 查詢客戶List
 * @param customerCarVO
 * @param curPage
 * @return
 * @throws Exception
 */
@Deprecated
PageResult<CustomerCarVO> getPageCustomerList(CustomerCarVO customerCarVO, int curPage) throws Exception;
/**
 * 查詢客戶List
 * @param
customerCarSo * @return * @throws Exception */
PageResult<CustomerCarVO> getPageCustomerList(CustomerCarSo customerCarSo) throws Exception;

用了這麼久 似乎也沒有出現問題~

那麼是否可以很武斷的認為同名方法沒有問題???

我們想一下以前webservice中似乎有個限制 方法同名將不能使用 也就是說不支援方法的過載 那麼為何dubbo沒有這個問題?

或者說其實存在問題 只不過我們沒注意???

我們首先來看一下對應url如何對映到invoker的!

/**
 * 根據invokerURL列表轉換為invoker列表。轉換規則如下:
 * 1.如果url已經被轉換為invoker,則不在重新引用,直接從快取中獲取,注意如果url中任何一個引數變更也會重新引用
 * 2.如果傳入的invoker列表不為空,則表示最新的invoker列表
 * 3.如果傳入的invokerUrl列表是空,則表示只是下發的override規則或route規則,需要重新交叉對比,決定是否需要重新引用。
 * @param invokerUrls 傳入的引數不能為null
 */
private void refreshInvoker(List<URL> invokerUrls){
    if
(invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) { this.forbidden = true; // 禁止訪問 this.methodInvokerMap = null; // 置空列表 destroyAllInvokers(); // 關閉所有Invoker } else { this.forbidden = false; // 允許訪問 Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap; // local reference if (invokerUrls.size() == 0 && this.cachedInvokerUrls != null){ invokerUrls.addAll(this.cachedInvokerUrls); } else { this.cachedInvokerUrls = new HashSet<URL>(); this.cachedInvokerUrls.addAll(invokerUrls);//快取invokerUrls列表,便於交叉對比 } if (invokerUrls.size() ==0 ){ return; } Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls) ;// 將URL列表轉成Invoker列表 Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap); // 換方法名對映Invoker列表 // state change //如果計算錯誤,則不進行處理. if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0 ){ logger.error(new IllegalStateException("urls to invokers error .invokerUrls.size :"+invokerUrls.size() + ", invoker.size :0. urls :"+invokerUrls.toString())); return ; } this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap; this.urlInvokerMap = newUrlInvokerMap; try{ destroyUnusedInvokers(oldUrlInvokerMap,newUrlInvokerMap); // 關閉未使用的Invoker }catch (Exception e) { logger.warn("destroyUnusedInvokers error. ", e); } } }

我們來看到這邊通過invokerUrls來重新整理獲取到對應的遠端執行者 那麼這段核心程式碼將會對映直接的呼叫者!根據method的對映newMethodInvokerMap 也就是根據method名稱獲取到最新的invoker列表

如下方法通過url轉換成為特定的根據method的invoker列表

/**
 * 將invokers列表轉成與方法的對映關係
 *
 * @param invokersMap Invoker列表
 * @return Invoker與方法的對映關係
 */
private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) {
    Map<String, List<Invoker<T>>> newMethodInvokerMap = new HashMap<String, List<Invoker<T>>>();
    // 按提供者URL所宣告的methods分類,相容註冊中心執行路由過濾掉的methods
    List<Invoker<T>> invokersList = new ArrayList<Invoker<T>>();
    if (invokersMap != null && invokersMap.size() > 0) {
        for (Invoker<T> invoker : invokersMap.values()) {
            String parameter = invoker.getUrl().getParameter(Constants.METHODS_KEY);
            if (parameter != null && parameter.length() > 0) {
                String[] methods = Constants.COMMA_SPLIT_PATTERN.split(parameter);
                if (methods != null && methods.length > 0) {
                    for (String method : methods) {
                        if (method != null && method.length() > 0
                                && ! Constants.ANY_VALUE.equals(method)) {
                            List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
                            if (methodInvokers == null) {
                                methodInvokers = new ArrayList<Invoker<T>>();
                                newMethodInvokerMap.put(method, methodInvokers);
                            }
                            methodInvokers.add(invoker);
                        }
                    }
                }
            }
            invokersList.add(invoker);
        }
    }
    newMethodInvokerMap.put(Constants.ANY_VALUE, invokersList);
    if (serviceMethods != null && serviceMethods.length > 0) {
        for (String method : serviceMethods) {
            List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
            if (methodInvokers == null || methodInvokers.size() == 0) {
                methodInvokers = invokersList;
            }
            newMethodInvokerMap.put(method, route(methodInvokers, method));
        }
    }
    // sort and unmodifiable
    for (String method : new HashSet<String>(newMethodInvokerMap.keySet())) {
        List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
        Collections.sort(methodInvokers, InvokerComparator.getComparator());
        newMethodInvokerMap.put(method, Collections.unmodifiableList(methodInvokers));
    }
    return Collections.unmodifiableMap(newMethodInvokerMap);
}

由於通過url拼接的時候如下操作

if (generic) {
    map.put("generic", String.valueOf(true));
    map.put("methods", Constants.ANY_VALUE);
} else {
    String revision = Version.getVersion(interfaceClass, version);
    if (revision != null && revision.length() > 0) {
        map.put("revision", revision);
    }

    String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
    if(methods.length == 0) {
        logger.warn("NO method found in service interface " + interfaceClass.getName());
        map.put("methods", Constants.ANY_VALUE);
    }
    else {
        map.put("methods", StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
    }
}

很明顯的set將會抹除掉重複的方法名稱 那麼實質上在url上不會出現相同的methodname

那麼為何在使用的時候沒有錯呢???

我們來繼續回顧上一段程式碼

由於根據方法名稱放入了特定的invoker 而後根據invoker傳遞給特殊的呼叫引數【這個就是實際的網路請求 也就是rpc】

此時可以看到真正傳遞過去的是

/**
 * Invocation. (API, Prototype, NonThreadSafe)
 *
 * @serial Don't change the class name and package name.
 * @see com.alibaba.dubbo.rpc.Invoker#invoke(Invocation)
 * @see com.alibaba.dubbo.rpc.RpcInvocation
 * @author qian.lei
 * @author william.liangf
 */
public interface Invocation {
/**
 * get method name.
 *
 * @serial
 * @return method name.
 */
String getMethodName();

/**
 * get parameter types.
 *
 * @serial
 * @return parameter types.
 */
Class<?>[] getParameterTypes();

/**
 * get arguments.
 *
 * @serial
 * @return arguments.
 */
Object[] getArguments();

/**
 * get attachments.
 *
 * @serial
 * @return attachments.
 */
Map<String, String> getAttachments();

/**
    * get attachment by key.
    *
    * @serial
    * @return attachment value.
    */
String getAttachment(String key);

/**
    * get attachment by key with default value.
    *
    * @serial
    * @return attachment value.
    */
String getAttachment(String key, String defaultValue);

   /**
    * get the invoker in current context.
    *
    * @transient
    * @return invoker.
    */
   Invoker<?> getInvoker();
}

很明顯這裡面可以獲取到真實的引數 包括引數型別 自然呼叫不會出錯了啊!!!

深思

可以認為沒問題了麼???對的 大部分場景是沒有問題的!!!

彩蛋

設想如此場景

存在同一個服務的兩個副本

假設分別稱為服務1-1和服務1-2

服務1-1中存在方法為getA()

結果我們升級了服務1-1 增加了方法getA(A a) ====>只是多了一個方法

當我們使用藍綠髮布的時候 首先將服務1-2成功升級多了一個getA(A a)方法

此時refer到該service的 接收到服務推送 此時重新解析methodMap 此時getA方法中放置了兩個invoker 分別對用服務1-1和服務1-2

那麼當呼叫refer呼叫getA()從服務1-1和服務1-2呼叫均沒有問題 但是refer也升級了之後呼叫大搜了方法getA(A a)

此時將呼叫到了getA 但是方法列表仍然返回了兩個invoker 此時如果取到了服務1-1 那麼在呼叫之後服務1-1 將會發生失敗

我們可以檢視實質上通過netty等方式傳輸之後 在provider可以獲得一個對應的DecodeableRpcInvocation 這樣就回到了包裝前的invoker

public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
    // TODO Wrapper類不能正確處理帶$的類名
    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);
        }
    };
}

最終完成業務呼叫