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

最终完成业务调用


原网址: 访问
创建于: 2018-10-13 16:31:18
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论