本文基于 Dubbo 2.6.1 版本,望知悉。
1. 概述
本文接 《精尽 Dubbo 源码解析 —— 集群容错(六)之 Configurator 实现》 一文,分享 dubbo-cluster
模块, router
包,实现 Dubbo 的路由规则功能。
Router 相关类,如下图:
老艿艿:本文对应 《Dubbo 用户指南 —— 路由规则》 文档。如果之前没了解过该功能的胖友,请先阅读了解下哈。
2. RouterFactory
com.alibaba.dubbo.rpc.cluster.RouterFactory
,Router 工厂接口。代码如下:
|
@SPI
注解,Dubbo SPI 拓展点,无默认值。@Adaptive("protocol")
注解,基于 Dubbo SPI Adaptive 机制,加载对应的 Router 实现,使用URL.protocol
属性。#getRouter(URL url)
接口方法,获得 Router 对象。
2.1 ConditionRouterFactory
com.alibaba.dubbo.rpc.cluster.router.condition.ConditionRouterFactory
,实现 RouterFactory 接口,ConditionRouter 工厂实现类。代码如下:
public class ConditionRouterFactory implements RouterFactory { |
- 对应 Router 实现类为 ConditionRouter 。
2.2 ScriptRouterFactory
com.alibaba.dubbo.rpc.cluster.router.script.ScriptRouterFactory
,实现 RouterFactory 接口,ScriptRouter 工厂实现类。代码如下:
public class ScriptRouterFactory implements RouterFactory { |
- 对应 Router 实现类为 ScriptRouter 。
2.3 FileRouterFactory
com.alibaba.dubbo.rpc.cluster.router.file.FileRouterFactory
,实现 RouterFactory 接口,基于文件读取路由规则,创建对应的 Router 实现类的对象。代码如下:
1: public class FileRouterFactory implements RouterFactory { |
- 第 20 行:获得
"router"
配置项,默认为"script"
。 - 第 21 至 29 行:获得类型,基于文件后缀。
- 第 31 行:从文件中,读取规则内容。
- 第 33 至 37 行:创建路由规则 URL 对象。
- 第 40 行:通过 Dubbo SPI Adaptive 机制,获得对应的 Router 对象。
3. Router
com.alibaba.dubbo.rpc.cluster.Router
,实现 Comparable 接口,路由规则接口。代码如下:
public interface Router extends Comparable<Router> { |
- 一个 Router 对象,对应一条路由规则。
- Configurator 有优先级的要求,所以实现 Comparable 接口。
#getUrl()
接口方法,获得路由 URL ,里面带有路由规则。#route(List<Invoker<T>> invokers, URL url, Invocation invocation)
接口方法,路由,筛选匹配的 Invoker 集合。
3.1 ConditionRouter
com.alibaba.dubbo.rpc.cluster.router.condition.ConditionRouter
,实现 Router 接口,基于条件表达式的 Router 实现类。
基于条件表达式的路由规则,如:
host = 10.20.153.10 => host = 10.20.153.11
注意,胖友一定要看了 《Dubbo 用户指南 —— 路由规则》 的 条件路由规则 部分,不然下面影响理解。
3.1.1 构造方法
/** |
- 每个字段的解释,胖友自己看下注释。
- MatchPair ,见 「3.1.2 MatchPair」 中。
#parseRule()
方法,见 「3.1.3 parseRule」 中。
3.1.2 MatchPair
MatchPair 为 ConditionRouter 的内部静态类,用于匹配的值组。每个属性条件,例如 method
host
等,对应一个 MatchPair 对象。代码如下:
private static final class MatchPair { |
#isMatch(String value, URL param)
方法,判断value
是否匹配matches
和mismatches
。- 那么为什么会有
param
参数呢?因为要支持$
从 URL 中,读取参数。 #UrlUtils#isMatchGlobPattern(match, value, URL)
方法,支持*
通配,判断match
和value
是否匹配。代码如下:public static boolean isMatchGlobPattern(String pattern, String value, URL param) {
// 以美元符 `$` 开头,表示引用参数
if (param != null && pattern.startsWith("$")) {
pattern = param.getRawParameter(pattern.substring(1));
}
// 匹配
return isMatchGlobPattern(pattern, value);
}
public static boolean isMatchGlobPattern(String pattern, String value) {
// 全匹配
if ("*".equals(pattern)) {
return true;
}
// 全部为空,匹配
if ((pattern == null || pattern.length() == 0) && (value == null || value.length() == 0)) {
return true;
}
// 有一个为空,不匹配
if ((pattern == null || pattern.length() == 0) || (value == null || value.length() == 0)) {
return false;
}
// 支持 * 的通配
int i = pattern.lastIndexOf('*');
// doesn't find "*"
if (i == -1) {
return value.equals(pattern);
}
// "*" is at the end
else if (i == pattern.length() - 1) {
return value.startsWith(pattern.substring(0, i));
}
// "*" is at the beginning
else if (i == 0) {
return value.endsWith(pattern.substring(i + 1));
}
// "*" is in the middle
else {
String prefix = pattern.substring(0, i);
String suffix = pattern.substring(i + 1);
return value.startsWith(prefix) && value.endsWith(suffix);
}
}- x
- 那么为什么会有
😈 代码比较简单,所以胖友自己读下。
3.1.3 parseRule
#parseRule(rule)
方法,解析路由配置内容 "rule"
。代码如下:
1: private static Map<String, MatchPair> parseRule(String rule) throws ParseException { |
第 11 至 14 行:通过
ROUTE_PATTERN
正则匹配rule
,循环多次,直到结束。如下是两个例子:rule: host = 192.168.3.17 & method = say01
host
= 192.168.3.17
& method
= say01
---------- 分割线 ----------
rule: host = 192.168.3.17
host
= 192.168.3.17第 16 至 29 行:处理条件属性的情况,例如:
host
和& method
等等,此时会获得对应的 MatchPair 对象。若不存在,则进行创建 MatchPair 对象。- 第 30 至 45 行:处理条件条件值的情况,例如:
= 192.168.3.17
和!= say01
等等,此时会添加到 MatchPair 的matches
或mismatches
中。- 第 46 至 51 行:处理条件条件值以逗号(
,
)分隔多个值的情况,此时也会添加到 MatchPair 的matches
或mismatches
中。
- 第 46 至 51 行:处理条件条件值以逗号(
- 第 52 至 54 行:非法,抛出 ParseException 异常。
3.1.4 route
1: |
- 第 3 至 6 行:若
invokers
为空,直接返回空 Invoker 集合。 第 8 至 11 行:调用
#matchWhen(url, invocation)
方法,使用服务消费者url
匹配whenCondition
。代码如下:boolean matchWhen(URL url, Invocation invocation) {
return whenCondition == null || whenCondition.isEmpty() || matchCondition(whenCondition, url, null, invocation);
}- 如果匹配条件为空,表示对所有消费方应用,如:
=> host != 10.20.153.11
。 - 若不匹配,则直接返回全
invokers
集合,因为不需要走whenThen
的匹配。 #matchCondition(...)
方法的详细解析,见 「3.1.5 matchCondition」 。
- 如果匹配条件为空,表示对所有消费方应用,如:
- 第 13 至 17 行:若
whenThen
为空,则返回空 Invoker 集合。 第 18 至 23 行:循环调用
#matchThen(url, invocation)
方法,使用服务提供者者invokers
的 URL ,匹配whenThen
集合。代码如下:private boolean matchThen(URL url, URL param) {
return !(thenCondition == null || thenCondition.isEmpty()) && matchCondition(thenCondition, url, param, null);
}- 如果过滤条件为空,表示禁止访问,如:
host = 10.20.153.10 =>
。 - 若匹配,添加到
result
中。
- 如果过滤条件为空,表示禁止访问,如:
- ========== 处理
result
+force
的三种情况 ========== - 第 24 至 26 行:若
result
非空,返回它。 - 第 27 至 31 行:若
result
为空,如果force=true
,代表强制执行,返回空 Invoker 集合。 - 第 36 行:若
result
为空,如果force=false
,代表不强制执行,返回全invokers
集合,即忽略路由规则。
😈 情况比较多,胖友可以回过头在理一理。
3.1.5 matchCondition
private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) { |
3.1.5 compareTo
|
- 优先,按照
"priority"
降序。 - 其次,按照
"url"
升序。
3.2 ScriptRouter
com.alibaba.dubbo.rpc.cluster.router.script.ScriptRouter
,实现 Router 接口,基于脚本的 Router 实现类。
脚本路由规则 4 支持 JDK 脚本引擎的所有脚本,比如:javascript, jruby, groovy 等,通过
type=javascript
参数设置脚本类型,缺省为 javascript。
> "script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("function route(invokers) { ... } (invokers)")
>
基于脚本引擎的路由规则,如:
> function route(invokers) {
> var result = new java.util.ArrayList(invokers.size());
> for (i = 0; i < invokers.size(); i ++) {
> if ("10.20.153.10".equals(invokers.get(i).getUrl().getHost())) {
> result.add(invokers.get(i));
> }
> }
> return result;
> } (invokers); // 表示立即执行方法
>
3.2.1 构造方法
/** |
3.2.2 route
|
- 🙂 比较易懂,胖友自己看代码注释。
3.2.3 compareTo
|
- 优先,按照
"priority"
降序。 - 其次,按照
"rule"
升序。
3.3 MockInvokersSelector
详细解析,见 《精尽 Dubbo 源码解析 —— 集群容错(八)之 Mock 实现》 。
4. 集成 Router 模块
如下图所示,我们可以看到,有二个类,调用 Router#route(List<Invoker<T>>, URL, Invocation)
方法,集成 Router 模块。
4.1 AbstractDirectory
4.1.1 setRouters
#setRouters(List<Router> routers)
方法,设置路由规则们。代码如下:
1: protected void setRouters(List<Router> routers) { |
- 第 3 行:复制重新创建
routers
数组,因为下面会进行修改。 第 5 至 10 行:添加
url
中配置的路由规则到routers
中。例如:<dubbo:registry id="zk01" address="zookeeper://127.0.0.1:2181">
<dubbo:parameter key="router" value="file" />
<dubbo:parameter key="rule" value="/Users/yunai/xxx.js" />
</dubbo:registry>- 受限于 XML 对字符的限制,
"condition"
或"script"
类型的路由配置会比较难设置。所以笔者认为,如果是使用 XML 配置路由规则,"file"
类型是比较合适的方式。当然,如果使用 Java API 又或者注解的方式,应该不存在这样的问题。
- 受限于 XML 对字符的限制,
- 第 12 行:添加 MockInvokersSelector 到
routers
中。 - 第 14 行:排序
routers
。 - 第 16 行:赋值属性给 AbstractDirectory 。
4.1.2 list
1: |
- 第 8 至 20 行:循环,调用
Router#route(invokers, url, invocation)
方法,不断路由,筛选匹配的 Invoker 集合。- 第 13 行:判断
"runtime"
为 true 才执行:是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为true
,需要注意设置会影响调用的性能,可不填,缺省为flase
。
- 第 13 行:判断
4.2 RegistryDirectory
4.2.1 notify
1: |
- 第 12 行:若注册中心通知的
routerUrls
非空,进行处理routerUrls
集合。 第 13 行:调用
#toRouters(routerUrls)
方法,将路由规则 URL 集合,转换成对应的 Router 集合。代码如下:private List<Router> toRouters(List<URL> urls) {
List<Router> routers = new ArrayList<Router>();
if (urls == null || urls.isEmpty()) {
return routers;
}
for (URL url : urls) {
// 忽略,若是 "empty://" 。一般情况下,所有路由规则被删除时,有且仅有一条协议为 "empty://" 的路由规则 URL
if (Constants.EMPTY_PROTOCOL.equals(url.getProtocol())) {
continue;
}
// 获得 "router"
String routerType = url.getParameter(Constants.ROUTER_KEY);
if (routerType != null && routerType.length() > 0) {
url = url.setProtocol(routerType);
}
try {
// 创建 Router 对象
Router router = routerFactory.getRouter(url);
// 添加到返回结果
if (!routers.contains(router)) {
routers.add(router);
}
} catch (Throwable t) {
logger.error("convert router url to router error, url: " + url, t);
}
}
return routers;
}- 代码易懂,胖友看下注释理解。
第 14 至 16 行:
routers
集合非 null( 允许集合大小为 0 ),调用#setRouters(routers)
方法,设置路由规则集合,即 「4.1.1 setRouters」 。
4.2.2 toMethodInvokers
1: private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) { |
第 8 行:调用
#route(invokers, method)
方法,路由全invokersList
,匹配合适的 Invoker 集合进行缓存,这就是上文提到的“只在提供者地址列表变更时预先执行并缓存结果”。代码如下:private List<Invoker<T>> route(List<Invoker<T>> invokers, String method) {
// 创建 Invocation 对象
Invocation invocation = new RpcInvocation(method, new Class<?>[0], new Object[0]);
// 获得 Router 数组
List<Router> routers = getRouters();
// 根据路由规则,筛选 Invoker 集合
if (routers != null) {
for (Router router : routers) {
if (router.getUrl() != null) {
invokers = router.route(invokers, getConsumerUrl(), invocation);
}
}
}
return invokers;
}- 主要是调用
Router#route(...)
方法,路由。
- 主要是调用
第 11 至 20 行:循环,调用
#route(invokers, method)
方法,路由每个方法的methodInvokers
,匹配合适的 Invoker 集合进行缓存。
666. 彩蛋
周日和 didi 面基了一波,很 nice 的人。
明明可以靠脸吃饭的人,为什么还这么有才华~~~