1:分析

不管是通过xml方式,还是注解方式,还是API方式都会生成对应的XXXConfig类,然后解析这些XXXConfig类生成URL的参数信息,例如<dubbo:application> -> ApplicationConfig,<dubbo:registry> -> RegistryConfig
如果我们查看在zk中注册的服务提供者信息(服务消费者类似),可能看到如下的信息:

[zk: localhost:2181(CONNECTED) 8] ls /dubbo/dongshi.daddy.api.UserRpcService/providers 
[dubbo://192.168.10.119:20881/dongshi.daddy.api.UserRpcService?anyhost=true&application=user-service-provider11191458&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=dongshi.daddy.api.UserRpcService&methods=get&pid=19844&release=2.7.4.1&side=provider&threads=101×tamp=1637462018314]

比如其中的端口号20881,线程数threads=101就是通过类ProtocolConfig来配置的,interface=dongshi.daddy.api.UserRpcService就是通过ServiceConfig来配置的,本文我们要分析的也正是dubbo对于这些配置的相关源码,推荐通过这篇文章 来感受一下。

我们想要使用dubbo,不管是服务提供者还是服务消费者,为了能够定制和表达自己的信息,或者是自己要使用的信息,需要进行一系列的配置,比如服务提供者要设置自己的应用信息,协议信息,连接注册中心的信息等,dubbo针对此提供了配置相关API,其中顶层的是一个抽象类org.apache.dubbo.config.AbstractConfig,在这个抽象类中主要定义了一些公共的属性,公共的工具类等,我们在后面的部分来继续看下这些内容。

2:AbstractConfig

org.apache.dubbo.config.AbstractConfig是dubbo配置相关的顶层抽象类,在这篇文章 中我使用了相关的API来进行服务提供者和服务消费者的定义,比如如下定义服务提供者的代码:

public class ProviderApplication {

    public static void main(String[] args) {
        // 当前应用配置
        ApplicationConfig applicationConfig = new ApplicationConfig();
        applicationConfig.setName("user-service-provider11191458");
        // 连接注册中心配置
        RegistryConfig registryConfig = new RegistryConfig();
        registryConfig.setProtocol("zookeeper");
        registryConfig.setAddress("127.0.0.1:2181");
        // 服务提供者协议配置
        ProtocolConfig protocolConfig = new ProtocolConfig();
        protocolConfig.setName("dubbo");
        protocolConfig.setPort(20880);
        protocolConfig.setThreads(100);
        // 暴露服务
        ServiceConfig<UserRpcService> serviceConfig = new ServiceConfig<>();
        serviceConfig.setApplication(applicationConfig);
        serviceConfig.setRegistry(registryConfig); // 设置注册地址
        serviceConfig.setProtocol(protocolConfig); // 设置协议
        serviceConfig.setInterface(UserRpcService.class);
        serviceConfig.setRef(new UserRpcServiceImpl()); // 设置具体实现类
        serviceConfig.export(); // 完成暴露

        new Scanner(System.in).next();

    }
}

其中的类ApplicationConfig是AbstractConfig的子类,全限定名是org.apache.dubbo.config.ApplicationConfig,我们以代码registryConfig.setProtocol("zookeeper");看下是如何调用到AbstractConfig的工具类方法的,首先执行方法如下:

// org.apache.dubbo.config.ApplicationConfig#setName
public void setName(String name) {
    // 2021-11-21 16:06:24
    checkName(NAME, name);
    this.name = name;
    // 2021-11-21 16:07:02
    if (StringUtils.isEmpty(id)) {
        id = name;
    }
}

2021-11-21 16:06:24处调用的就是父类AbstractConfig的方法了,源码如下:

// org.apache.dubbo.config.AbstractConfig#checkName
protected static void checkName(String property, String value) {
    checkProperty(property, value, MAX_LENGTH, PATTERN_NAME);
}

// org.apache.dubbo.config.AbstractConfig#checkProperty
protected static void checkProperty(String property, String value, int maxlength, Pattern pattern) {
    // 允许给空
    if (StringUtils.isEmpty(value)) {
        return;
    }
    // 判断长度是否符合要求
    if (value.length() > maxlength) {
        throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" is longer than " + maxlength);
    }
    // 判断内容是否符合要求
    if (pattern != null) {
        Matcher matcher = pattern.matcher(value);
        if (!matcher.matches()) {
            throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" contains illegal " +
                    "character, only digit, letter, '-', '_' or '.' is legal.");
        }
    }
}

修改代码registryConfig.setProtocol("zookeeper");->registryConfig.setProtocol("zookeeper***");,因为不允许出现*,则会抛出如下异常:

Exception in thread "main" java.lang.IllegalStateException: Invalid protocol="zookeeper**" contains illegal character, only digit, letter, '-', '_' or '.' is legal.
	at org.apache.dubbo.config.AbstractConfig.checkProperty(AbstractConfig.java:381)
	at org.apache.dubbo.config.AbstractConfig.checkName(AbstractConfig.java:339)
	at org.apache.dubbo.config.RegistryConfig.setProtocol(RegistryConfig.java:169)
	at dongshi.daddy.provider.ProviderApplication.main(ProviderApplication.java:23)

2021-11-21 16:07:02处是设置id,这也是在AbstractConfig中定义的属性,用来作为配置的唯一标识,定义如下:

/**
 * The config id
 */
protected String id;

接下来我们来看下方法org.apache.dubbo.config.AbstractConfig#appendParameters,在看这个方法之前先看下3:URL,4:Parameter

// org.apache.dubbo.config.AbstractConfig#appendParameters(java.util.Map<java.lang.String,java.lang.String>, java.lang.Object, java.lang.String)
// parameters,url的参数信息,最终封装到org.apache.dubbo.common.URL#parameters
// config:用于解析url参数的配置类
// prefix:前缀,配置项添加到参数中的键的前缀
protected static void appendParameters(Map<String, String> parameters, Object config, String prefix) {
    // 配置类为null直接return
    if (config == null) {
        return;
    }
    // 获取所有的方法
    Method[] methods = config.getClass().getMethods();
    // 依此处理每个方法,获取url的参数信息
    for (Method method : methods) {
        try {
            String name = method.getName();
            if (MethodUtils.isGetter(method)) { // 普通的get方法
                // 尝试获取@Parameter注解
                Parameter parameter = method.getAnnotation(Parameter.class);
                // 当返回类型是Object,配置了@Paramter注解,但是注解的excluded属性为true,则continue,即过滤掉该方法 
                if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
                    continue;
                }
                String key;
                // 获取key,配置了@Parameter注解则使用key属性,否则使用属性值
                if (parameter != null && parameter.key().length() > 0) {
                    key = parameter.key();
                } else {
                    key = calculatePropertyFromGetter(name);
                }
                // 反射调用get方法获取值
                Object value = method.invoke(config);
                String str = String.valueOf(value).trim();
                if (value != null && str.length() > 0) {
                    // 需要URL编码
                    if (parameter != null && parameter.escaped()) {
                        str = URL.encode(str);
                    }
                    // 需要拼接前缀
                    if (parameter != null && parameter.append()) {
                        String pre = parameters.get(DEFAULT_KEY + "." + key);
                        if (pre != null && pre.length() > 0) {
                            str = pre + "," + str;
                        }
                        pre = parameters.get(key);
                        if (pre != null && pre.length() > 0) {
                            str = pre + "," + str;
                        }
                    }
                    if (prefix != null && prefix.length() > 0) {
                        key = prefix + "." + key;
                    }
                    parameters.put(key, str);
                } else if (parameter != null && parameter.required()) { // Paramter注解中设置了强制需要,但是没有返回值,则抛出java.lang.IllegalStateException
                    throw new IllegalStateException(config.getClass().getSimpleName() + "." + key + " == null");
                }
            } else if (isParametersGetter(method)) { // 如果是getParameter方法,这种方式是直接通过返回map的方式来获取参数,即批量方式
                Map<String, String> map = (Map<String, String>) method.invoke(config, new Object[0]);
                parameters.putAll(convert(map, prefix));
            }
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

与该方法类似的还有一个org.apache.dubbo.config.AbstractConfig#appendAttributes是用来获取方法上设置了Paramter(attributes=true)的方法,源码如下:

@Deprecated // 废弃。慎用!
protected static void appendAttributes(Map<String, Object> parameters, Object config, String prefix) {
    if (config == null) {
        return;
    }
    Method[] methods = config.getClass().getMethods();
    // 循环处理
    for (Method method : methods) {
        try {
            // 获取@org.apache.dubbo.config.support.Parameter注解
            Parameter parameter = method.getAnnotation(Parameter.class);
            // 配置了,并且attribute属性值为true才继续
            if (parameter == null || !parameter.attribute()) {
                continue;
            }
            // 获取方法名称
            String name = method.getName();
            // 获取键和值
            if (MethodUtils.isGetter(method)) {
                String key;
                if (parameter.key().length() > 0) {
                    key = parameter.key();
                } else {
                    key = calculateAttributeFromGetter(name);
                }
                Object value = method.invoke(config);
                if (value != null) {
                    if (prefix != null && prefix.length() > 0) {
                        key = prefix + "." + key;
                    }
                    parameters.put(key, value);
                }
            }
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }
}

具体的配置类和其对应的xml如下:

ApplicationConfig:<dubbo:application>配置应用级别的信息,如名称,所有者等。
RegistryConfig:<dubbo:registry>配置注册中心的信息,如配置zk,nacos的地址。
ModuleConfig:<dubbo:module>。
MonitorConfig:<dubbo:monitor>。
ArgumentConfig:<dubbo:argument>。

3:URL

计算机中URL形式是协议://ip:port/path?key1=val1&key2=val2&key3=val3以及协议://username:password/path?key1=val1&key2=val2&key3=val3,org.apache.dubbo.common.URL类就是是用来封装dubbo中可能使用到的url格式信息的类,如zookeeper://127.0.0.1:2181/ConfigCenterConfig?check=true&config-file=dubbo.properties
dubbo://192.168.10.119:20881/dongshi.daddy.api.UserRpcService?anyhost=true&application=user-service-provider11191458&deprecated=false,
consumer://192.168.10.119/dongshi.daddy.api.UserRpcService?application=user-service-consumer37,在整个网络世界可能的格式如下:

常规的:
   http://www.facebook.com/friends?param1=value1¶m2=value2
   http://username:password@10.20.130.230:8080/list?version=1.0.0
   ftp://username:password@192.168.1.7:21/1/read.txt
   registry://192.168.1.7:9090/org.apache.dubbo.service1?param1=value1¶m2=value2
非常规的:
   192.168.1.3:20880->这种情况,url协议=null,url host=192.168.1.3, port = 20880, url path = null
   file:///home/user1/router.js?type=script->这种情况 url协议=file,url host=null,url path=/home/user1/router.js
   file://home/user1/router.js?type=script->这种情况, url protocol = file, url host = home, url path = user1/router.js
   file:///D:/1/router.js?type=script->这种情况, url protocol = file, url host = null, url path = D:/1/router.js
   file:/D:/1/router.js?type=script->这种情况同上一种情况,file:///D:/1/router.js?type=script
   /home/user1/router.js?type=script->这种情况, url protocol = null, url host = null, url path = home/user1/router.js
   home/user1/router.js?type=script->这种情况, url protocol = null, url host = home, url path = user1/router.js

URL类源码如下:

// org.apache.dubbo.common.URL
public /*final**/
class URL implements Serializable {
    private static final long serialVersionUID = -1985165475234910535L;
    // url协议
    private final String protocol;
    // url用户名
    private final String username;
    // url密码
    private final String password;
    // url中的host,默认是注册中心的地址,如zk的ip地址
    private final String host;
    // url中的port,默认是注册中心的端口,如zk的默认端口2181
    private final int port;
    // url路径
    private final String path;
    // url参数
    private final Map<String, String> parameters;

    // ==== 提高效率相关缓存参数开始 ===== //
    private volatile transient Map<String, Number> numbers;

    private volatile transient Map<String, URL> urls;

    private volatile transient String ip;

    private volatile transient String full;

    private volatile transient String identity;

    private volatile transient String parameter;

    private volatile transient String string;
    // ==== 提高效率相关缓存参数结束 ===== //
}

4:Parameter

dubbo会通过类的getXxx方法来生成url参数的key和value,默认key就是属性本身,value就是方法的返回值,为了可自定义,定义了注解org.apache.dubbo.config.support.Parameter用在方法上,源码如下:

// org.apache.dubbo.config.support.Parameter
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Parameter {
    // 设置参数键
    String key() default "";
    // 是否必须
    boolean required() default false;
    // 是否排除不作为参数的一部分
    boolean excluded() default false;
    // 是否转义
    boolean escaped() default false;
    // 属性
    boolean attribute() default false;
    // 是否拼接,如拼接前缀
    boolean append() default false;

    boolean useKeyAsProperty() default true;
}