美文网首页
Dubbo插件化SPI

Dubbo插件化SPI

作者: 你值得拥有更好的12138 | 来源:发表于2019-01-04 22:07 被阅读0次

    一、SPI

    SPI 简介

    SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现, 简单来说,它就是一种动态替换发现的机制, 举个例子来说, 有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。如果是普通的面向接口编程,你虽然不需要去改原有代码只需要加一个实现类但是你需要重新打包。这就很不方便,SPI通过配置文件的方式不需要重新打包。他会遍历所有resource下的目录。详情(https://juejin.im/post/5af952fdf265da0b9e652de3

    二、Dubbo的ExtensionLoader插件工具

    1.ExtensionLoader是url总线模式,url中的参数变化会影响获取的实现类结果。
    2.ExtensionLoader类似于JAVA的SPI通过配置文件配置,JAVA的SPI不能获取具体的实现类,但是配置方式是键值对的方式,所以可以通过键的方式获取具体的实现类。
    3.ExtensionLoader获取的实现类,并不是实现类本身,而是接口类的代理类!当调用的时候才能具体到某一个实现类。
    4.ExtensionLoader在实现过程还有一些Dubbo的注解进行处理,比JAVA的SPI复杂

    三、获取自适应类的例子

    ExtensionLoader有几种方式获取,这里只简述下获取自适应类的方法。

    先来看构造URL的过程,在服务注册的方法doExportUrls中有加载配置的注册中心

    private void doExportUrls() {
            //加载注册中心
            List<URL> registryURLs = loadRegistries(true);
            for (ProtocolConfig protocolConfig : protocols) {
                doExportUrlsFor1Protocol(protocolConfig, registryURLs);
            }
        }
    

    加载注册中心的方法进行配置文件的读取,然后进行URL的构建,在构造过程中有proctol这个参数就决定后面获取Protocol的实现类。

    protected List<URL> loadRegistries(boolean provider) {
            //读取配置文件获取注册中心
            checkRegistry();
            List<URL> registryList = new ArrayList<URL>();
            if (registries != null && !registries.isEmpty()) {
                for (RegistryConfig config : registries) {
                    String address = config.getAddress();
                    if (address == null || address.length() == 0) {
                        address = Constants.ANYHOST_VALUE;
                    }
                    String sysaddress = System.getProperty("dubbo.registry.address");
                    if (sysaddress != null && sysaddress.length() > 0) {
                        address = sysaddress;
                    }
                    if (address.length() > 0 && !RegistryConfig.NO_AVAILABLE.equalsIgnoreCase(address)) {
                        Map<String, String> map = new HashMap<String, String>();
                        appendParameters(map, application);
                        appendParameters(map, config);
                        map.put("path", RegistryService.class.getName());
                        map.put("dubbo", Version.getProtocolVersion());
                        map.put(Constants.TIMESTAMP_KEY, String.valueOf(System.currentTimeMillis()));
                        if (ConfigUtils.getPid() > 0) {
                            map.put(Constants.PID_KEY, String.valueOf(ConfigUtils.getPid()));
                        }
                        //协议默认没有,默认也是没有配置remote这个选项的所以,这里默认先是dubbo
                        if (!map.containsKey("protocol")) {
                            if (ExtensionLoader.getExtensionLoader(RegistryFactory.class).hasExtension("remote")) {
                                map.put("protocol", "remote");
                            } else {
                                map.put("protocol", "dubbo");
                            }
                        }
                        //构造URL,将map中的参数设置到URL中
                        List<URL> urls = UrlUtils.parseURLs(address, map);
                        for (URL url : urls) {
                            //添加一个键值对registry---- zookeeper(如果你配置的是zookeeper)
                            url = url.addParameter(Constants.REGISTRY_KEY, url.getProtocol());
                            //将成员变量设置为registry
                            url = url.setProtocol(Constants.REGISTRY_PROTOCOL);
                            if ((provider && url.getParameter(Constants.REGISTER_KEY, true))
                                    || (!provider && url.getParameter(Constants.SUBSCRIBE_KEY, true))) {
                                registryList.add(url);
                            }
                        }
                    }
                }
            }
            return registryList;
        }
    

    在上述构造出来的URL会把你配置在配置文件中的注册中心的的协议设置到URL的protocl成员变量中,然后把map设置到paramters成员变量中。

    public /**final**/ class URL implements Serializable {
    
        private static final long serialVersionUID = -1985165475234910535L;
    
        private final String protocol;
    
        private final String username;
    
        private final String password;
    
        // by default, host to registry
        private final String host;
    
        // by default, port to registry
        private final int port;
    
        private final String path;
    
        private final Map<String, String> parameters;
    }
    

    再来看看UrlUtils.parseURLs发生了什么

      public static URL parseURL(String address, Map<String, String> defaults) {
            if (address == null || address.length() == 0) {
                return null;
            }
            String url;
            if (address.contains("://") || address.contains(URL_PARAM_STARTING_SYMBOL)) {
                url = address;
            } else {
                String[] addresses = Constants.COMMA_SPLIT_PATTERN.split(address);
                url = addresses[0];
                if (addresses.length > 1) {
                    StringBuilder backup = new StringBuilder();
                    for (int i = 1; i < addresses.length; i++) {
                        if (i > 1) {
                            backup.append(",");
                        }
                        backup.append(addresses[i]);
                    }
                    url += URL_PARAM_STARTING_SYMBOL + Constants.BACKUP_KEY + "=" + backup.toString();
                }
            }
            String defaultProtocol = defaults == null ? null : defaults.get("protocol");
            if (defaultProtocol == null || defaultProtocol.length() == 0) {
                defaultProtocol = "dubbo";
            }
            String defaultUsername = defaults == null ? null : defaults.get("username");
            String defaultPassword = defaults == null ? null : defaults.get("password");
            int defaultPort = StringUtils.parseInteger(defaults == null ? null : defaults.get("port"));
            String defaultPath = defaults == null ? null : defaults.get("path");
            Map<String, String> defaultParameters = defaults == null ? null : new HashMap<String, String>(defaults);
            if (defaultParameters != null) {
                defaultParameters.remove("protocol");
                defaultParameters.remove("username");
                defaultParameters.remove("password");
                defaultParameters.remove("host");
                defaultParameters.remove("port");
                defaultParameters.remove("path");
            }
            //这方法会将url.indexOf("://")所在位置前面截断赋值给URL的protocol
            URL u = URL.valueOf(url);
            boolean changed = false;
            //把url中的协议部分提出来
            String protocol = u.getProtocol();
            String username = u.getUsername();
            String password = u.getPassword();
            String host = u.getHost();
            int port = u.getPort();
            String path = u.getPath();
            Map<String, String> parameters = new HashMap<String, String>(u.getParameters());
             //假如protocl是空就从传入的map中获取
            if ((protocol == null || protocol.length() == 0) && defaultProtocol != null && defaultProtocol.length() > 0) {
                changed = true;
                protocol = defaultProtocol;
            }
            if ((username == null || username.length() == 0) && defaultUsername != null && defaultUsername.length() > 0) {
                changed = true;
                username = defaultUsername;
            }
            if ((password == null || password.length() == 0) && defaultPassword != null && defaultPassword.length() > 0) {
                changed = true;
                password = defaultPassword;
            }
            /*if (u.isAnyHost() || u.isLocalHost()) {
                changed = true;
                host = NetUtils.getLocalHost();
            }*/
            if (port <= 0) {
                if (defaultPort > 0) {
                    changed = true;
                    port = defaultPort;
                } else {
                    changed = true;
                    port = 9090;
                }
            }
            if (path == null || path.length() == 0) {
                if (defaultPath != null && defaultPath.length() > 0) {
                    changed = true;
                    path = defaultPath;
                }
            }
            if (defaultParameters != null && defaultParameters.size() > 0) {
                for (Map.Entry<String, String> entry : defaultParameters.entrySet()) {
                    String key = entry.getKey();
                    String defaultValue = entry.getValue();
                    if (defaultValue != null && defaultValue.length() > 0) {
                        String value = parameters.get(key);
                        if (value == null || value.length() == 0) {
                            changed = true;
                            parameters.put(key, defaultValue);
                        }
                    }
                }
            }
            if (changed) {
               //使用构造函数进行成员变量的设置
                u = new URL(protocol, username, password, host, port, path, parameters);
            }
            return u;
        }
    

    举例如果你配置注册中心的是这样的zookeeper://192.168.99.100:32770,那么回来的URL的成员变量protocol就是zookeeper,然后parameters中键值对protocol------dubbo.然后在进行上一段循环代码的处理 ,添加一个键值对registry---- zookeeper(如果你配置的是zookeeper),将成员变量设置为registry。
    先对Dubbo的注解有个概念:

    /**
     * Protocol. (API/SPI, Singleton, ThreadSafe)
     */
    @SPI("dubbo")
    public interface Protocol {
    
        /**
         * Get default port when user doesn't config the port.
         *
         * @return default port
         */
        int getDefaultPort();
    
        /**
         * Export service for remote invocation: <br>
         * 1. Protocol should record request source address after receive a request:
         * RpcContext.getContext().setRemoteAddress();<br>
         * 2. export() must be idempotent, that is, there's no difference between invoking once and invoking twice when
         * export the same URL<br>
         * 3. Invoker instance is passed in by the framework, protocol needs not to care <br>
         *
         * @param <T>     Service type
         * @param invoker Service invoker
         * @return exporter reference for exported service, useful for unexport the service later
         * @throws RpcException thrown when error occurs during export the service, for example: port is occupied
         */
        @Adaptive
        <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    }
    

    然后来看在ServiceConfig的插件化工具是怎么根据URL获取对应的实现类

    public class ServiceConfig<T> extends AbstractServiceConfig {
    
        private static final long serialVersionUID = 3033787999037024738L;
    
        private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
    
        private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
    
        private static final Map<String, Integer> RANDOM_PORT_MAP = new HashMap<String, Integer>();
    }
    
    

    ExtensionLoader是一个类型对应一个实例,这实例会被一个map缓存起来,这些每一个实例也有一个map把通过扫描符合传入的类型接口的实现类缓存起来下次使用。
    这里以Protocol为例,看看getAdaptiveExtension在做什么?顺着方法一直走到这个方法createAdaptiveExtensionClassCode,下面截取部分

            //拼接类的开始代码
           codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
            codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
            codeBuilder.append("\npublic class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {");
    
            codeBuilder.append("\nprivate static final org.apache.dubbo.common.logger.Logger logger = org.apache.dubbo.common.logger.LoggerFactory.getLogger(ExtensionLoader.class);");
            codeBuilder.append("\nprivate java.util.concurrent.atomic.AtomicInteger count = new java.util.concurrent.atomic.AtomicInteger(0);\n");
            //判断方法是否符合要求
            for (Method method : methods) {
                Class<?> rt = method.getReturnType();
                Class<?>[] pts = method.getParameterTypes();
                Class<?>[] ets = method.getExceptionTypes();
                //方法必须要有Adaptive 注解
                Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
                StringBuilder code = new StringBuilder(512);
                if (adaptiveAnnotation == null) {
                    code.append("throw new UnsupportedOperationException(\"method ")
                            .append(method.toString()).append(" of interface ")
                            .append(type.getName()).append(" is not adaptive method!\");");
                } else {
                     //父类子类必须有跟URL有关系的方法
                    int urlTypeIndex = -1;
                    for (int i = 0; i < pts.length; ++i) {
                        if (pts[i].equals(URL.class)) {
                            urlTypeIndex = i;
                            break;
                        }
                    }
                    // found parameter in URL type
                    if (urlTypeIndex != -1) {
                        // Null Point check
                        String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
                                urlTypeIndex);
                        code.append(s);
    
                        s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex);
                        code.append(s);
                    }
                    // did not find parameter in URL type
                    else {
                        String attribMethod = null;
    
                        // find URL getter method
                        LBL_PTS:
                        //如果子类没有这样的方法,那么就通过方法的参数的子父类来查找
                        for (int i = 0; i < pts.length; ++i) {
                            Method[] ms = pts[i].getMethods();
                            for (Method m : ms) {
                                String name = m.getName();
                                if ((name.startsWith("get") || name.length() > 3)
                                        && Modifier.isPublic(m.getModifiers())
                                        && !Modifier.isStatic(m.getModifiers())
                                        && m.getParameterTypes().length == 0
                                        && m.getReturnType() == URL.class) {
                                    urlTypeIndex = i;
                                    attribMethod = name;
                                    break LBL_PTS;
                                }
                            }
                        }
                        if (attribMethod == null) {
                            throw new IllegalStateException("fail to create adaptive class for interface " + type.getName()
                                    + ": not found url parameter or url attribute in parameters of method " + method.getName());
                        }
    
                        // Null point check
                        String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
                                urlTypeIndex, pts[urlTypeIndex].getName());
                        code.append(s);
                        s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",
                                urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
                        code.append(s);
    
                        s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod);
                        code.append(s);
                    }
    
    

    然后在这过程中会获取注解中的值,下面value就是注解中adaptiveAnnotation 的值,hasInvocation表示方法是否有参数Invocation。defaultExtName是SPI注解中的值。重要的代码在这里

    for (int i = value.length - 1; i >= 0; --i) {
                        if (i == value.length - 1) {
                          //判断有默认配置的类吗
                            if (null != defaultExtName) {
                              //adaptiveAnnotation 中值
                                if (!"protocol".equals(value[i])) {
                              
                                    if (hasInvocation) {
                                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                                    } else {
                                        getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                                    }
                                } else {
                                    getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
                                }
                            } else {
                                if (!"protocol".equals(value[i])) {
                                    if (hasInvocation) {
                                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                                    } else {
                                        getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                                    }
                                } else {
                                    getNameCode = "url.getProtocol()";
                                }
                            }
                        } else {
                            if (!"protocol".equals(value[i])) {
                                if (hasInvocation) {
                                    getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                                } else {
                                    getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                                }
                            } else {
                                getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
                            }
                        }
                    }
                    //获取对应的配置文件中的键
                    code.append("\nString extName = ").append(getNameCode).append(";");
                    // check extName == null?
                    String s = String.format("\nif(extName == null) " +
                                    "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");",
                            type.getName(), Arrays.toString(value));
                    code.append(s);
                    //通过获取的url的值,在插件化工具的扫描结果中筛选出指定的实现类
                    code.append(String.format("\n%s extension = null;\n try {\nextension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);\n}catch(Exception e){\n",
                            type.getName(), ExtensionLoader.class.getSimpleName(), type.getName()));
                    code.append(String.format("if (count.incrementAndGet() == 1) {\nlogger.warn(\"Failed to find extension named \" + extName + \" for type %s, will use default extension %s instead.\", e);\n}\n",
                            type.getName(), defaultExtName));
                    code.append(String.format("extension = (%s)%s.getExtensionLoader(%s.class).getExtension(\"%s\");\n}",
                            type.getName(), ExtensionLoader.class.getSimpleName(), type.getName(), defaultExtName));
    

    以获取Protocol为例,extName 最终的值会是从URL的protocol成员变量中获取,前面说过值registry,所以到配置文件中去查找是那一个类。

    registry=org.apache.dubbo.registry.integration.RegistryProtocol
    

    有些地方还是没说清楚,后面再改吧!

    相关文章

      网友评论

          本文标题:Dubbo插件化SPI

          本文链接:https://www.haomeiwen.com/subject/dqdxxqtx.html