条件路由规则由两个条件组成,分别用于对服务消费者和提供者进行匹配。比如有这样一条规则:

该条规则表示 IP 为 10.20.153.10 的服务消费者只可调用 IP 为 10.20.153.11 机器上的服务,不可调用其他机器上的服务。条件路由规则的格式如下:

[服务消费者匹配条件] => [服务提供者匹配条件]

如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。官方文档中对条件路由进行了比较详细的介绍,大家可以参考下,这里就不过多说明了。

条件路由实现类 ConditionRouter 在进行工作前,需要先对用户配置的路由规则进行解析,得到一系列的条件。然后再根据这些条件对服务进行路由。本章将分两节进行说明,2.1节介绍表达式解析过程。2.2 节介绍服务路由的过程。下面,我们先从表达式解析过程看起。

条件路由规则是一条字符串,对于 Dubbo 来说,它并不能直接理解字符串的意思,需要将其解析成内部格式才行。条件表达式的解析过程始于 ConditionRouter 的构造方法,下面一起看一下:

如上,ConditionRouter 构造方法先是对路由规则做预处理,然后调用 parseRule 方法分别对服务提供者和消费者规则进行解析,最后将解析结果赋值给 whenCondition 和 thenCondition 成员变量。ConditionRouter 构造方法不是很复杂,这里就不多说了。下面我们把重点放在 parseRule 方法上,在详细介绍这个方法之前,我们先来看一个内部类。

  1. private static final class MatchPair {
  2. final Set<String> matches = new HashSet<String>();
  3. final Set<String> mismatches = new HashSet<String>();
  4. }

MatchPair 内部包含了两个 Set 类型的成员变量,分别用于存放匹配和不匹配的条件。这个类两个成员变量会在 parseRule 方法中被用到,下面来看一下。

  1. private static Map<String, MatchPair> parseRule(String rule)
  2. throws ParseException {
  3. // 定义条件映射集合
  4. Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
  5. if (StringUtils.isBlank(rule)) {
  6. return condition;
  7. }
  8. MatchPair pair = null;
  9. Set<String> values = null;
  10. // 通过正则表达式匹配路由规则,ROUTE_PATTERN = ([&!=,]*)\s*([^&!=,\s]+)
  11. // 这个表达式看起来不是很好理解,第一个括号内的表达式用于匹配"&", "!", "=" 和 "," 等符号。
  12. // 第二括号内的用于匹配英文字母,数字等字符。举个例子说明一下:
  13. // host = 2.2.2.2 & host != 1.1.1.1 & method = hello
  14. // 匹配结果如下:
  15. // 括号一 括号二
  16. // 1. null host
  17. // 2. = 2.2.2.2
  18. // 3. & host
  19. // 4. != 1.1.1.1
  20. // 5. & method
  21. // 6. = hello
  22. final Matcher matcher = ROUTE_PATTERN.matcher(rule);
  23. while (matcher.find()) {
  24. // 获取括号一内的匹配结果
  25. String separator = matcher.group(1);
  26. // 获取括号二内的匹配结果
  27. String content = matcher.group(2);
  28. // 分隔符为空,表示匹配的是表达式的开始部分
  29. if (separator == null || separator.length() == 0) {
  30. // 创建 MatchPair 对象
  31. pair = new MatchPair();
  32. // 存储 <匹配项, MatchPair> 键值对,比如 <host, MatchPair>
  33. condition.put(content, pair);
  34. }
  35. // 如果分隔符为 &,表明接下来也是一个条件
  36. else if ("&".equals(separator)) {
  37. // 尝试从 condition 获取 MatchPair
  38. if (condition.get(content) == null) {
  39. // 未获取到 MatchPair,重新创建一个,并放入 condition 中
  40. pair = new MatchPair();
  41. condition.put(content, pair);
  42. } else {
  43. pair = condition.get(content);
  44. }
  45. }
  46. // 分隔符为 =
  47. else if ("=".equals(separator)) {
  48. if (pair == null)
  49. throw new ParseException("Illegal route rule ...");
  50. values = pair.matches;
  51. // 将 content 存入到 MatchPair 的 matches 集合中
  52. values.add(content);
  53. }
  54. // 分隔符为 !=
  55. else if ("!=".equals(separator)) {
  56. if (pair == null)
  57. throw new ParseException("Illegal route rule ...");
  58. values = pair.mismatches;
  59. // 将 content 存入到 MatchPair 的 mismatches 集合中
  60. values.add(content);
  61. }
  62. // 分隔符为 ,
  63. else if (",".equals(separator)) {
  64. if (values == null || values.isEmpty())
  65. throw new ParseException("Illegal route rule ...");
  66. // 将 content 存入到上一步获取到的 values 中,可能是 matches,也可能是 mismatches
  67. values.add(content);
  68. } else {
  69. throw new ParseException("Illegal route rule ...");
  70. }
  71. }

现在线程进入 while 循环:

第一次循环:分隔符 separator = null,content = "host"。此时创建 MatchPair 对象,并存入到 condition 中,condition = {"host": MatchPair@123}

第二次循环:分隔符 separator = "=",content = "2.2.2.2",pair = MatchPair@123。此时将 2.2.2.2 放入到 MatchPair@123 对象的 matches 集合中。

第三次循环:分隔符 separator = "&",content = "host"。host 已存在于 condition 中,因此 pair = MatchPair@123。

第四次循环:分隔符 separator = "!=",content = "1.1.1.1",pair = MatchPair@123。此时将 1.1.1.1 放入到 MatchPair@123 对象的 mismatches 集合中。

第五次循环:分隔符 separator = "&",content = "method"。condition.get("method") = null,因此新建一个 MatchPair 对象,并放入到 condition 中。此时 condition = {"host": MatchPair@123, "method": MatchPair@ 456}

第六次循环:分隔符 separator = "=",content = "2.2.2.2",pair = MatchPair@456。此时将 hello 放入到 MatchPair@456 对象的 matches 集合中。

循环结束,此时 condition 的内容如下:

  1. {
  2. "host": {
  3. "matches": ["2.2.2.2"],
  4. "mismatches": ["1.1.1.1"]
  5. },
  6. "method": {
  7. "matches": ["hello"],
  8. "mismatches": []
  9. }
  10. }

路由规则的解析过程稍微有点复杂,大家可通过 ConditionRouter 的测试类对该逻辑进行测试。并且找一个表达式,对照上面的代码走一遍,加深理解。

2.2 服务路由

  1. public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
  2. if (invokers == null || invokers.isEmpty()) {
  3. return invokers;
  4. }
  5. try {
  6. // 先对服务消费者条件进行匹配,如果匹配失败,表明服务消费者 url 不符合匹配规则,
  7. // 无需进行后续匹配,直接返回 Invoker 列表即可。比如下面的规则:
  8. // host = 10.20.153.10 => host = 10.0.0.10
  9. // 这条路由规则希望 IP 为 10.20.153.10 的服务消费者调用 IP 为 10.0.0.10 机器上的服务。
  10. // 当消费者 ip 为 10.20.153.11 时,matchWhen 返回 false,表明当前这条路由规则不适用于
  11. // 当前的服务消费者,此时无需再进行后续匹配,直接返回即可。
  12. if (!matchWhen(url, invocation)) {
  13. return invokers;
  14. }
  15. List<Invoker<T>> result = new ArrayList<Invoker<T>>();
  16. // 服务提供者匹配条件未配置,表明对指定的服务消费者禁用服务,也就是服务消费者在黑名单中
  17. if (thenCondition == null) {
  18. logger.warn("The current consumer in the service blacklist...");
  19. return result;
  20. }
  21. // 这里可以简单的把 Invoker 理解为服务提供者,现在使用服务提供者匹配规则对
  22. // Invoker 列表进行匹配
  23. for (Invoker<T> invoker : invokers) {
  24. // 若匹配成功,表明当前 Invoker 符合服务提供者匹配规则。
  25. // 此时将 Invoker 添加到 result 列表中
  26. if (matchThen(invoker.getUrl(), url)) {
  27. result.add(invoker);
  28. }
  29. }
  30. // 返回匹配结果,如果 result 为空列表,且 force = true,表示强制返回空列表,
  31. // 否则路由结果为空的路由规则将自动失效
  32. if (!result.isEmpty()) {
  33. return result;
  34. } else if (force) {
  35. logger.warn("The route result is empty and force execute ...");
  36. return result;
  37. }
  38. } catch (Throwable t) {
  39. logger.error("Failed to execute condition router rule: ...");
  40. }
  41. // 原样返回,此时 force = false,表示该条路由规则失效
  42. return invokers;
  43. }

router 方法先是调用 matchWhen 对服务消费者进行匹配,如果匹配失败,直接返回 Invoker 列表。如果匹配成功,再对服务提供者进行匹配,匹配逻辑封装在了 matchThen 方法中。下面来看一下这两个方法的逻辑:

这两个方法长的有点像,不过逻辑上还是有差别的,大家注意看。这两个方法均调用了 matchCondition 方法,但它们所传入的参数是不同的。这个需要特别注意一下,不然后面的逻辑不好弄懂。下面我们对这几个参数进行溯源。matchWhen 方法向 matchCondition 方法传入的参数为 [whenCondition, url, null, invocation],第一个参数 whenCondition 为服务消费者匹配条件,这个前面分析过。第二个参数 url 源自 route 方法的参数列表,该参数由外部类调用 route 方法时传入。比如:

  1. private List<Invoker<T>> route(List<Invoker<T>> invokers, String method) {
  2. Invocation invocation = new RpcInvocation(method, new Class<?>[0], new Object[0]);
  3. List<Router> routers = getRouters();
  4. if (routers != null) {
  5. for (Router router : routers) {
  6. if (router.getUrl() != null) {
  7. // 注意第二个参数
  8. invokers = router.route(invokers, getConsumerUrl(), invocation);
  9. }
  10. }
  11. }
  12. return invokers;
  13. }

上面这段代码来自 RegistryDirectory,第二个参数表示的是服务消费者 url。matchCondition 的 invocation 参数也是从这里传入的。

接下来再来看看 matchThen 向 matchCondition 方法传入的参数 [thenCondition, url, param, null]。第一个参数不用解释了。第二个和第三个参数来自 matchThen 方法的参数列表,这两个参数分别为服务提供者 url 和服务消费者 url。搞清楚这些参数来源后,接下来就可以分析 matchCondition 方法了。

  1. private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) {
  2. // 将服务提供者或消费者 url 转成 Map
  3. Map<String, String> sample = url.toMap();
  4. boolean result = false;
  5. // 遍历 condition 列表
  6. for (Map.Entry<String, MatchPair> matchPair : condition.entrySet()) {
  7. // 获取匹配项名称,比如 host、method 等
  8. String key = matchPair.getKey();
  9. String sampleValue;
  10. // 如果 invocation 不为空,且 key 为 mehtod(s),表示进行方法匹配
  11. if (invocation != null && (Constants.METHOD_KEY.equals(key) || Constants.METHODS_KEY.equals(key))) {
  12. // 从 invocation 获取被调用方法的名称
  13. } else {
  14. // 从服务提供者或消费者 url 中获取指定字段值,比如 host、application 等
  15. sampleValue = sample.get(key);
  16. if (sampleValue == null) {
  17. // 尝试通过 default.xxx 获取相应的值
  18. sampleValue = sample.get(Constants.DEFAULT_KEY_PREFIX + key);
  19. }
  20. }
  21. // --------------------✨ 分割线 ✨-------------------- //
  22. if (sampleValue != null) {
  23. // 调用 MatchPair 的 isMatch 方法进行匹配
  24. if (!matchPair.getValue().isMatch(sampleValue, param)) {
  25. // 只要有一个规则匹配失败,立即返回 false 结束方法逻辑
  26. return false;
  27. } else {
  28. result = true;
  29. }
  30. } else {
  31. // sampleValue 为空,表明服务提供者或消费者 url 中不包含相关字段。此时如果
  32. // MatchPair 的 matches 不为空,表示匹配失败,返回 false。比如我们有这样
  33. // 一条匹配条件 loadbalance = random,假设 url 中并不包含 loadbalance 参数,
  34. // 此时 sampleValue = null。既然路由规则里限制了 loadbalance 必须为 random,
  35. // 但 sampleValue = null,明显不符合规则,因此返回 false
  36. if (!matchPair.getValue().matches.isEmpty()) {
  37. return false;
  38. } else {
  39. result = true;
  40. }
  41. }
  42. }
  43. return result;
  44. }

如上,matchCondition 方法看起来有点复杂,这里简单说明一下。分割线以上的代码实际上用于获取 sampleValue 的值,分割线以下才是进行条件匹配。条件匹配调用的逻辑封装在 isMatch 中,代码如下:

isMatch 方法逻辑比较清晰,由三个条件分支组成,用于处理四种情况。这里对四种情况下的匹配逻辑进行简单的总结,如下:

isMatch 方法是通过 UrlUtils 的 isMatchGlobPattern 方法进行匹配,因此下面我们再来看看 isMatchGlobPattern 方法的逻辑。

  1. public static boolean isMatchGlobPattern(String pattern, String value, URL param) {
  2. if (param != null && pattern.startsWith("$")) {
  3. // 引用服务消费者参数,param 参数为服务消费者 url
  4. pattern = param.getRawParameter(pattern.substring(1));
  5. }
  6. // 调用重载方法继续比较
  7. return isMatchGlobPattern(pattern, value);
  8. }
  9. public static boolean isMatchGlobPattern(String pattern, String value) {
  10. // 对 * 通配符提供支持
  11. if ("*".equals(pattern))
  12. // 匹配规则为通配符 *,直接返回 true 即可
  13. return true;
  14. if ((pattern == null || pattern.length() == 0)
  15. && (value == null || value.length() == 0))
  16. // pattern 和 value 均为空,此时可认为两者相等,返回 true
  17. return true;
  18. if ((pattern == null || pattern.length() == 0)
  19. || (value == null || value.length() == 0))
  20. // pattern 和 value 其中有一个为空,表明两者不相等,返回 false
  21. return false;
  22. // 定位 * 通配符位置
  23. int i = pattern.lastIndexOf('*');
  24. if (i == -1) {
  25. // 匹配规则中不包含通配符,此时直接比较 value 和 pattern 是否相等即可,并返回比较结果
  26. return value.equals(pattern);
  27. }
  28. // 通配符 "*" 在匹配规则尾部,比如 10.0.21.*
  29. else if (i == pattern.length() - 1) {
  30. // 检测 value 是否以“不含通配符的匹配规则”开头,并返回结果。比如:
  31. // pattern = 10.0.21.*,value = 10.0.21.12,此时返回 true
  32. return value.startsWith(pattern.substring(0, i));
  33. }
  34. // 通配符 "*" 在匹配规则头部
  35. else if (i == 0) {
  36. // 检测 value 是否以“不含通配符的匹配规则”结尾,并返回结果
  37. return value.endsWith(pattern.substring(i + 1));
  38. }
  39. // 通配符 "*" 在匹配规则中间位置
  40. else {
  41. // 通过通配符将 pattern 分成两半,得到 prefix 和 suffix
  42. String prefix = pattern.substring(0, i);
  43. String suffix = pattern.substring(i + 1);
  44. // 检测 value 是否以 prefix 开头,且以 suffix 结尾,并返回结果
  45. return value.startsWith(prefix) && value.endsWith(suffix);
  46. }

以上就是 isMatchGlobPattern 两个重载方法的全部逻辑,这两个方法分别对普通的匹配过程,以及”引用消费者参数“和通配符匹配等特性提供了支持。这两个方法的逻辑不是很复杂,且代码中也进行了比较详细的注释,因此就不多说了。

本篇文章对条件路由的表达式解析和服务路由过程进行了较为细致的分析。总的来说,条件路由的代码还是有一些复杂的,需要静下心来看。在阅读条件路由代码的过程中,要多调试。一般的框架都会有单元测试,Dubbo 也不例外,因此大家可以直接通过 ConditionRouterTest 对条件路由进行调试,无需重头构建测试用例。