Dubbo进阶(十四):Router的实现
首先我们依然再看一遍Dubbo调用的流程
Router是什么
Router
是一种约定的规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。比如服务A配置了调用的服务的IP是192.168.1.1
,那么路由会过滤到除192.168.1.1
之外的所有的服务,只会返回192.168.1.1
。
通常服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者。
在Dubbo 2.7.x的版本中,路由包含了条件路由、脚本路由、文件路由和标签路由四种路由规则。
本文以ConditionRouter条件路由
为例,分析一下对应的规则和源码。
ConditionRouter条件路由的规则
参数名称 | 含义 |
---|---|
condition:// | 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。 |
0.0.0.0 | 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填。 |
com.foo.BarService | 表示只对指定服务生效,必填。 |
category=routers | 表示该数据为动态配置类型,必填。 |
dynamic=false | 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。 |
enabled=true | 覆盖规则是否生效,可不填,缺省生效。 |
force=false | 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 flase。 |
runtime=false | 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 flase。 |
priority=1 | 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。 |
rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11") | 表示路由规则的内容,必填。 |
以上就是ConditionRouter条件路由的一些规则。
比如有这样一条规则:host = 10.20.153.10 => host = 10.20.153.11
,该条规则表示 IP 为 10.20.153.10
的服务消费者只可调用 IP 为 10.20.153.11
机器上的服务,不可调用其他机器上的服务。条件路由规则的格式如下:[服务消费者匹配条件] => [服务提供者匹配条件]
=>
之前的部分为消费者匹配条件,向所有的参数和消费者的URL进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。=>
之后的部分为提供者地址列表的过滤条件,将所有参数和URL进行对比,消费者最终只会去过滤后的地址列表。- 如果匹配条件为空,则表示应用于所有消费方,如
=> host ! = 10.20.153.11
。 - 如果过滤条件为空,则表示禁止访问,如
host = 10.20.153.11 =>
。
接下来分析一下表达式解析的源码
这里是一个工厂模式,通过 routerFactory.getRouter(url)
会创建一个 Router 对象。
public class ConditionRouterFactory implements RouterFactory {public static final String NAME = "condition";
@Override
public Router getRouter(URL url) {
return new ConditionRouter(url);
}
}
然后看下ConditionRouter
的构造函数
public ConditionRouter(URL url) {this.url = url;
// 获取 priority、force和enabled配置
this.priority = url.getParameter(PRIORITY_KEY, 0);
this.force = url.getParameter(FORCE_KEY, false);
this.enabled = url.getParameter(ENABLED_KEY, true);
// 获取路由规则
init(url.getParameterAndDecoded(RULE_KEY));
}
public void init(String rule) {
try {
if (rule == null || rule.trim().length() == 0) {
throw new IllegalArgumentException("Illegal route rule!");
}
rule = rule.replace("consumer.", "").replace("provider.", "");
// 定位 =>
int i = rule.indexOf("=>");
// 分别获取服务消费者和提供者匹配规则
String whenRule = i < 0 ? null : rule.substring(0, i).trim();
String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
// 解析服务消费者匹配规则
Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<String, MatchPair>() : parseRule(whenRule);
// 解析服务提供者匹配规则
Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ? null : parseRule(thenRule);
// NOTE: It should be determined on the business level whether the `When condition` can be empty or not.
this.whenCondition = when;
this.thenCondition = then;
} catch (ParseException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
重点的逻辑都在parseRule(String rule)
方法中,源码如下(来自官网):
private static Map<String, MatchPair> parseRule(String rule)throws ParseException {
// 定义条件映射集合
Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
if (StringUtils.isBlank(rule)) {
return condition;
}
MatchPair pair = null;
Set<String> values = null;
// 通过正则表达式匹配路由规则,ROUTE_PATTERN = ([&!=,]*)s*([^&!=,s]+)
// 这个表达式看起来不是很好理解,第一个括号内的表达式用于匹配"&", "!", "=" 和 "," 等符号。
// 第二括号内的用于匹配英文字母,数字等字符。举个例子说明一下:
// host = 2.2.2.2 & host != 1.1.1.1 & method = hello
// 匹配结果如下:
// 括号一 括号二
// 1. null host
// 2. = 2.2.2.2
// 3. & host
// 4. != 1.1.1.1
// 5. & method
// 6. = hello
final Matcher matcher = ROUTE_PATTERN.matcher(rule);
while (matcher.find()) {
// 获取括号一内的匹配结果
String separator = matcher.group(1);
// 获取括号二内的匹配结果
String content = matcher.group(2);
// 分隔符为空,表示匹配的是表达式的开始部分
if (separator == null || separator.length() == 0) {
// 创建 MatchPair 对象
pair = new MatchPair();
// 存储 <匹配项, MatchPair> 键值对,比如 <host, MatchPair>
condition.put(content, pair);
}
// 如果分隔符为 &,表明接下来也是一个条件
elseif ("&".equals(separator)) {
// 尝试从 condition 获取 MatchPair
if (condition.get(content) == null) {
// 未获取到 MatchPair,重新创建一个,并放入 condition 中
pair = new MatchPair();
condition.put(content, pair);
} else {
pair = condition.get(content);
}
}
// 分隔符为 =
elseif ("=".equals(separator)) {
if (pair == null)
throw new ParseException("Illegal route rule ...");
values = pair.matches;
// 将 content 存入到 MatchPair 的 matches 集合中
values.add(content);
}
// 分隔符为 !=
elseif ("!=".equals(separator)) {
if (pair == null)
throw new ParseException("Illegal route rule ...");
values = pair.mismatches;
// 将 content 存入到 MatchPair 的 mismatches 集合中
values.add(content);
}
// 分隔符为 ,
elseif (",".equals(separator)) {
if (values == null || values.isEmpty())
throw new ParseException("Illegal route rule ...");
// 将 content 存入到上一步获取到的 values 中,可能是 matches,也可能是 mismatches
values.add(content);
} else {
throw new ParseException("Illegal route rule ...");
}
}
return condition;
}
通过源码可以总结得出parseRule
由正则表达式和一个 while 循环以及数个条件分支组成。
服务路由的源码
服务路由的入口方法是 ConditionRouter 的 router 方法,该方法定义在 Router 接口中。
@Overridepublic <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
if (!enabled) {
return invokers;
}
if (CollectionUtils.isEmpty(invokers)) {
return invokers;
}
try {
// 对服务消费者进行匹配,如果匹配失败,直接返回 Invoker 列表
if (!matchWhen(url, invocation)) {
return invokers;
}
List<Invoker<T>> result = new ArrayList<Invoker<T>>();
if (thenCondition == null) {
logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
return result;
}
// 如果匹配成功,再对服务提供者进行匹配
for (Invoker<T> invoker : invokers) {
if (matchThen(invoker.getUrl(), url)) {
result.add(invoker);
}
}
if (!result.isEmpty()) {
return result;
} elseif (force) {
logger.warn("The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(RULE_KEY));
return result;
}
} catch (Throwable t) {
logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);
}
return invokers;
}
router 方法先是调用 matchWhen 对服务消费者进行匹配,如果匹配失败,直接返回 Invoker 列表。如果匹配成功,再对服务提供者进行匹配,匹配逻辑封装在了 matchThen 方法中。
boolean matchWhen(URL url, Invocation invocation) {// 服务消费者条件为 null 或空,均返回 true,比如:
// => host != 172.22.3.91
// 表示所有的服务消费者都不得调用 IP 为 172.22.3.91 的机器上的服务
return whenCondition == null || whenCondition.isEmpty()
|| matchCondition(whenCondition, url, null, invocation); // 进行条件匹配
}
private boolean matchThen(URL url, URL param) {
// 服务提供者条件为 null 或空,表示禁用服务
return !(thenCondition == null || thenCondition.isEmpty())
&& matchCondition(thenCondition, url, param, null); // 进行条件匹配
}
再配合之前Directory
的代码分析可以得出
- 监听注册中心节点的变化,如果有节点新增或者删除,就会触发 notify 方法来更新;
- 当获取了当前服务的 Invoker 列表后,如果配置了路由规则,则使用该路由规则过滤 Invoker 列表。
以上是 Dubbo进阶(十四):Router的实现 的全部内容, 来源链接: utcz.com/a/25356.html