Dubbo——路由机制(上)

前言

Dubbo 路由机制是在服务间的调用时,通过将服务提供者按照设定的路由规则来决定调用哪一个具体的服务。

Router 的主要功能就是根据用户配置的路由规则以及请求携带的信息,过滤出符合条件的 Invoker 集合,供后续负载均衡逻辑使用。在介绍 RegistryDirectory 实现的时候,我们就已经看到了 RouterChain 这个 Router 链的存在,但是没有深入分析,下面我们就来深入 Router 进行分析。

RouterChain、RouterFactory 与 Router

首先我们来看 RouterChain 的核心字段:

  • invokers(List<Invoker<T>> 类型):当前 RouterChain 对象要过滤的 Invoker 集合。我们可以看到,在 StaticDirectory 中是通过 RouterChain.setInvokers() 方法进行设置的。

  • builtinRouters(List<Router> 类型):当前 RouterChain 激活的内置 Router 集合。

  • routers(List<Router> 类型):当前 RouterChain 中真正要使用的 Router 集合,其中不仅包括了上面 builtinRouters 集合中全部的 Router 对象,还包括通过 addRouters() 方法添加的 Router 对象。

在 RouterChain 的构造函数中,会在传入的 URL 参数中查找 router 参数值,并根据该值获取确定激活的 RouterFactory,之后通过 Dubbo SPI 机制加载这些激活的 RouterFactory 对象,由 RouterFactory 创建当前激活的内置 Router 实例,具体实现如下:

public class RouterChain<T> {

    private List<Invoker<T>> invokers = Collections.emptyList();
    
    private volatile List<Router> routers = Collections.emptyList();
        
    private List<Router> builtinRouters = Collections.emptyList();  
    
    private RouterChain(URL url) {
        // 通过ExtensionLoader加载激活的RouterFactory
        List<RouterFactory> extensionFactories = ExtensionLoader.getExtensionLoader(RouterFactory.class)
                .getActivateExtension(url, "router");

        // 遍历所有RouterFactory,调用其getRouter()方法创建相应的Router对象
        List<Router> routers = extensionFactories.stream()
                .map(factory -> factory.getRouter(url))
                .collect(Collectors.toList());
        // 初始化buildinRouters字段以及routers字段
        initWithRouters(routers);
    }

    public void initWithRouters(List<Router> builtinRouters) {
        this.builtinRouters = builtinRouters;
        this.routers = new ArrayList<>(builtinRouters);
        this.sort();
    }
}   

完成内置 Router 的初始化之后,在 Directory 实现中还可以通过 addRouter() 方法添加新的 Router 实例到 routers 字段中,具体实现如下:

public class RouterChain<T> {

    public void addRouters(List<Router> routers) {
        List<Router> newRouters = new ArrayList<>();
        // 添加builtinRouters集合
        newRouters.addAll(builtinRouters);
        // 添加传入的Router集合
        newRouters.addAll(routers);
        // 重新排序
        CollectionUtils.sort(newRouters);
        this.routers = newRouters;
    }

}   

RouterChain.route() 方法会遍历 routers 字段,逐个调用 Router 对象的 route() 方法,对 invokers 集合进行过滤,具体实现如下:

public class RouterChain<T> {

    public List<Invoker<T>> route(URL url, Invocation invocation) {
        List<Invoker<T>> finalInvokers = invokers;
        // 遍历全部的Router对象
        for (Router router : routers) {
            finalInvokers = router.route(finalInvokers, url, invocation);
        }
        return finalInvokers;
    }

}

了解了 RouterChain 的大致逻辑之后,我们知道真正进行路由的是 routers 集合中的 Router 对象。接下来我们再来看 RouterFactory 这个工厂接口,RouterFactory 接口是一个扩展接口,具体定义如下:

@SPI
public interface RouterFactory {

    // 动态生成的适配器会根据protocol参数选择扩展实现
    @Adaptive("protocol")
    Router getRouter(URL url);
}

RouterFactory 接口有很多实现类,如下图所示:


RouterFactory 继承关系图

下面就来深入介绍下每个 RouterFactory 实现类以及对应的 Router 实现对象。Router 决定了一次 Dubbo 调用的目标服务,Router 接口的每个实现类代表了一个路由规则,当 Consumer 访问 Provider 时,Dubbo 根据路由规则筛选出合适的 Provider 列表,之后通过负载均衡算法再次进行筛选。Router 接口的继承关系如下图所示:

Router 继承关系图

接下来我们就开始介绍 RouterFactory 以及 Router 的具体实现。

ConditionRouterFactory&ConditionRouter

首先来看 ConditionRouterFactory 实现,其扩展名为 condition,在其 getRouter() 方法中会创建 ConditionRouter 对象,如下所示:

public class ConditionRouterFactory implements RouterFactory {

    public static final String NAME = "condition";

    @Override
    public Router getRouter(URL url) {
        return new ConditionRouter(url);
    }

}

ConditionRouter 是基于条件表达式的路由实现类,下面就是一条基于条件表达式的路由规则:

host = 192.168.0.100 => host = 192.168.0.150

在上述规则中,=>之前的为 Consumer 匹配的条件,该条件中的所有参数会与 Consumer 的 URL 进行对比,当 Consumer 满足匹配条件时,会对该 Consumer 的此次调用执行 => 后面的过滤规则。

=> 之后为 Provider 地址列表的过滤条件,该条件中的所有参数会和 Provider 的 URL 进行对比,Consumer 最终只拿到过滤后的地址列表。

如果 Consumer 匹配条件为空,表示 => 之后的过滤条件对所有 Consumer 生效,例如:=> host != 192.168.0.150,含义是所有 Consumer 都不能请求 192.168.0.150 这个 Provider 节点。

如果 Provider 过滤条件为空,表示禁止访问所有 Provider,例如:host = 192.168.0.100 =>,含义是 192.168.0.100 这个 Consumer 不能访问任何 Provider 节点。

ConditionRouter 的核心字段有如下几个:

  • url(URL 类型):路由规则的 URL,可以从 rule 参数中获取具体的路由规则。

  • ROUTE_PATTERN(Pattern 类型):用于切分路由规则的正则表达式。

  • priority(int 类型):路由规则的优先级,用于排序,该字段值越大,优先级越高,默认值为 0。

  • force(boolean 类型):当路由结果为空时,是否强制执行。如果不强制执行,则路由结果为空的路由规则将会自动失效;如果强制执行,则直接返回空的路由结果。

  • whenCondition(Map<String, MatchPair> 类型):Consumer 匹配的条件集合,通过解析条件表达式 rule 的 => 之前半部分,可以得到该集合中的内容。

  • thenCondition(Map<String, MatchPair> 类型):Provider 匹配的条件集合,通过解析条件表达式 rule 的 => 之后半部分,可以得到该集合中的内容。

在 ConditionRouter 的构造方法中,会根据 URL 中携带的相应参数初始化 priority、force、enable 等字段,然后从 URL 的 rule 参数中获取路由规则进行解析,具体的解析逻辑是在 init() 方法中实现的,如下所示:

public class ConditionRouter extends AbstractRouter {

    public void init(String rule) {
        try {
            if (rule == null || rule.trim().length() == 0) {
                throw new IllegalArgumentException("Illegal route rule!");
            }
            // 将路由规则中的"consumer."和"provider."字符串清理掉
            rule = rule.replace("consumer.", "").replace("provider.", "");
            // 按照"=>"字符串进行分割,得到whenRule和thenRule两部分
            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();
            // // 解析消费者路由规则 解析whenRule和thenRule,得到whenCondition和thenCondition两个条件集合
            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);
        }
    }
}

whenCondition 和 thenCondition 两个集合中,Key 是条件表达式中指定的参数名称(例如 host = 192.168.0.150 这个表达式中的 host)。ConditionRouter 支持三类参数:

  • 服务调用信息,例如,method、argument 等;

  • URL 本身的字段,例如,protocol、host、port 等;

  • URL 上的所有参数,例如,application 等。

Value 是 MatchPair 对象,包含两个 Set 类型的集合—— matches 和 mismatches。在使用 MatchPair 进行过滤的时候,会按照下面四条规则执行。

  • 当 mismatches 集合为空的时候,会逐个遍历 matches 集合中的匹配条件,匹配成功任意一条即会返回 true。这里具体的匹配逻辑以及后续 mismatches 集合中条件的匹配逻辑,都是在 UrlUtils.isMatchGlobPattern() 方法中实现,其中完成了如下操作:如果匹配条件以 "$" 符号开头,则从 URL 中获取相应的参数值进行匹配;当遇到 "" 通配符的时候,会处理""通配符在匹配条件开头、中间以及末尾三种情况。

  • 当 matches 集合为空的时候,会逐个遍历 mismatches 集合中的匹配条件,匹配成功任意一条即会返回 false。

  • 当 matches 集合和 mismatches 集合同时不为空时,会优先匹配 mismatches 集合中的条件,成功匹配任意一条规则,就会返回 false;若 mismatches 中的条件全部匹配失败,才会开始匹配 matches 集合,成功匹配任意一条规则,就会返回 true。

  • 当上述三个步骤都没有成功匹配时,直接返回 false。

上述流程具体实现在 MatchPair 的 isMatch() 方法中。

了解了每个 MatchPair 的匹配流程之后,我们来看parseRule() 方法是如何解析一条完整的条件表达式,生成对应 MatchPair 的,具体实现如下:

public class ConditionRouter extends AbstractRouter {

    // 正则验证路由规则
    protected static final Pattern ROUTE_PATTERN = Pattern.compile("([&!=,]*)\\s*([^&!=,\\s]+)");

    private static Map<String, MatchPair> parseRule(String rule)
            throws ParseException {
        /**
         * 条件变量和条件变量值的映射关系
         * 比如 host => 127.0.0.1 则保存着 host 和 127.0.0.1 的映射关系
         */ 
        Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
        if (StringUtils.isBlank(rule)) {
            return condition;
        }
        // Key-Value pair, stores both match and mismatch conditions
        MatchPair pair = null;
        // Multiple values
        Set<String> values = null;
        // 首先,按照ROUTE_PATTERN指定的正则表达式匹配整个条件表达式
        final Matcher matcher = ROUTE_PATTERN.matcher(rule);
        while (matcher.find()) { // 遍历匹配的结果
            // 每个匹配结果有两部分(分组),第一部分是分隔符,第二部分是内容
            
            // 获取正则前部分匹配(第一个括号)的内容
            String separator = matcher.group(1);
            // 获取正则后部分匹配(第二个括号)的内容
            String content = matcher.group(2);

            // 如果获取前部分为空,则表示规则开始位置,则当前 content 必为条件变量
            if (StringUtils.isEmpty(separator)) {
                // ---(1) 没有分隔符,content即为参数名称
                pair = new MatchPair();
                // 初始化MatchPair对象,并将其与对应的Key(即content)记录到condition集合中
                condition.put(content, pair);
            }
            // The KV part of the condition expression
            // 如果分隔符是 &,则 content 为条件变量
            else if ("&".equals(separator)) {
                // &分隔符表示多个表达式,会创建多个MatchPair对象
                if (condition.get(content) == null) {
                // 当前 content 是条件变量,用来做映射集合的 key 的,如果没有则添加一个元素
                    pair = new MatchPair();
                    condition.put(content, pair);
                } else {
                    pair = condition.get(content);
                }
            }
            // The Value in the KV part.
            // 如果当前分割符是 = ,则当前 content 为条件变量值
            else if ("=".equals(separator)) {
                 // =以及!=两个分隔符表示KV的分界线
                if (pair == null) {
                    throw new ParseException("Illegal route rule \""
                            + rule + "\", The error char '" + separator
                            + "' at index " + matcher.start() + " before \""
                            + content + "\".", matcher.start());
                }
                // 由于 pair 还没有被重新初始化,所以还是上一个条件变量的对象,所以可以将当前条件变量值在引用对象上赋值
                values = pair.matches;
                values.add(content);
            }
            // 如果当前分割符是 != ,则当前 content 也是条件变量值
            else if ("!=".equals(separator)) {
                if (pair == null) {
                    throw new ParseException("Illegal route rule \""
                            + rule + "\", The error char '" + separator
                            + "' at index " + matcher.start() + " before \""
                            + content + "\".", matcher.start());
                }

                values = pair.mismatches;
                values.add(content);
            }
            / 如果当前分割符为 ',',则当前 content 也为条件变量值
            else if (",".equals(separator)) { // Should be separated by ','
                // 逗号分隔符表示有多个Value值
                if (values == null || values.isEmpty()) {
                    throw new ParseException("Illegal route rule \""
                            + rule + "\", The error char '" + separator
                            + "' at index " + matcher.start() + " before \""
                            + content + "\".", matcher.start());
                }
                // 直接向条件变量值集合中添加数据
                values.add(content);
            } else {
                throw new ParseException("Illegal route rule \"" + rule
                        + "\", The error char '" + separator + "' at index "
                        + matcher.start() + " before \"" + content + "\".", matcher.start());
            }
        }
        return condition;
    }
}

上面就是解析条件路由规则的过程,条件变量的值都保存在 MatchPair 中的 matches、mismatches 属性中,=和,的条件变量值放在可以匹配的 matches 中,!=的条件变量值放在不可匹配路由规则的 mismatches 中。赋值过程中,代码还是比较优雅。

public class ConditionRouter extends AbstractRouter {

    protected static final class MatchPair {
        //可匹配的条件变量值
        final Set<String> matches = new HashSet<String>();
        //不可匹配的条件变量值
        final Set<String> mismatches = new HashSet<String>();
    }
}

实际上 matches、mismatches 就是保存的是条件变量值。

介绍完 parseRule() 方法的实现之后,我们可以再通过下面这个条件表达式示例的解析流程,更深入地体会 parseRule() 方法的工作原理:

host = 2.2.2.2,1.1.1.1,3.3.3.3 & method !=get => host = 1.2.3.4

经过 ROUTE_PATTERN 正则表达式的分组之后,我们得到如下分组:


Rule 分组示意图

我们先来看 => 之前的 Consumer 匹配规则的处理。

  • 分组 1 中,separator 为空字符串,content 为 host 字符串。此时会进入上面示例代码展示的 parseRule() 方法中(1)处的分支,创建 MatchPair 对象,并以 host 为 Key 记录到 condition 集合中。

  • 分组 2 中,separator 为 "=" 空字符串,content 为 "2.2.2.2" 字符串。处理该分组时,会进入 parseRule() 方法中(2) 处的分支,在 MatchPair 的 matches 集合中添加 "2.2.2.2" 字符串。

  • 分组 3 中,separator 为 "," 字符串,content 为 "3.3.3.3" 字符串。处理该分组时,会进入 parseRule() 方法中(3)处的分支,继续向 MatchPair 的 matches 集合中添加 "3.3.3.3" 字符串。

  • 分组 4 中,separator 为 "&" 字符串,content 为 "method" 字符串。处理该分组时,会进入 parseRule() 方法中(4)处的分支,创建新的 MatchPair 对象,并以 method 为 Key 记录到 condition 集合中。

  • 分组 5 中,separator 为 "!=" 字符串,content 为 "get" 字符串。处理该分组时,会进入 parseRule() 方法中(5)处的分支,向步骤 4 新建的 MatchPair 对象中的 mismatches 集合添加 "get" 字符串。

最后,我们得到的 whenCondition 集合如下图所示:


whenCondition 集合示意图

同理,parseRule() 方法解析上述表达式 => 之后的规则得到的 thenCondition 集合,如下图所示:


thenCondition 集合示意图

了解了 ConditionRouter 解析规则的流程以及 MatchPair 内部的匹配原则之后,ConditionRouter 中最后一个需要介绍的内容就是它的 route() 方法了。

ConditionRouter.route() 方法首先会尝试前面创建的 whenCondition 集合,判断此次发起调用的 Consumer 是否符合表达式中 => 之前的 Consumer 过滤条件,若不符合,直接返回整个 invokers 集合;若符合,则通过 thenCondition 集合对 invokers 集合进行过滤,得到符合 Provider 过滤条件的 Invoker 集合,然后返回给上层调用方。ConditionRouter.route() 方法的核心实现如下:

public class ConditionRouter extends AbstractRouter {

    // 在初始化中进行被复制的变量
    // 消费者条件匹配规则
    protected Map<String, MatchPair> whenCondition;
    // 提供者条件匹配规则
    protected Map<String, MatchPair> thenCondition;
    
    @Override
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
            throws RpcException {
        // 通过enable字段判断当前ConditionRouter对象是否可用      
        if (!enabled) {
            return invokers;
        }
        // 验证 invokers 是否为空
        if (CollectionUtils.isEmpty(invokers)) {
            return invokers;
        }
        try {   
            // 校验消费者是否有规则匹配,如果没有则返回传入的 Invoker
            if (!matchWhen(url, invocation)) {
                // 匹配发起请求的Consumer是否符合表达式中=>之前的过滤条件
                return invokers;
            }
            List<Invoker<T>> result = new ArrayList<Invoker<T>>();
            // 判断=>之后是否存在Provider过滤条件,若不存在则直接返回空集合,表示无Provider可用
            if (thenCondition == null) {
                logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
                return result;
            }
            // 遍历传入的 invokers,匹配提供者是否有规则匹配
            for (Invoker<T> invoker : invokers) {
                // 逐个判断Invoker是否符合表达式中=>之后的过滤条件
                if (matchThen(invoker.getUrl(), url)) {
                    // 记录符合条件的Invoker
                    result.add(invoker);
                }
            }
             // 如果 result 不为空,或当前对象 force=true 则返回 result 的 Invoker 列表 
            if (!result.isEmpty()) {
                return result;
            } else if (force) {
                // 在无Invoker符合条件时,根据force决定是返回空集合还是返回全部Invoker
                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;
    }   
}

上面代码可以看到,只要消费者没有匹配的规则或提供者没有匹配的规则及 force=false 时,不会返回传入的参数的 Invoker。

匹配消费者路由规则和提供者路由规则方法是 matchWhen 和 matchThen

public class ConditionRouter extends AbstractRouter {

    boolean matchWhen(URL url, Invocation invocation) {
        return CollectionUtils.isEmptyMap(whenCondition) || matchCondition(whenCondition, url, null, invocation);
    }

    private boolean matchThen(URL url, URL param) {
        return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null);
    }
    
    private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) {
        Map<String, String> sample = url.toMap();
        boolean result = false;
        for (Map.Entry<String, MatchPair> matchPair : condition.entrySet()) {
            String key = matchPair.getKey();
            String sampleValue;
            //get real invoked method name from invocation
            if (invocation != null && (METHOD_KEY.equals(key) || METHODS_KEY.equals(key))) {
                sampleValue = invocation.getMethodName();
            } else if (ADDRESS_KEY.equals(key)) {
                sampleValue = url.getAddress();
            } else if (HOST_KEY.equals(key)) {
                sampleValue = url.getHost();
            } else {
                sampleValue = sample.get(key);
                if (sampleValue == null) {
                    sampleValue = sample.get(key);
                }
            }
            if (sampleValue != null) {
                if (!matchPair.getValue().isMatch(sampleValue, param)) {
                    return false;
                } else {
                    result = true;
                }
            } else {
                //not pass the condition
                if (!matchPair.getValue().matches.isEmpty()) {
                    return false;
                } else {
                    result = true;
                }
            }
        }
        return result;
    }   

}

这两个匹配方法都是调用同一个方法 matchCondition 实现的。将消费者或提供者 URL 转为 Map,然后与 whenCondition 或 thenCondition 进行匹配。

匹配过程中,如果 key (即 sampleValue 值)存在对应的值,则通过 MatchPair#isMatch 方法再进行匹配。

public class ConditionRouter extends AbstractRouter {

    protected static final class MatchPair {
        final Set<String> matches = new HashSet<String>();
        final Set<String> mismatches = new HashSet<String>();
        
        // 存在可匹配的规则,不存在不可匹配的规则
        private boolean isMatch(String value, URL param) {
            if (!matches.isEmpty() && mismatches.isEmpty()) {
                // 不可匹配的规则列表为空时,只要可匹配的规则匹配上,直接返回 true
                for (String match : matches) {
                    if (UrlUtils.isMatchGlobPattern(match, value, param)) {
                        return true;
                    }
                }
                return false;
            }
            // 存在不可匹配的规则,不存在可匹配的规则
            if (!mismatches.isEmpty() && matches.isEmpty()) {
                // 不可匹配的规则列表中存在,则返回false
                for (String mismatch : mismatches) {
                    if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
                        return false;
                    }
                }
                return true;
            }
            // 存在可匹配的规则,也存在不可匹配的规则
            if (!matches.isEmpty() && !mismatches.isEmpty()) {
                // 都不为空时,不可匹配的规则列表中存在,则返回 false
                for (String mismatch : mismatches) {
                    if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
                        return false;
                    }
                }
                for (String match : matches) {
                    if (UrlUtils.isMatchGlobPattern(match, value, param)) {
                        return true;
                    }
                }
                return false;
            }
            // 最后剩下的是 可匹配规则和不可匹配规则都为空时
            return false;
        }
    }
}

匹配过程再调用 UrlUtils#isMatchGlobPattern 实现

public class UrlUtils {

    public static boolean isMatchGlobPattern(String pattern, String value, URL param) {
        // 如果以 $ 开头,则获取 URL 中对应的值
        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 (StringUtils.isEmpty(pattern) && StringUtils.isEmpty(value)) {
            return true;
        }
        if (StringUtils.isEmpty(pattern) || StringUtils.isEmpty(value)) {
            return false;
        }
        // 获取通配符位置
        int i = pattern.lastIndexOf('*');
        // 如果value中没有 "*" 通配符,则整个字符串值匹配
        if (i == -1) {
            return value.equals(pattern);
        }
        // 如果 "*" 在最后面,则匹配字符串 "*" 之前的字符串即可
        else if (i == pattern.length() - 1) {
            return value.startsWith(pattern.substring(0, i));
        }
        // 如果 "*" 在最前面,则匹配字符串 "*" 之后的字符串即可
        else if (i == 0) {
            return value.endsWith(pattern.substring(i + 1));
        }
        // 如果 "*" 不在字符串两端,则同时匹配字符串 "*" 左右两边的字符串
        else {
            String prefix = pattern.substring(0, i);
            String suffix = pattern.substring(i + 1);
            return value.startsWith(prefix) && value.endsWith(suffix);
        }
    }   
}

就这样完成全部的条件路由规则匹配,虽然看似代码较为繁杂,但是理清规则、思路,一步一步还是较好解析,前提是要熟悉相关参数的用法及形式,不然代码较难理解。

ScriptRouterFactory&ScriptRouter

ScriptRouterFactory 的扩展名为 script,其 getRouter() 方法中会创建一个 ScriptRouter 对象并返回。

ScriptRouter 支持 JDK 脚本引擎的所有脚本,例如,JavaScript、JRuby、Groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript。下面我们就定义一个 route() 函数进行 host 过滤:

function route(invokers, invocation, context){
    var result = new java.util.ArrayList(invokers.size()); 
    var targetHost = new java.util.ArrayList();
    targetHost.add("10.134.108.2"); 
    for (var i = 0; i < invokers.length; i) {  // 遍历Invoker集合
        // 判断Invoker的host是否符合条件
        if(targetHost.contains(invokers[i].getUrl().getHost())){
            result.add(invokers[i]);
        }
    }
    return result;
}
route(invokers, invocation, context)  // 立即执行route()函数

我们可以将上面这段代码进行编码并作为 rule 参数的值添加到 URL 中,在这个 URL 传入 ScriptRouter 的构造函数时,即可被 ScriptRouter 解析。

ScriptRouter 的核心字段有如下几个:

  • url(URL 类型):路由规则的 URL,可以从 rule 参数中获取具体的路由规则。

  • priority(int 类型):路由规则的优先级,用于排序,该字段值越大,优先级越高,默认值为 0。

  • ENGINES(ConcurrentHashMap<String, ScriptEngine> 类型):这是一个 static 集合,其中的 Key 是脚本语言的名称,Value 是对应的 ScriptEngine 对象。这里会按照脚本语言的类型复用 ScriptEngine 对象。

  • engine(ScriptEngine 类型):当前 ScriptRouter 使用的 ScriptEngine 对象。

  • rule(String 类型):当前 ScriptRouter 使用的具体脚本内容。

  • function(CompiledScript 类型):根据 rule 这个具体脚本内容编译得到。

在 ScriptRouter 的构造函数中,首先会初始化 url 字段以及 priority 字段(用于排序),然后根据 URL 中的 type 参数初始化 engine、rule 和 function 三个核心字段 ,具体实现如下:

public class ScriptRouter extends AbstractRouter {

    public ScriptRouter(URL url) {
        this.url = url;
        this.priority = url.getParameter(PRIORITY_KEY, SCRIPT_ROUTER_DEFAULT_PRIORITY);
        // 根据URL中的type参数值,从ENGINES集合中获取对应的ScriptEngine对象
        engine = getEngine(url);
        // 获取URL中的rule参数值,即为具体的脚本
        rule = getRule(url);
        try {
            Compilable compilable = (Compilable) engine;
            // 编译rule字段中的脚本,得到function字段
            function = compilable.compile(rule);
        } catch (ScriptException e) {
            logger.error("route error, rule has been ignored. rule: " + rule +
                    ", url: " + RpcContext.getContext().getUrl(), e);
        }
    }
}

接下来看 ScriptRouter 对 route() 方法的实现,其中首先会创建调用 function 函数所需的入参,也就是 Bindings 对象,然后调用 function 函数得到过滤后的 Invoker 集合,最后通过 getRoutedInvokers() 方法整理 Invoker 集合得到最终的返回值。

public class ScriptRouter extends AbstractRouter {

    @Override
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        try {
            // 创建Bindings对象作为function函数的入参
            Bindings bindings = createBindings(invokers, invocation);
            if (function == null) {
                return invokers;
            }
            // 调用function函数,并在getRoutedInvokers()方法中整理得到的Invoker集合
            return getRoutedInvokers(function.eval(bindings));
        } catch (ScriptException e) {
            logger.error("route error, rule has been ignored. rule: " + rule + ", method:" +
                    invocation.getMethodName() + ", url: " + RpcContext.getContext().getUrl(), e);
            return invokers;
        }
    }
    
    private <T> Bindings createBindings(List<Invoker<T>> invokers, Invocation invocation) {
        Bindings bindings = engine.createBindings();
        // 与前面的javascript的示例脚本结合,我们可以看到这里在Bindings中为脚本中的route()函数提供了invokers、Invocation、context三个参数
        bindings.put("invokers", new ArrayList<>(invokers));
        bindings.put("invocation", invocation);
        bindings.put("context", RpcContext.getContext());
        return bindings;
    }   
}

总结

本文介绍了 Router 接口的相关内容。首先我们介绍了 RouterChain 的核心实现以及构建过程,然后讲解了 RouterFactory 接口和 Router 接口中核心方法的功能。接下来,我们还深入分析了ConditionRouter 对条件路由功能的实现,以及ScriptRouter 对脚本路由功能的实现。

单纯从逻辑上,如果能够掌握条件路由的实现,去研究其它方式的路由实现,相信不会有太大问题。只是例如像脚本路由的实现,你得先会使用脚本执行引擎为前提,不然就不理解它的代码。最后,在 dubbo-admin 上可以设置路由,大家可以尝试各种使用规则,通过实操才能更好掌握和理解路由机制的实现。

参考:
https://www.cnblogs.com/ytao-blog/p/12704763.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342