深度解析dubbo集群路由之脚本路由

本文基于dubbo v2.6.x

1.脚本路由介绍

脚本路由,顾名思义,就是使用脚本语言进行路由处理,脚本路由规则支持 JDK 脚本引擎的所有脚本,比如:javascript, jruby, groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript。
这里拿官网文档上的例子介绍下(官方文档:链接
脚本路由的url示例:

"script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("(function route(invokers) { ... } (invokers))")

脚本 rule 规则:

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); // 表示立即执行方法

可以看到这里服务提供者ip只有是10.20.153.10 才能匹配上。

2.ScriptRouterFactory源码解析

public class ScriptRouterFactory implements RouterFactory {
    public static final String NAME = "script";
    @Override
    public Router getRouter(URL url) {
        return new ScriptRouter(url);
    }
}

ScriptRouterFactory脚本路由工厂,实现RouterFactory接口,重写getRouter()方法,我们可以看到可以通过脚本路由工厂ScriptRouterFactory获得脚本路由ScriptRouter对象,接下来我们看下 ScriptRouter的源码

3.ScriptRouter源码解析

ScriptRouter就是我们的脚本路由,支持多种脚本引擎。我们先来看下它的class 定义:

public class ScriptRouter extends AbstractRouter {...}

可以看到是继承AbstractRouter 抽象类的。在看下它的成员

private static final int DEFAULT_PRIORITY = 1;  // 默认优先级
/**
 * 脚本类型与脚本引擎
 */
private static final Map<String, ScriptEngine> engines = new ConcurrentHashMap<String, ScriptEngine>();

private final ScriptEngine engine;// 脚本执行引擎

private final String rule;// 规则

上面那两个类成员一个是优先级,一个是脚本引擎的缓存(key:类型, value:对应的脚本引擎对象)
engine:当前该对象使用的脚本执行引擎
rule:这个也就是你写的那个脚本
接下来我们来看下ScriptRouter的构造方法:


    public ScriptRouter(URL url) {
        this.url = url;

        //获得type 。这里没有设置 下面会默认是javascript
        String type = url.getParameter(Constants.TYPE_KEY);

        //获取优先级 , 默认是1
        this.priority = url.getParameter(Constants.PRIORITY_KEY, DEFAULT_PRIORITY);

        // rule
        String rule = url.getParameterAndDecoded(Constants.RULE_KEY);
        if (type == null || type.length() == 0) {// 类型是空  默认是javascript
            type = Constants.DEFAULT_SCRIPT_TYPE_KEY;
        }

        // 规则不能为空
        if (rule == null || rule.length() == 0) {// rule不能为空
            throw new IllegalStateException(new IllegalStateException("route rule can not be empty. rule:" + rule));
        }

        // 根据脚本类型从缓存中获取 engine ,如果没有的话就创建,然后塞到缓存中
        ScriptEngine engine = engines.get(type); // 从缓存中获取
        if (engine == null) {

            // 使用java 自带script引擎管理器获得相应的脚本引擎
            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;
    }

首先是从路由url中获取type(这个type是脚本的类型),获取优先级priority ,缺省是1,接着就是从路由url中获取具体的执行规则。判断脚本type是否为空,如果是空的话默认是javascript。根据脚本类型从引擎缓存engines中获取对应的引擎,如果没有就使用java自己的引擎管理器获取,如果没有对应的脚本执行引擎,就抛出异常,如果有的话就缓存起来。构造还是比较简单的,就是获取一些参数,脚本类型type,具体规则rule,优先级priority,再就是根据脚本类型获取对应的执行引擎engine。
接着在看下它实现的route具体执行路由方法。

	@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) {
            // 执行错误只进行打印
            //fail then ignore rule .invokers.
            logger.error("route error , rule has been ignored. rule: " + rule + ", method:" + invocation.getMethodName() + ", url: " + RpcContext.getContext().getUrl(), e);
            //返回全部invoker
            return invokers;
        }
    }

解释下这个三个参数:
List<Invoker> invokers: 服务提供者列表
URL url : 服务调用者url
Invocation invocation: 调用信息
我们看下方法里面,先是获取Bindings(可以理解为用来存放数据的容器),向容器中放了invokers,invocation,context这三个变量(可以在脚本中直接使用),接着就是编译rule,编译咱们写的那个路由规则函数。调用eval 执行返回obj,后面的就是判断obj的类型了,如果是invoker数组类型,就处理成invoker集合,如果是Object数组类型,也处理成invoker集合,否则就是强制转成invoker集合,返回过滤完的invoker集合,如果期间出现了异常,打印error日志,并返回全部的invoker。

4.FileRouterFactory源码解析

FileRouterFactory 文件路由工厂,文章最后解析FileRouterFactory的原因是:这个文件路由工厂其实也是创建ScriptRouter的工厂,我们来看下它的实现

public class FileRouterFactory implements RouterFactory {

    public static final String NAME = "file";

    private RouterFactory routerFactory;

    public void setRouterFactory(RouterFactory routerFactory) {
        this.routerFactory = routerFactory;
    }
    @Override
    public Router getRouter(URL url) {
        try {
            // Transform File URL into Script Route URL, and Load
            // file:///d:/path/to/route.js?router=script ==> script:///d:/path/to/route.js?type=js&rule=<file-content>
            // 获取router 配置项。默认是script
            String protocol = url.getParameter(Constants.ROUTER_KEY, ScriptRouterFactory.NAME); // Replace original protocol (maybe 'file') with 'script'
            // 使用文件后缀作为类型
            String type = null; // Use file suffix to config script type, e.g., js, groovy ...
            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())));
           // 创建路由规则 URL runtime 缺省 false
            boolean runtime = url.getParameter(Constants.RUNTIME_KEY, false);
            // type
            URL script = url.setProtocol(protocol).addParameter(Constants.TYPE_KEY, type).addParameter(Constants.RUNTIME_KEY, runtime).addParameterAndEncoded(Constants.RULE_KEY, rule);
            //使用dubbo spi机制获得router
            return routerFactory.getRouter(script);
        } catch (IOException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

}

这里我们直接看下文件路由工厂FileRouterFactory的getRouter方法,根据router参数获取对应的protocol值,缺省的话我们看到是script,也就是脚本。接着就是解析path的尾缀,也就是文件的尾缀,赋值给type,读取文件的内容为 rule规则,然后就是生成脚本路由url了,使用自适应路由工厂routerFactory获取对应的Router,这里其实是使用ScriptRouterFactory脚本路由工厂根据类型type获取对应的路由实现。

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页