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);
}
};
}
最終完成業務呼叫