文章目录
Dubbo路由实现原理
关注可以查看更多粉丝专享blog~
Dubbo的路由分为条件路由、文件路由和脚本路由,对应的dubbo-admin中三种不同的规则配置方式。条件路由是用户使用Dubbo定义的语法规则去写的路由规则;文件路由则需要用户提交一个文件,里面写着对应的路由规则,框架基于文件读取对应的规则;脚本路由则是使用JDK自身的脚本引擎解析路由规则脚本,所有JDK脚本引擎支持的脚本都能解析,默认是JavaScript。
ConditionRouter(条件路由)
参数规则
condition://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&enabled=true&force=true&runtime=false&priority=1&rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11")
参数名称 | 含义 |
---|---|
condition:// | (必填)表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展 |
0.0.0.0 | (必填)表示对所有ip生效,如果只想对某个IP生效,则填入具体IP |
com.foo.BarService | (必填)表示只对指定服务生效 |
category=routers | (必填)表示该数据为动态配置类型 |
dynamic=false | (必填)表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心 |
enabled=true | (非必填)覆盖规则是否生效,默认true |
force=true | (非必填) 当路由规则为空时,是否强制执行,如果不强制执行,则路由结果为空的的路由规则将自动失效,默认false |
runtime=false | (非必填)是否在每次调用时都执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,则必须设置为true,需要注意设置会影响调用的性能,默认为false |
priority=1 | (非必填)路由规则优先级,用于排序,优先级越大越靠前,默认为0 |
rule= + URL.encode(“host = 10.20.153.10 => host = 10.20.153.11”) | (必填)表示路由规则的内容 |
示例:
method = find* => host = 192.168.1.22,192.168.1.23
上面的路由规则表示,调用方法以find开头的路由到192.168.1.22,192.168.1.23,此时在调用的时候获取到的原始Invoker(RegisterDirectory或者StaticDirectory中的原始InvokerList)会针对route规则进行过滤。
流程解析
- 校验:如果规则没有启用,则直接返回;如果传入的Invoker为空,则直接返回;如果没有任何whenRule匹配,即没有匹配规则,则直接发怒会传入的Invoker列表;如果whenRule有匹配,但是thenRule为空,即没有匹配上规则的Invoker,则返回空
- 匹配:遍历Invoker列表,通过thenRule找出所有符合规则的Invoker介入result集合。
- 返回:如果result不为空则直接返回result结果集;如果结果集为空,则查看force(强制执行)是否为true,如果为true,则表示强制执行,此时打印warn日志,并返回空结果集,否则返回所有的Invoker列表。
源码解析
// ConditionRouter#route
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
// invoker列表为空直接return
if (invokers == null || invokers.isEmpty()) {
return invokers;
}
try {
// 匹配是否符合条件,不匹配直接return所有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;
} else if (force) {
// 如果为空且设置了强制执行则打印日志,并返回空结果集
logger.warn("The route result is empty and force execute. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(Constants.RULE_KEY));
return result;
}
} catch (Throwable t) {
logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);
}
return invokers;
}
FileRouter(文件路由)
流程解析
文件路由是把规则写在文件中,文件中写的是自定义脚本规则,可以是JavaScript、Groovy等,URL中的key值填写的是文件路径。文件路由主要做的就是把文件中的路由脚本读出来,然后调用路由的工厂去匹配对应的 脚本路由 做解析。
源码解析
// FileRouterFactory#getRouter
// file:///d:/path/to/route.js?router=script ==> script:///d:/path/to/route.js?type=js&rule=<file-content>
@Override
public Router getRouter(URL url) {
try {
// 将文件URL转换为脚本路由URL,并加载
// 将原来的协议(可能是“file”)替换为“script”
String protocol = url.getParameter(Constants.ROUTER_KEY, ScriptRouterFactory.NAME);
// 使用文件后缀配置脚本类型,如js, groovy…
String type = null;
String path = url.getPath();
if (path != null) {
int i = path.lastIndexOf('.');
if (i > 0) {
type = path.substring(i + 1);
}
}
// 读取文件
String rule = IOUtils.read(new FileReader(new File(url.getAbsolutePath())));
// 读取是否是运行时的参数
boolean runtime = url.getParameter(Constants.RUNTIME_KEY, false);
// 生成路由工厂可以识别的URL,并把参数添加进去
URL script = url.setProtocol(protocol).addParameter(Constants.TYPE_KEY, type).addParameter(Constants.RUNTIME_KEY, runtime).addParameterAndEncoded(Constants.RULE_KEY, rule);
// 再次调用路由的工厂,由于前面配置了protocol为script类型,这里会使用脚本路由进行解析
return routerFactory.getRouter(script);
} catch (IOException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
ScriptRouter(脚本路由)
流程解析
脚本路由使用JDK自带的脚本解析器解析脚本并运行,默认使用JavaScript解析器,其逻辑分为构造方法和route方法两大部分。
源码解析
// 以官方文档中的JavaScript脚本为例
function route(invokers) {
// 创建一个list
var result = new java.util.ArrayList(invokers.size());
// 遍历传入的所有Invoker,过滤所有IP不是10.20.153.10的Invoker
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); // 表示立即执行方法
注意事项:
我们在写JavaScript脚本的时候需要注意,一个服务只能有一条规则,如果有多条规则,并且规则之间没有交集,则会把所有的Invoker都过滤。另外,脚本路由中没有看到沙箱约束,因此会由注入的风险。
- 构造方法主要负责一些初始化工作。
- 初始化参数。获取规则的脚本类型、路由优先级。如果没有设置脚本,则默认设置为JavaScript类型,如果没有解析到任何规则,则抛出异常。
- 初始化脚本执行引擎。根据脚本的类型,通过Java的ScriptEngineManager创建不同的脚本执行器并缓存起来。
// ScriptRouter#constructor
public ScriptRouter(URL url) {
// 初始化参数。获取规则的脚本类型、路由优先级
this.url = url;
String type = url.getParameter(Constants.TYPE_KEY);
this.priority = url.getParameter(Constants.PRIORITY_KEY, 0);
String rule = url.getParameterAndDecoded(Constants.RULE_KEY);
// 如果没有设置脚本,则默认设置为JavaScript类型
if (type == null || type.length() == 0) {
type = Constants.DEFAULT_SCRIPT_TYPE_KEY;
}
// 如果没有解析到任何规则,则抛出异常
if (rule == null || rule.length() == 0) {
throw new IllegalStateException(new IllegalStateException("route rule can not be empty. rule:" + rule));
}
// 初始化脚本执行引擎
ScriptEngine engine = engines.get(type);
if (engine == null) {
// 根据脚本的类型,通过Java的ScriptEngineManager创建不同的脚本执行器
engine = new ScriptEngineManager().getEngineByName(type);
if (engine == null) {
throw new IllegalStateException(new IllegalStateException("Unsupported route rule type: " + type + ", rule: " + rule));
}
// 缓存脚本执行器
engines.put(type, engine);
}
this.engine = engine;
this.rule = rule;
}
- route方法则负责具体的过滤逻辑执行。
route方法的核心逻辑就是调用脚本引擎,获取执行结果并返回。主要是JDK脚本引擎相关知识,不会涉及具体的过滤逻辑,因为逻辑已经下沉到用户自定义的脚本里面了。
@Override
@SuppressWarnings("unchecked")
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
try {
List<Invoker<T>> invokersCopy = new ArrayList<Invoker<T>>(invokers);
Compilable compilable = (Compilable) engine;
// 构造要传入的脚本参数
Bindings bindings = engine.createBindings();
bindings.put("invokers", invokersCopy);
bindings.put("invocation", invocation);
bindings.put("context", RpcContext.getContext());
CompiledScript function = compilable.compile(rule);
// 执行脚本
Object obj = function.eval(bindings);
if (obj instanceof Invoker[]) {
invokersCopy = Arrays.asList((Invoker<T>[]) obj);
} else if (obj instanceof Object[]) {
invokersCopy = new ArrayList<Invoker<T>>();
for (Object inv : (Object[]) obj) {
invokersCopy.add((Invoker<T>) inv);
}
} else {
invokersCopy = (List<Invoker<T>>) obj;
}
return invokersCopy;
} catch (ScriptException e) {
// 如果失败,则忽略规则。返回invokers列表
logger.error("route error , rule has been ignored. rule: " + rule + ", method:" + invocation.getMethodName() + ", url: " + RpcContext.getContext().getUrl(), e);
return invokers;
}
}
总结
Dubbo的路由规则本质上只有条件路由和脚本路由,文件路由内部其实是读取的文件中的脚本,最终调用脚本路由的路由方法,相对而言,路由规则比较容易理解。
相关文章:
Davids原理探究:Dubbo源码编译(2.7.8)
Davids原理探究:Dubbo SPI和Java SPI实现原理
Davids原理探究:Dubbo注册中心(ZooKeeper、Redis)实现原理
Davids原理探究:Dubbo配置解析原理
Davids原理探究:Dubbo服务暴露原理
Davids原理探究:Dubbo服务消费原理
Davids原理探究:Dubbo优雅停机原理解析
Davids原理探究:Dubbo调用流程图
Davids原理探究:Dubbo路由实现原理
Davids原理探究:Dubbo负载均衡实现原理
Davids原理探究:Dubbo过滤器原理