路由的实现
路由接口会根据用户配置的不同路由策略对Invoker列表进行过滤,只返回符合规则的Invoekr。例如:如果用户配置了接口A的所有调用,都是用IP为192.168.1.22的节点,则路由会过滤其他的Invoekr,只返回IP为192.168.1.22的Invoker。
路由的总体结构
路由分为条件路由、文件路由、脚本路由
,对应dubbo-admin中三种不同的规则匹配方法。
- 条件路由是用户使用Dubbo定义的语法规则去写路由规则;
- 文件路由则需要用户提交一个文件,里面写着对应的路由规则,框架基于文件读取对应的规则;
- 脚本路由则是使用JDK自身的脚本引擎解析路由规则脚本,所有JDK脚本引擎支持的脚本都能解析,默认是Javascript。
RouterFactory是一个SPI接口,没有设置默认值,但由于有@Adaptive("protocol")
注解,因此他会根据URL中的protocol参数确定要初始化哪个具体的Router实现。
在SPI的配置文件中可以看到,URL中的protocol可以设置file、script、condition
三种值:
RouterFactory的实现也非常简单,就是直接"new"一个对应的Router并返回。例如:ConditionRouterFactory直接"new"并返回一个ConditionRouter。当然,FileRouterFctory除外,直接在工厂类中实现了所有逻辑。
条件路由的参数规则
条件路由使用的是condition://
协议,URL形式是:
condition://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=URL.encode("host=10.20.153.0=>host=10.20.153.11")
路由规则:
参数名称 | 含义 |
---|
condition:// | 表示路由规则的类型,自持条件路由、脚本路由,可扩展,必填 |
0.0.0.0 | 表示对所有IP地址生效,如果想对某个IP生效,则填入具体IP,必填 |
com.foo.BarService | 表示只对指定服务生效,必填 |
category=routers | 表示该数据为动态配置类型,必填 |
dynamic=fales | 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填 |
enable=true | 覆盖规则是否生效,可不填,默认生效 |
force=false | 当路由结果为空时,是否强制执行,如果不强制执行,则路由结果为空的路由将自动失效,可不填,默认为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
- 这条配置说明所有调用find开头的方法都会被路由到IP为192.168.1.22的服务节点上;
=>
之前的部分为消费者匹配条件,将所有参数和消费者的URL进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则;=>
之后的部分为提供者地址列表的过滤条件,将所有参数和提供者的URL进行对比,消费者最终只获取过滤后的地址列表;- 如果匹配条件为空,则表示应用于所有消费方,如
=> host != 192.168.1.22
; - 如果过滤条件为空,则表示禁止访问,如
host = 192.168.1.22 ->
;
整个规则的表达式支持$protocol
等占位符方式,也支持=、!=
等条件。值可以支持多个,有那个逗号分隔,如host = 192.168.1.22,192.168.1.23
;如果以"*"
号结尾,则说明是通配符,如host = 192.168.1.*
表示撇皮192.168.1.网段下所有IP
。
条件路由的实现
条件路由的具体实现类是ComditionRouter
,Dubbo会根据自定义的规则语法来实现路由规则。我们主要需要关注其构造方法和实现父类接口的route方法。
1. ConditionRouter构造方法的逻辑:
ConditionRouterFactory在初始化ConditionRouter的时候,其构造方法汇总含有规则解析的逻辑。步骤如下:
-
粮据URL的健rule获取对应的规则字符串,以=>为界
,把规则分成两段,前面部分为whenRule,即消费者匹配条件:后面部分为thenRule, 即提供者地址列表的过滤条件。我们以上面的示例规则为例,其会被解析为whenRule method = find*
和thenRule host = 192.168.1.22
。
-
分别解析两个路由规则。调用parseRule 方法
,通过正则表达式不断循环匹配whenRule和thenRule字符串。解析的时候,会根据key-vaue之间的分隔符对key-value做分类(如果A=B,则分隔符为=),支持的分隔符形式有:A=B、 A&B、A!=B、A,B
这4种形式。最终参数都会被封装成一个个MatchPair对象,放入Map中保存。Map的key是参数值,value 是MatchPair对象。若以上面的示例规则为例,则会生成以method为key的whenMap,以host为key的then Map。value 则分别是包装了find*和192. 168.1.22的MatchPair对象。
MatchPair对象是用来做什么的呢?这个对象一共有两个作用。
- 第一个作用是通配符的匹配和占位符的赋值。MatchPair对象是内部类,里面只有一个
isMatch
方法,用于判断值是否能匹配得上规则。规则里的$、*
等通配符都会在MatchPair对象中进行匹配。其中$支持protocol、username、password、 host、 port、 path 这几个动态参数的占位符
。例如:规则中写了S$protocol
,则会自动从URL中获取protocol的值,并赋值进去。 - 第二个作用是缓存规则。MatchPair 对象中有两个Set 集合,一个用于保存匹配的规则,如
=find*
; 另一个则用于保存不匹配的规则,如!=find*
。这两个集合在后续路由规则匹配的时候会使用到。
2. route方法的实现原理:
ConditionRouter继承了Router 接口,需要实现接口的route方法。该方法的主要功能是过滤出符合路由规则的Invoker列表,即做具体的条件匹配判断,其步骤如下:
-
校验。如果规则没有启用,则直接返回;如果传入的Invoker列表为空,则直接返回空:如果没有任何的whenRule匹配,即没有规则匹配,则直接返回传入的Invoker列表:如果whenRule有匹配的,但是thenRule 为空,即没有匹配上规则的Invoke,则返回空。
-
遍历Invoker列表,通过heRule找出所有符合规则的Invoker加入集合。例如:匹配规则中的method名称和当前URL中的method是不是相等。
-
返回结果。如果结果集不为空,则直接返回:如果结果集为空,但是规则配置了frcerue即强制过滤,那么就会返回空结果集:非强制则不过滤,即返回所有lnoer列表。具体的逻辑还,是比较简单的,但代码中的if判断会比较多。
文件路由的实现
文件路由是把规则写在文件中,文件中写的是自定义的脚本规则,可以是Javascript、Groovy等,URL中对应的key值填写的是文件的路径。文件路由主要做的就是把文件中的路由脚本读出来,然后调用路由的工厂去匹配对应的脚本路由做解析。
脚本路由的实现
脚本路由使用JDK自带的脚本解析器解析脚本并运行,默认使用Javascript解析器,其逻辑分为构造方法和route方法两大部分。构造方法主要负责一些初始化的工作,route方法则是具体的过滤逻辑执行的地方。
脚本示例:
function route(invokers) {var result &#61; new java.util.ArrayList(invokers.size());for(i &#61; 0; i < invokers.size(); i&#43;&#43;) {if("10.20.153.10".equals(invokers.get(i).getUrl().getHost())){result.add(invokers.get(i));}}return result;
}(invokers);
在写Javascript脚本的时候需要注意&#xff0c;一个服务只能有一条规则&#xff0c;如果有多条规则&#xff0c;并且规则之间没有交集&#xff0c;则会把所有的Invoker都过滤。另外&#xff0c;脚本路由中也没看到沙箱约束&#xff0c;因此会有注入的风险。
脚本路由的构造方法逻辑&#xff1a;
- 初始化参数。获取规则的脚本类型、路由优先级。如果没有设置脚本类型&#xff0c;则默认设置为Javascript类型&#xff0c;如果没有解析到任何规则&#xff0c;则抛出异常。
- 初始化脚本执行引擎。根据脚本的类型&#xff0c;通过Java的ScriptEngineManager创建不同的脚本执行器&#xff0c;并缓存起来。
route方法的核心逻辑就是调用脚本引擎&#xff0c;获取执行结果并返回。主要是JDK脚本引擎的调用&#xff0c;不会涉及具体的过滤逻辑。