2.1 前置工作

前置工作主要包含两个部分,分别是配置检查,以及 URL 装配。在导出服务之前,Dubbo 需要检查用户的配置是否合理,或者为用户补充缺省配置。配置检查完成后,接下来需要根据这些配置组装 URL。在 Dubbo 中,URL 的作用十分重要。Dubbo 使用 URL 作为配置载体,所有的拓展点都是通过 URL 获取配置。这一点,官方文档中有所说明。

采用 URL 作为配置信息的统一格式,所有扩展点都通过传递 URL 携带配置信息。

接下来,我们先来分析配置检查部分的源码,随后再来分析 URL 组装部分的源码。

2.1.1 检查配置

本节我们接着前面的源码向下分析,前面说过 onApplicationEvent 方法在经过一些判断后,会决定是否调用 export 方法导出服务。那么下面我们从 export 方法开始进行分析,如下:

  1. public synchronized void export() {
  2. if (provider != null) {
  3. // 获取 export 和 delay 配置
  4. if (export == null) {
  5. export = provider.getExport();
  6. }
  7. if (delay == null) {
  8. delay = provider.getDelay();
  9. }
  10. }
  11. // 如果 export 为 false,则不导出服务
  12. if (export != null && !export) {
  13. return;
  14. }
  15. // delay > 0,延时导出服务
  16. if (delay != null && delay > 0) {
  17. delayExportExecutor.schedule(new Runnable() {
  18. @Override
  19. public void run() {
  20. doExport();
  21. }
  22. }, delay, TimeUnit.MILLISECONDS);
  23. // 立即导出服务
  24. } else {
  25. doExport();
  26. }
  27. }

export 方法对两项配置进行了检查,并根据配置执行相应的动作。首先是 export 配置,这个配置决定了是否导出服务。有时候我们只是想本地启动服务进行一些调试工作,我们并不希望把本地启动的服务暴露出去给别人调用。此时,我们可通过配置 export 禁止服务导出,比如:

  1. <dubbo:provider export="false" />

delay 配置顾名思义,用于延迟导出服务,这个就不分析了。下面,我们继续分析源码,这次要分析的是 doExport 方法。

  1. protected synchronized void doExport() {
  2. if (unexported) {
  3. throw new IllegalStateException("Already unexported!");
  4. }
  5. if (exported) {
  6. return;
  7. }
  8. exported = true;
  9. // 检测 interfaceName 是否合法
  10. if (interfaceName == null || interfaceName.length() == 0) {
  11. throw new IllegalStateException("interface not allow null!");
  12. }
  13. // 检测 provider 是否为空,为空则新建一个,并通过系统变量为其初始化
  14. checkDefault();
  15. // 下面几个 if 语句用于检测 provider、application 等核心配置类对象是否为空,
  16. // 若为空,则尝试从其他配置类对象中获取相应的实例。
  17. if (provider != null) {
  18. if (application == null) {
  19. application = provider.getApplication();
  20. }
  21. if (module == null) {
  22. module = provider.getModule();
  23. }
  24. if (registries == null) {...}
  25. if (monitor == null) {...}
  26. if (protocols == null) {...}
  27. }
  28. if (module != null) {
  29. if (registries == null) {
  30. registries = module.getRegistries();
  31. }
  32. if (monitor == null) {...}
  33. }
  34. if (application != null) {
  35. if (registries == null) {
  36. registries = application.getRegistries();
  37. }
  38. if (monitor == null) {...}
  39. }
  40. // 检测 ref 是否为泛化服务类型
  41. if (ref instanceof GenericService) {
  42. // 设置 interfaceClass 为 GenericService.class
  43. interfaceClass = GenericService.class;
  44. if (StringUtils.isEmpty(generic)) {
  45. // 设置 generic = "true"
  46. generic = Boolean.TRUE.toString();
  47. }
  48. // ref 非 GenericService 类型
  49. } else {
  50. try {
  51. interfaceClass = Class.forName(interfaceName, true, Thread.currentThread()
  52. .getContextClassLoader());
  53. } catch (ClassNotFoundException e) {
  54. throw new IllegalStateException(e.getMessage(), e);
  55. }
  56. // 对 interfaceClass,以及 <dubbo:method> 标签中的必要字段进行检查
  57. checkInterfaceAndMethods(interfaceClass, methods);
  58. // 对 ref 合法性进行检测
  59. checkRef();
  60. // 设置 generic = "false"
  61. generic = Boolean.FALSE.toString();
  62. }
  63. // local 和 stub 在功能应该是一致的,用于配置本地存根
  64. if (local != null) {
  65. if ("true".equals(local)) {
  66. local = interfaceName + "Local";
  67. }
  68. Class<?> localClass;
  69. try {
  70. // 获取本地存根类
  71. localClass = ClassHelper.forNameWithThreadContextClassLoader(local);
  72. } catch (ClassNotFoundException e) {
  73. throw new IllegalStateException(e.getMessage(), e);
  74. }
  75. // 检测本地存根类是否可赋值给接口类,若不可赋值则会抛出异常,提醒使用者本地存根类类型不合法
  76. if (!interfaceClass.isAssignableFrom(localClass)) {
  77. throw new IllegalStateException("The local implementation class " + localClass.getName() + " not implement interface " + interfaceName);
  78. }
  79. }
  80. if (stub != null) {
  81. // 此处的代码和上一个 if 分支的代码基本一致,这里省略
  82. }
  83. // 检测各种对象是否为空,为空则新建,或者抛出异常
  84. checkApplication();
  85. checkRegistry();
  86. checkProtocol();
  87. appendProperties(this);
  88. checkStubAndMock(interfaceClass);
  89. if (path == null || path.length() == 0) {
  90. path = interfaceName;
  91. }
  92. // 导出服务
  93. doExportUrls();
  94. // ProviderModel 表示服务提供者模型,此对象中存储了与服务提供者相关的信息。
  95. // 比如服务的配置信息,服务实例等。每个被导出的服务对应一个 ProviderModel。
  96. // ApplicationModel 持有所有的 ProviderModel。
  97. ProviderModel providerModel = new ProviderModel(getUniqueServiceName(), this, ref);
  98. ApplicationModel.initProviderModel(getUniqueServiceName(), providerModel);
  99. }

以上就是配置检查的相关分析,代码比较多,需要大家耐心看一下。下面对配置检查的逻辑进行简单的总结,如下:

  1. 检测 <dubbo:service> 标签的 interface 属性合法性,不合法则抛出异常
  2. 检测 ProviderConfig、ApplicationConfig 等核心配置类对象是否为空,若为空,则尝试从其他配置类对象中获取相应的实例。
  3. 检测并处理泛化服务和普通服务类
  4. 检测本地存根配置,并进行相应的处理
  5. 对 ApplicationConfig、RegistryConfig 等配置类进行检测,为空则尝试创建,若无法创建则抛出异常

配置检查并非本文重点,因此这里不打算对 doExport 方法所调用的方法进行分析(doExportUrls 方法除外)。在这些方法中,除了 appendProperties 方法稍微复杂一些,其他方法逻辑不是很复杂。因此,大家可自行分析。

2.1.2 多协议多注册中心导出服务

Dubbo 允许我们使用不同的协议导出服务,也允许我们向多个注册中心注册服务。Dubbo 在 doExportUrls 方法中对多协议,多注册中心进行了支持。相关代码如下:

  1. private void doExportUrls() {
  2. // 加载注册中心链接
  3. List<URL> registryURLs = loadRegistries(true);
  4. // 遍历 protocols,并在每个协议下导出服务
  5. for (ProtocolConfig protocolConfig : protocols) {
  6. doExportUrlsFor1Protocol(protocolConfig, registryURLs);
  7. }
  8. }

上面代码首先是通过 loadRegistries 加载注册中心链接,然后再遍历 ProtocolConfig 集合导出每个服务。并在导出服务的过程中,将服务注册到注册中心。下面,我们先来看一下 loadRegistries 方法的逻辑。

  1. protected List<URL> loadRegistries(boolean provider) {
  2. // 检测是否存在注册中心配置类,不存在则抛出异常
  3. checkRegistry();
  4. List<URL> registryList = new ArrayList<URL>();
  5. if (registries != null && !registries.isEmpty()) {
  6. for (RegistryConfig config : registries) {
  7. String address = config.getAddress();
  8. if (address == null || address.length() == 0) {
  9. // 若 address 为空,则将其设为 0.0.0.0
  10. address = Constants.ANYHOST_VALUE;
  11. }
  12. // 从系统属性中加载注册中心地址
  13. String sysaddress = System.getProperty("dubbo.registry.address");
  14. if (sysaddress != null && sysaddress.length() > 0) {
  15. address = sysaddress;
  16. }
  17. // 检测 address 是否合法
  18. if (address.length() > 0 && !RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
  19. Map<String, String> map = new HashMap<String, String>();
  20. // 添加 ApplicationConfig 中的字段信息到 map 中
  21. appendParameters(map, application);
  22. // 添加 RegistryConfig 字段信息到 map 中
  23. appendParameters(map, config);
  24. // 添加 path、pid,protocol 等信息到 map 中
  25. map.put("path", RegistryService.class.getName());
  26. map.put("dubbo", Version.getProtocolVersion());
  27. map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));
  28. if (ConfigUtils.getPid() > 0) {
  29. map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));
  30. }
  31. if (!map.containsKey("protocol")) {
  32. if (ExtensionLoader.getExtensionLoader(RegistryFactory.class).hasExtension("remote")) {
  33. map.put("protocol", "remote");
  34. } else {
  35. map.put("protocol", "dubbo");
  36. }
  37. }
  38. // 解析得到 URL 列表,address 可能包含多个注册中心 ip,
  39. // 因此解析得到的是一个 URL 列表
  40. List<URL> urls = UrlUtils.parseURLs(address, map);
  41. for (URL url : urls) {
  42. url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
  43. // 将 URL 协议头设置为 registry
  44. url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
  45. // 通过判断条件,决定是否添加 url 到 registryList 中,条件如下:
  46. // (服务提供者 && register = true 或 null)
  47. // || (非服务提供者 && subscribe = true 或 null)
  48. if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
  49. || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
  50. registryList.add(url);
  51. }
  52. }
  53. }
  54. }
  55. }
  56. return registryList;
  57. }

loadRegistries 方法主要包含如下的逻辑:

  1. 检测是否存在注册中心配置类,不存在则抛出异常
  2. 构建参数映射集合,也就是 map
  3. 构建注册中心链接列表
  4. 遍历链接列表,并根据条件决定是否将其添加到 registryList 中

关于多协议多注册中心导出服务就先分析到这,代码不是很多,接下来分析 URL 组装过程。

2.1.3 组装 URL

配置检查完毕后,紧接着要做的事情是根据配置,以及其他一些信息组装 URL。前面说过,URL 是 Dubbo 配置的载体,通过 URL 可让 Dubbo 的各种配置在各个模块之间传递。URL 之于 Dubbo,犹如水之于鱼,非常重要。大家在阅读 Dubbo 服务导出相关源码的过程中,要注意 URL 内容的变化。既然 URL 如此重要,那么下面我们来了解一下 URL 组装的过程。

  1. private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
  2. String name = protocolConfig.getName();
  3. // 如果协议名为空,或空串,则将协议名变量设置为 dubbo
  4. if (name == null || name.length() == 0) {
  5. name = "dubbo";
  6. }
  7. Map<String, String> map = new HashMap<String, String>();
  8. // 添加 side、版本、时间戳以及进程号等信息到 map 中
  9. map.put(Constants.SIDE_KEY, Constants.PROVIDER_SIDE);
  10. map.put(Constants.DUBBO_VERSION_KEY, Version.getProtocolVersion());
  11. map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));
  12. if (ConfigUtils.getPid() > 0) {
  13. map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));
  14. }
  15. // 通过反射将对象的字段信息添加到 map 中
  16. appendParameters(map, application);
  17. appendParameters(map, module);
  18. appendParameters(map, provider, Constants.DEFAULT_KEY);
  19. appendParameters(map, protocolConfig);
  20. appendParameters(map, this);
  21. // methods 为 MethodConfig 集合,MethodConfig 中存储了 &lt;dubbo:method> 标签的配置信息
  22. if (methods != null && !methods.isEmpty()) {
  23. // 这段代码用于添加 Callback 配置到 map 中,代码太长,待会单独分析
  24. }
  25. // 检测 generic 是否为 "true",并根据检测结果向 map 中添加不同的信息
  26. if (ProtocolUtils.isGeneric(generic)) {
  27. map.put(Constants.GENERIC_KEY, generic);
  28. map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
  29. } else {
  30. String revision = Version.getVersion(interfaceClass, version);
  31. if (revision != null && revision.length() > 0) {
  32. map.put("revision", revision);
  33. }
  34. // 为接口生成包裹类 Wrapper,Wrapper 中包含了接口的详细信息,比如接口方法名数组,字段信息等
  35. String[] methods = Wrapper.getWrapper(interfaceClass).getMethodNames();
  36. // 添加方法名到 map 中,如果包含多个方法名,则用逗号隔开,比如 method = init,destroy
  37. if (methods.length == 0) {
  38. logger.warn("NO method found in service interface ...");
  39. map.put(Constants.METHODS_KEY, Constants.ANY_VALUE);
  40. } else {
  41. // 将逗号作为分隔符连接方法名,并将连接后的字符串放入 map 中
  42. map.put(Constants.METHODS_KEY, StringUtils.join(new HashSet<String>(Arrays.asList(methods)), ","));
  43. }
  44. }
  45. // 添加 token 到 map 中
  46. if (!ConfigUtils.isEmpty(token)) {
  47. if (ConfigUtils.isDefault(token)) {
  48. // 随机生成 token
  49. map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
  50. } else {
  51. map.put(Constants.TOKEN_KEY, token);
  52. }
  53. }
  54. // 判断协议名是否为 injvm
  55. if (Constants.LOCAL_PROTOCOL.equals(protocolConfig.getName())) {
  56. protocolConfig.setRegister(false);
  57. map.put("notify", "false");
  58. }
  59. // 获取上下文路径
  60. String contextPath = protocolConfig.getContextpath();
  61. if ((contextPath == null || contextPath.length() == 0) && provider != null) {
  62. contextPath = provider.getContextpath();
  63. }
  64. // 获取 host 和 port
  65. String host = this.findConfigedHosts(protocolConfig, registryURLs, map);
  66. Integer port = this.findConfigedPorts(protocolConfig, name, map);
  67. // 组装 URL
  68. URL url = new URL(name, host, port, (contextPath == null || contextPath.length() == 0 ? "" : contextPath + "/") + path, map);
  69. // 省略无关代码
  70. }

上面的代码首先是将一些信息,比如版本、时间戳、方法名以及各种配置对象的字段信息放入到 map 中,map 中的内容将作为 URL 的查询字符串。构建好 map 后,紧接着是获取上下文路径、主机名以及端口号等信息。最后将 map 和主机名等数据传给 URL 构造方法创建 URL 对象。需要注意的是,这里出现的 URL 并非 java.net.URL,而是 com.alibaba.dubbo.common.URL。

上面省略了一段代码,这里简单分析一下。这段代码用于检测 <dubbo:method> 标签中的配置信息,并将相关配置添加到 map 中。代码如下:

  1. private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, List<URL> registryURLs) {
  2. // ...
  3. // methods 为 MethodConfig 集合,MethodConfig 中存储了 &lt;dubbo:method> 标签的配置信息
  4. if (methods != null && !methods.isEmpty()) {
  5. for (MethodConfig method : methods) {
  6. // 添加 MethodConfig 对象的字段信息到 map 中,键 = 方法名.属性名。
  7. // 比如存储 &lt;dubbo:method name="sayHello" retries="2"> 对应的 MethodConfig,
  8. // 键 = sayHello.retries,map = {"sayHello.retries": 2, "xxx": "yyy"}
  9. appendParameters(map, method, method.getName());
  10. String retryKey = method.getName() + ".retry";
  11. if (map.containsKey(retryKey)) {
  12. String retryValue = map.remove(retryKey);
  13. // 检测 MethodConfig retry 是否为 false,若是,则设置重试次数为0
  14. if ("false".equals(retryValue)) {
  15. map.put(method.getName() + ".retries", "0");
  16. }
  17. }
  18. // 获取 ArgumentConfig 列表
  19. List<ArgumentConfig> arguments = method.getArguments();
  20. if (arguments != null && !arguments.isEmpty()) {
  21. for (ArgumentConfig argument : arguments) {
  22. // 检测 type 属性是否为空,或者空串(分支1 ⭐️)
  23. if (argument.getType() != null && argument.getType().length() > 0) {
  24. Method[] methods = interfaceClass.getMethods();
  25. if (methods != null && methods.length > 0) {
  26. for (int i = 0; i < methods.length; i++) {
  27. String methodName = methods[i].getName();
  28. // 比对方法名,查找目标方法
  29. if (methodName.equals(method.getName())) {
  30. Class<?>[] argtypes = methods[i].getParameterTypes();
  31. if (argument.getIndex() != -1) {
  32. // 检测 ArgumentConfig 中的 type 属性与方法参数列表
  33. // 中的参数名称是否一致,不一致则抛出异常(分支2 ⭐️)
  34. if (argtypes[argument.getIndex()].getName().equals(argument.getType())) {
  35. // 添加 ArgumentConfig 字段信息到 map 中,
  36. // 键前缀 = 方法名.index,比如:
  37. // map = {"sayHello.3": true}
  38. appendParameters(map, argument, method.getName() + "." + argument.getIndex());
  39. } else {
  40. throw new IllegalArgumentException("argument config error: ...");
  41. }
  42. } else { // 分支3 ⭐️
  43. for (int j = 0; j < argtypes.length; j++) {
  44. Class<?> argclazz = argtypes[j];
  45. // 从参数类型列表中查找类型名称为 argument.type 的参数
  46. if (argclazz.getName().equals(argument.getType())) {
  47. appendParameters(map, argument, method.getName() + "." + j);
  48. if (argument.getIndex() != -1 && argument.getIndex() != j) {
  49. throw new IllegalArgumentException("argument config error: ...");
  50. }
  51. }
  52. }
  53. }
  54. }
  55. }
  56. }
  57. // 用户未配置 type 属性,但配置了 index 属性,且 index != -1
  58. } else if (argument.getIndex() != -1) { // 分支4 ⭐️
  59. // 添加 ArgumentConfig 字段信息到 map 中
  60. appendParameters(map, argument, method.getName() + "." + argument.getIndex());
  61. } else {
  62. throw new IllegalArgumentException("argument config must set index or type");
  63. }
  64. }
  65. }
  66. }
  67. }
  68. // ...
  69. }

上面这段代码 for 循环和 if else 分支嵌套太多,导致层次太深,不利于阅读,需要耐心看一下。大家在看这段代码时,注意把几个重要的条件分支找出来。只要理解了这几个分支的意图,就可以弄懂这段代码。请注意上面代码中⭐️符号,这几个符号标识出了4个重要的分支,下面用伪代码解释一下这几个分支的含义。

  1. // 获取 ArgumentConfig 列表
  2. for (遍历 ArgumentConfig 列表) {
  3. if (type 不为 null,也不为空串) { // 分支1
  4. 1. 通过反射获取 interfaceClass 的方法列表
  5. for (遍历方法列表) {
  6. 1. 比对方法名,查找目标方法
  7. 2. 通过反射获取目标方法的参数类型数组 argtypes
  8. if (index != -1) { // 分支2
  9. 1. argtypes 数组中获取下标 index 处的元素 argType
  10. 2. 检测 argType 的名称与 ArgumentConfig 中的 type 属性是否一致
  11. 3. 添加 ArgumentConfig 字段信息到 map 中,或抛出异常
  12. } else { // 分支3
  13. 1. 遍历参数类型数组 argtypes,查找 argument.type 类型的参数
  14. 2. 添加 ArgumentConfig 字段信息到 map
  15. }
  16. }
  17. } else if (index != -1) { // 分支4
  18. 1. 添加 ArgumentConfig 字段信息到 map
  19. }
  20. }

在本节分析的源码中,appendParameters 这个方法出现的次数比较多,该方法用于将对象字段信息添加到 map 中。实现上则是通过反射获取目标对象的 getter 方法,并调用该方法获取属性值。然后再通过 getter 方法名解析出属性名,比如从方法名 getName 中可解析出属性 name。如果用户传入了属性名前缀,此时需要将属性名加入前缀内容。最后将 <属性名,属性值> 键值对存入到 map 中就行了。限于篇幅原因,这里就不分析 appendParameters 方法的源码了,大家请自行分析。