Dubbo之URL源码解析

功能概述

  • URL是dubbo的公共契约,可叫做“配置总线”,“统一配置模型”,用于各个扩展点的数据传递。URL进行数据模型统一,方便理解,且易读易写。

功能分析

核心类URL分析

主要成员变量分析

private final String protocol; //协议

private final String username; //用户名

private final String password; //密码

private final String host; //主机号

private final int port; //端口(默认值为0)

private final String path; //路径(是host后面, "/" 到 "?" 之间的字符串值,而interface接口信息是参数"interface"的值)

private final Map<String, String> parameters; //参数键值对:存的是url中的参数键值,即?与&分隔的键值对

private final Map<String, Map<String, String>> methodParameters; //方法级别的参数设置,如<"sayHello2, ">

private volatile transient String full; //带有完整信息的url字符串

private transient String serviceKey; //服务对应的键:格式如:group/path:version

主要成员方法分析

构造方法

public URL(String protocol,
           String username,
           String password,
           String host,
           int port,
           String path,
           Map<String, String> parameters,
           Map<String, Map<String, String>> methodParameters) {
    if (StringUtils.isEmpty(username)
            && StringUtils.isNotEmpty(password)) {
        throw new IllegalArgumentException("Invalid url, password without username!"); //密码与用户名要么没有,要么一起出现
    }
    this.protocol = protocol;
    this.username = username;
    this.password = password;
    this.host = host;
    this.port = Math.max(port, 0);
    this.address = getAddress(this.host, this.port); //构建地址,由host、port组成,如:127.0.0.1:2181(port<=0时,只包含host值)

    // trim the beginning "/"
    while (path != null && path.startsWith("/")) { //循环去掉path前的"/"
        path = path.substring(1);
    }
    this.path = path;
    if (parameters == null) {
        parameters = new HashMap<>();
    } else {
        parameters = new HashMap<>(parameters);
    }
    this.parameters = Collections.unmodifiableMap(parameters); //unmodifiableMap:不可修改的Map,(内部维护者用final修饰的Map,只读不可修修改 put、remove等)
    this.methodParameters = Collections.unmodifiableMap(methodParameters);
}

解析URL字符串

public static URL valueOf(String url) { //按URL语法规则进行解析,并构建URL对象。 如输入字符串为:zookeeper://127.0.0.1:2181?name=test
    if (url == null || (url = url.trim()).length() == 0) { //URL语法格式:protocol://username:password@host:port/path?key1=value1&key2=value2
        throw new IllegalArgumentException("url == null");
    }
    String protocol = null;
    String username = null;
    String password = null;
    String host = null;
    int port = 0;
    String path = null;
    Map<String, String> parameters = null;
    int i = url.indexOf('?'); // separator between body and parameters(使用"?" 来分隔出 url的主体和参数)
    if (i >= 0) { //(url的参数parameters处理)判断是否包含参数,若url中包含"?",则先把参数解析出来,把"?"后面的字符串先处理掉
        String[] parts = url.substring(i + 1).split("&"); //按&符号分隔出参数键值对
        parameters = new HashMap<>();
        for (String part : parts) { //遍历参数键值对,如name=test
            part = part.trim();
            if (part.length() > 0) {
                int j = part.indexOf('='); //按等号解析,解析出参数的键值对
                if (j >= 0) {
                    String key = part.substring(0, j); //key如:name
                    String value = part.substring(j + 1);//value如:test (dubbo不会对key、value去空格,若有空格,原样输出,如key=value1 value2,那么值就为"value1 value2")
                    parameters.put(key, value); //设置到url的参数map集合中
                    // compatible with lower versions registering "default." keys
                    if (key.startsWith(DEFAULT_KEY_PREFIX)) { //若参数名是"default."开头的,则把这个前缀去掉存储,即这个值会有两个不同的key
                        parameters.putIfAbsent(key.substring(DEFAULT_KEY_PREFIX.length()), value);
                    }
                } else { //若没有带上等号的,则键值对都存一样的值
                    parameters.put(part, part); //若没有带上等号,则键值都存为一样的(如?k0&k1=v1,则k0的值存为k0)
                }
            }
        }
        url = url.substring(0, i); //截取参数前的url,如zookeeper://127.0.0.1:2181
    }
    i = url.indexOf("://"); //解析出protocol值
    if (i >= 0) { //解析协议,如:zookeeper://127.0.0.1:2181
        if (i == 0) { // "://"前面没有设置协议时,就会抛出异常
            throw new IllegalStateException("url missing protocol: \"" + url + "\"");
        }
        protocol = url.substring(0, i); //如:zookeeper
        url = url.substring(i + 3); //如:127.0.0.1:2181
    } else { //未设置协议(系统没有设置默认协议,若没设定,则protocol=null)
        // case: file:/path/to/file.txt(兼容不规范的写法)
        i = url.indexOf(":/");
        if (i >= 0) {
            if (i == 0) {
                throw new IllegalStateException("url missing protocol: \"" + url + "\""); //URL处理protocol是必须的,其它属性都是可选的
            }
            protocol = url.substring(0, i);
            url = url.substring(i + 1);
        }
    }

    i = url.indexOf('/'); //解析出资源路径,如127.0.0.1:2181/all的 path为all
    if (i >= 0) {
        path = url.substring(i + 1); //接口路径,即接口路径,如com.foo.BarService,path中没有"?"后面带的参数
        url = url.substring(0, i);
    }
    i = url.lastIndexOf('@'); //解析出用户名、密码(未设置用户名、密码,则为null,以最后一个@来分隔,用户名和密码可包含@符号)
    if (i >= 0) {
        username = url.substring(0, i);
        int j = username.indexOf(':');
        if (j >= 0) {
            password = username.substring(j + 1); //解析出用户名、密码
            username = username.substring(0, j);
        }
        url = url.substring(i + 1);
    }
    i = url.lastIndexOf(':'); //解析出host与port,url如:127.0.0.1:2181(以最后一个":"做分隔)
    if (i >= 0 && i < url.length() - 1) {  //解析host、ip
        if (url.lastIndexOf('%') > i) {
            // ipv6 address with scope id (若ipv6地址带上范围id,则忽略不处理,即不做截取)
            // e.g. fe80:0:0:0:894:aeec:f37d:23e1%en0 对应的host、address为 fe80:0:0:0:894:aeec:f37d:23e1%en0
            // see https://howdoesinternetwork.com/2013/ipv6-zone-id
            // ignore
        } else { //host、port分隔处理
            port = Integer.parseInt(url.substring(i + 1));
            url = url.substring(0, i);
        }
    }
    if (url.length() > 0) { //若url字符串经过前面处理后,还存在字符,则设置为host,如url="10.20.130.230"
        host = url;
    }

    return new URL(protocol, username, password, host, port, path, parameters); //构建新的URL对象
}

构建URL字符串

private String buildString(boolean appendUser, boolean appendParameter, boolean useIP, boolean useService, String... parameters) {
    StringBuilder buf = new StringBuilder();
    if (StringUtils.isNotEmpty(protocol)) {
        buf.append(protocol);
        buf.append("://");
    }
    if (appendUser && StringUtils.isNotEmpty(username)) { //需要附加用户信息且用户名、密码不为空时进行拼接
        buf.append(username);
        if (StringUtils.isNotEmpty(password)) {
            buf.append(":");
            buf.append(password);
        }
        buf.append("@");
    }
    String host;
    if (useIP) { //是否使用ip作为主机号
        host = getIp(); //使用ip
    } else {
        host = getHost(); //使用host
    }
    if (StringUtils.isNotEmpty(host)) { //主机号不为空时,进行拼接
        buf.append(host);
        if (port > 0) {
            buf.append(":");
            buf.append(port);
        }
    }
    String path;
    if (useService) { //是否使用服务key作为path值
        path = getServiceKey(); //服务key,在有group时,是会带上group值的,如group/xxx.service
    } else {
        path = getPath();
    }
    if (StringUtils.isNotEmpty(path)) {
        buf.append("/");
        buf.append(path);
    }

    if (appendParameter) { //是否需要添加附加参数
        buildParameters(buf, true, parameters);
    }
    return buf.toString();
}
  • 构建的URL字符串格式为:protocol://username:password@ip:port/path?parameter

辅助类URLBuilder分析

主要成员方法分析

读取URL

public static URLBuilder from(URL url) { //读取url内容,创建构造器URLBuilder
    String protocol = url.getProtocol();
    String username = url.getUsername();
    String password = url.getPassword(); //读取URL内容
    String host = url.getHost();
    int port = url.getPort();
    String path = url.getPath();
    Map<String, String> parameters = new HashMap<>(url.getParameters());
    Map<String, Map<String, String>> methodParameters = new HashMap<>(url.getMethodParameters());
    return new URLBuilder( //将传入的URL参数,设置到URLBuilder的成员属性中
            protocol,
            username,
            password,
            host,
            port,
            path,
            parameters,
            methodParameters);
}

构建URL

public URL build() { //用URLBuilder维护的参数构建URL
    if (StringUtils.isEmpty(username) && StringUtils.isNotEmpty(password)) {
        throw new IllegalArgumentException("Invalid url, password without username!");
    }
    port = port < 0 ? 0 : port;
    // trim the leading "/"
    int firstNonSlash = 0; //Slash: 斜杠
    if (path != null) {
        while (firstNonSlash < path.length() && path.charAt(firstNonSlash) == '/') {
            firstNonSlash++; //找到斜杠的位置,并且加1
        }
        if (firstNonSlash >= path.length()) {
            path = "";
        } else if (firstNonSlash > 0) {
            path = path.substring(firstNonSlash); //获取去除斜杠后的子串
        }
    }
    if (CollectionUtils.isEmptyMap(methodParameters)) { //创建URL对象
        return new URL(protocol, username, password, host, port, path, parameters);
    } else {
        return new URL(protocol, username, password, host, port, path, parameters, methodParameters);
    }
}

辅助类URLStrParser分析

主要成员方法分析

解析已编码的URL字符串

public static URL parseEncodedStr(String encodedURLStr) { //解析编码后的URL字符串,产生对应的URL对象
    Map<String, String> parameters = null; //编码前的字符串/context/path?version=1.0.0&application=morgan,编码后的字符串:%2Fcontext%2Fpath%3Fapplication%3Dmorgan%26version%3D1.0.0
    int pathEndIdx = encodedURLStr.indexOf("%3F");// '?'  查找参数分隔符(%3F对应的ASCII值为'?')
    if (pathEndIdx >= 0) { //url中包含参数
        parameters = parseEncodedParams(encodedURLStr, pathEndIdx + 3); //取%3F后面的字符串处理
    } else { //url中不包含参数
        pathEndIdx = encodedURLStr.length();
    }

    //decodedBody format: [protocol://][username:password@][host:port]/[path]
    String decodedBody = decodeComponent(encodedURLStr, 0, pathEndIdx, false, DECODE_TEMP_BUF.get());
    return parseURLBody(encodedURLStr, decodedBody, parameters);
}

解析已编码的URL参数

private static Map<String, String> parseEncodedParams(String str, int from) { //解析出编码url中的参数键值对
    int len = str.length(); //取出字符串长度
    if (from >= len) { //起始位置不能超过字符串长度
        return Collections.emptyMap();
    }

    TempBuf tempBuf = DECODE_TEMP_BUF.get(); //从本地线程中获取缓存的TempBuf
    Map<String, String> params = new HashMap<>();
    int nameStart = from;
    int valueStart = -1;
    int i;
    for (i = from; i < len; i++) { //遍历url字符串的字符(from为参数起始位置)
        char ch = str.charAt(i);
        if (ch == '%') { //遇到百分号分隔符,解码得到原始的字符(ASCII中的字符,经过url编码后,都是类似 %xx,即%后面带上两个16进制字符)
            if (i + 3 > len) { //分隔符不是完整的情况,抛出异常,比如%3、%等,应该是%3D,百分号后面带两个十六进制数
                throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + str);
            }
            ch = (char) decodeHexByte(str, i + 1); //解码16进制字符,得到原有字符,如"3D"处理后,得到字符'='
            i += 2;
        }

        switch (ch) { //找到指定的分隔符,做对应的处理
            case '=': //按键值对处理
                if (nameStart == i) { //url未经过编码时,进入此处
                    nameStart = i + 1;
                } else if (valueStart < nameStart) {
                    valueStart = i + 1; //记录值的下标
                }
                break;
            case ';':
            case '&': //多个参数时,进行参数拼接
                addParam(str, true, nameStart, valueStart, i - 2, params, tempBuf);
                nameStart = i + 1;
                break;
            default: //非分隔符,不做处理
                // continue
        }
    }
    addParam(str, true, nameStart, valueStart, i, params, tempBuf);
    return params;
}

问题点答疑

  • URL都承载了什么信息,都有哪些功能?

    • 解答:可以理解为数据总线,扩展点之间的数据传递,都是通过URL传递的。使用URL进行上下文信息传递,可以将一系列的参数联系起来,使代码易读易写,
      URL中主要包含了protocol、host、port等主要信息,其余的信息,可放入parameter参数中。在没有URL 之前,只能以字符串传递参数,不停的解析和拼装,导致相同类型的接口,参数时而 Map, 时而 Parameters 类包装。
  • URLBuilder都有什么功能用途?

    • 解答:URLBuilder是URL的辅助类,使用了建造者设计模式,方便的构建URL对象
  • URLStrParser#DECODE_TEMP_BUF成员变量的用途是什么?

    • 解答:ThreadLocal.withInitial(() -> new TempBuf(1024)); 从TempBuf的定义看,里面存了字节数组和字符数组,即对字符数组或字节数组做临时缓存。构建字节数组或字符数组,只要容量不超过TempBuf的容量,就取TempBuf中的字节或字符数组。

归纳总结

  • 在 dubbo 中,类似Java中的URL,主要用于在各个扩展点之间传递数据,URL的数据格式为:protocol://username:password@ip:port/path?parameter。Dubbo 使用 URL 来统一描述了所有对象和配置信息,并贯穿在整个 Dubbo 框架之中

你可能感兴趣的:(Dubbo,dubbo)