Dubbo学习经验总结

不积跬步,无以至千里;不积小流,无以成江河。

干了四五年Java开发,总想写一些自己感觉上档次的文章,越是这样越是无处下手,到现在一遍自己的博客都没写过。所以从现在开始,从技术最基础的地方出发,记录下自己的学习经历和一点点心得。本文将通过官方资料和阅读源码来讲述我自己对dubbo的理解。一则记录自己学习的成果,再则帮助更多初学者少走弯路。有错误之处,还望各位指正 。

本文使用目前dubbo的最新版本2.7.5为基础,Java版本是1.8.212。后续关于dubbo相关的文章也全部是使用这个版本。


Apache Dubbo™是基于Java的高性能开源RPC框架。Dubbo提供了三个关键功能,基于接口的远程调用,容错和负载平衡以及自动服务注册和发现。

1 基础架构

第一节是根据官方介绍翻译而来,熟悉的朋友直接跳过!

也可以直接看Dubbo中文网址:http://dubbo.apache.org/zh-cn/docs/user/preface/architecture.html

这是Dubbo官方的架构图:
Architecture
先对这几个角色进行简要说明:

  1. Provider 服务提供者;
  2. Consumer 服务消费者,消费provider提供的服务;
  3. Registry 注册中心,服务的注册和发现以及配置;
  4. Monitor 监视器,负责监控服务的调用的次数和时间等;
  5. Container 服务容器,负责服务的生命周期;

架构图中角色之间的调用流程

  1. 先启动注册中心 ;
  2. 通过Container或者单独启动Provider, 启动后会把自己的元数据信息注册到Register上 ;
  3. 启动Consumer ,启动后会从Register上订阅所有的服务,然后把自己的元数据也注册到Register ;
  4. 启动Monitor ,启动后会从Register上读取Consumer和Provider的元数据信息 ;
  5. Register将提供者列表返回给消费者,当Provider有更改时,将通过长连接将更改后的数据推送到消费者;
  6. 当Consumer调用Provider的服务之前,会使用负载均衡算法确定调用哪一个Provider的服务,如果调用失败会选择其他的可用的Provider ;
  7. Consumer和Provider都将计算服务调用的数量和在内存中所花费的时间,并将统计信息发送给Monitor;

dubbo的四个特性:

1 Connectivity 连接性

  • Register负责服务地址的注册和搜索,例如目录服务,Provider和Consumer仅在启动期间与Register交互,并且Register不转发请求,因此压力较小
  • Monitor负责计算服务调用的数量和耗时,统计信息首先会在Provider和Consumer的内存中计算汇总,然后发送给Monitor
  • Provider将服务注册到Register,并将耗时的统计信息(不包括网络开销)报告给Monitor
  • Consumer从Register中获取Provider的地址列表,根据LB算法调用Provider的服务,并向Monitor报告耗时的统计信息(包括网络开销)
  • Register,Provider和Consumer之间的连接是长连接, Monitor与他们的连接是短连接
  • Register通过长连接知道Provider的存在,当Provider掉线时,Register会将事件推送到Consumer
  • 当Consumer启动后会从Register获取所有的已经注册了的Provider的信息,然后缓存到本地;此时就算Register和Monitor都挂掉,也不会影响Consumer对已经正常启动的Provider的调用
  • Register 和 Monitor是可选的,Consumer可以直连调用Provider

2 Robustness 稳定性

  • Monitor停机不会影响使用,只会丢失一些简单的数据
  • 当Register的储存服务挂掉时,Register可以通过其缓存将Provider的信息返回给Consumer。但此时新的Provider无法注册到Register
  • Register是一个对等集群,当任何实例出现故障时,它将自动切换到另一个
  • 即使所有Register的实例都崩溃了,Provider和Consumer仍可以通过检查其本地缓存来进行通信
  • Provider是无状态的,它的停机后启动不会影响使用
  • 当一项服务的所有Provider都挂掉之后,Consumer将无法使用该服务,会无限地重新连接以等待服务提供程序恢复

3 Scalability 扩展性

  • Register是一个可以动态增加其实例的对等集群,所有客户端将自动发现新实例
  • Provider是无状态的,它可以动态增加部署实例,并且Register会将新的Provider的信息推送给Consumer。

4 Upgradeability 灵活性

  • 当服务集群进一步扩展并且IT治理结构进一步升级时,需要动态部署,并且当前的分布式服务体系结构不会带来阻力。

这是未来的架构图
在这里插入图片描述

2 SPI原理

SPI是dubbo实现的基石,只有完全理解SPI的工作原理,才能更好的掌握Dubbo内部工作原理,这一节我将花大力气研究这个原理。

2.1 JAVA SPI

SPI 全称是Service Provider Interface。在Java中SPI是被用来设计给服务提供商做插件使用的

SPI是基于策略模式 来实现动态加载的机制 。我们在程序只定义一个接口,具体的实现交个不同的服务提供者;在程序启动的时候,读取配置文件,由配置确定要调用哪一个实现;
举个例子:在Java程序中想操作数据库,由于数据库有关系型数据和非关系型数据库,在关系型的数据库中又有Oracle,mysql,sqlServer等。那在程序中如何与他们建立连接呢?
在java.sql包中就定义了一个总的接口:Driver.java 假如要使用mysql数据库, 就需要导入MySQL的驱动包mysql-connector-java-5.1.46.jar

以下为Java中Driver这个SPI的源码

package java.sql;

import java.util.logging.Logger;

/**
 * The interface that every driver class must implement.
 */
public interface Driver {

    Connection connect(String url, java.util.Properties info)
        throws SQLException;
        
    boolean acceptsURL(String url) throws SQLException;

    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;

    int getMajorVersion();

    int getMinorVersion();

    boolean jdbcCompliant();

    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

在mysql-connector-java-5.1.46.jar包中会有MySQL提供的实现com.mysql.jdbc.Driver,这个在目录中的配置文件中指定java.sql.Driver连接MySQL的实现:如果有多个实现,就用分行符分开,如下所示:
在这里插入图片描述
这个是java.mysql.jdbc.Driver的源码,具体实现逻辑在其父类NonRegisteringDriver中:

package com.mysql.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

在Java中实现这个核心类为:java.util.ServiceLoader ;
这是部ServiceLoader的部分代码,这里配置文件默认的路径前缀是:META-INF/services/在这里插入图片描述

这是ServiceLoader中加载配置的地方,需要详细源码可以自己去JDK源码中查看,这里不做赘述。

 private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    //配置文件的全名为:默认前缀+SPI接口的全限定名,如:META-INF/services/java.sql.Driver
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

使用ServiceLoader加载个服务提供商的实现的简单实现如下:

public class TestM {
  public static void main(String[] args) {
      //获取所有Driver的扩展点,这个在实际中有点浪费资源
      ServiceLoader<Driver> driverServiceLoader = ServiceLoader.load(Driver.class);
      driverServiceLoader.forEach(driver ->{
          //打印当前我的项目中可用的所有服务商的Driver实现
          System.out.println(driver.getClass().getName());
      });
  }
}
//输出结果,这是我的项目,具体会有多少个Driver,要看自己导入了多少个服务商jar包
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
com.alibaba.druid.proxy.DruidDriver
com.alibaba.druid.mock.MockDriver

SPI主要用于框架扩展和替换组件。一个好的开源框架,必须要留一些扩展点,让使用者可以自定义某一些组件,而这个过程要做到黑盒扩展。

2.2 Dubbo SPI

Dubbo SPI和Java SPI其实很类似,但是Dubbo SPI做出了很多改进和优化,并且Dubbo的SPI接口都会使用@SPI注解标识,这是Dubbo自己的注解。
源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {

    /**
     * default extension name
     * 默认实现类的名字。配置在去路径接口名称文件中的key
     */
    String value() default "";

}

在Dubbo框架中,@SPI只是用在接口上,它的主要作用就是标记这个接口是一个SPI接口,可以有多个不同的内置的或者用户自己的实现,然后在运行的时候,@SPI中的参数作为实现类的名称来查找具体的实现类。

举个例子:
以下是dubbo序列化的SPI接口,首先在接口上使用@SPI标记,然后注解中传入了一个参数hessian2 。使用这个参数来确定Serialization接口的默认实现的名称是hessian2。用户也可以通过dubbo.protocol.serialization=xxx来指定自己想要的实现,如果系统中没有你指定的实现,你可以自行实现一个。非常灵活!

/**
 * Serialization strategy interface that specifies a serializer. (SPI, Singleton, ThreadSafe)
 *
 * The default extension is hessian2 and the default serialization implementation of the dubbo protocol.
 * <pre>
 *     e.g. &lt;dubbo:protocol serialization="xxx" /&gt;
 * </pre>
 */
@SPI("hessian2")
public interface Serialization {

    /**
     * Get content type unique id, recommended that custom implementations use values different with
     * any value of {@link Constants} and don't greater than ExchangeCodec.SERIALIZATION_MASK (31) 
     * because dubbo protocol use 5 bits to record serialization ID in header.
     *
     * @return content type id
     */
    byte getContentTypeId();

    /**
     * Get content type
     *
     * @return content type
     */
    String getContentType();

    /**
     * Get a serialization implementation instance
     *
     * @param url URL address for the remote service
     * @param output the underlying output stream
     * @return serializer
     * @throws IOException
     */
    @Adaptive
    ObjectOutput serialize(URL url, OutputStream output) throws IOException;

    /**
     * Get a deserialization implementation instance
     *
     * @param url URL address for the remote service
     * @param input the underlying input stream
     * @return deserializer
     * @throws IOException
     */
    @Adaptive
    ObjectInput deserialize(URL url, InputStream input) throws IOException;

}

这是hessian2的里面的具体配置,这也是dubbo的默认的内置实现:
在这里插入图片描述
Dubbo SPI相对于JAVA SPI的新特性:

  1. 扩展点的加载
    Java SPI在加载扩展点的时候,会一次性加载所有可用的扩展点,如上1.2.1中所示,我只需要MySQL的jdbc扩展,结果会查询很多我不需要的。因此,会浪费系统资源;
  2. 加载扩展点异常信息
    Java SPI加载失败是,可以能会导致异常信息丢失,导致追踪问题很困难;Dubbo SPI在加载扩展点失败时会先抛出真是异常,并打印日志,某一个扩展点加载失败也不会影响其他扩展点和整个框架的使用。
  3. 对扩展点IoC和AOP的支持
    Dubbo SPI加载扩展点的时候,会为扩展点中所有的setter属性(非基础类型属性)注入响应的实例。然后,Dubbo SPI只是加载配置文件中的类到系统的缓存中,不会立即初始化所有扩展点,实现懒加载。这个会更节约系统资源,提高系统的性能。

    现在来敲重点,Dubbo SPI的核心实现是:org.apache.dubbo.common.extension.ExtensionLoader

2.2.1 SPI扩展点的配置

在这里插入图片描述

 /**
     * synchronized in getExtensionClasses
     * */
    private Map<String, Class<?>> loadExtensionClasses() {
        cacheDefaultExtensionName();

        Map<String, Class<?>> extensionClasses = new HashMap<>();
        // internal extension load from ExtensionLoader's ClassLoader first
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);

        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
        loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
        return extensionClasses;
    }
  private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type) {
        loadDirectory(extensionClasses, dir, type, false);
    }

    private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {
        //配置文件的全称:路径前缀+接口的全限定名 ,这个与Java SPI是一直的算法
        String fileName = dir + type;
        try {
            Enumeration<java.net.URL> urls = null;
            ClassLoader classLoader = findClassLoader();
            
            // try to load from ExtensionLoader's ClassLoader first
            if (extensionLoaderClassLoaderFirst) {
                ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                    urls = extensionLoaderClassLoader.getResources(fileName);
                }
            }
            
            if(urls == null || !urls.hasMoreElements()) {
                if (classLoader != null) {
                    urls = classLoader.getResources(fileName);
                } else {
                    urls = ClassLoader.getSystemResources(fileName);
                }
            }

            if (urls != null) {
                while (urls.hasMoreElements()) {
                    java.net.URL resourceURL = urls.nextElement();
                    loadResource(extensionClasses, classLoader, resourceURL);
                }
            }
        } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }

由源码可以看出:

  1. Dubbo SPI配置文件存放的路径是: META-INF/services/ , META-INF/dubbo/,META-INF/dubbo/internal/
  2. 配置文件的名称为接口的全路径名
  3. 配置文件使用key=value的方式,多个就用换行符分割,key是扩展点在Dubbo/spring Ioc容器Ioc容器中的名称,value是扩展点类的全称

2.2.2 SPI扩展点的分类

①. 普通扩展类
在SPI配置文件中配置的扩展类

②. 包装扩展类
这个wrapper类没有具体的实现,它的核心功能就是包装扩展点,实现扩展点的通用逻辑;这个wrapper类的必须在构造发放中传入一个响应的扩展点的实现;

③. 自适应扩展类
一个SPI接口会有多个扩展点,具体使用哪一扩展点可以不用在配置或代码中写死。可以在程序运行时,通过URL[^1]中的某些参数动态来确定。这个也是dubbo最棒的特性之一!

[^1] :在源码org.apache.dubbo.common.URL中 ;官方解释Uniform Resource Locator (Immutable, ThreadSafe) 。

2.2.3 SPI 扩展点的四大特性

  • 自动包装类

    自动包装类使用装饰器模式,通过对原始类包装或者说是增强,在包装类中抽象出通用的逻辑,让被包装的类能更专注于业务具体的实现。
    如何判断一个类是否是Wrapper类,在ExtensionLoader#isWrapperClass 的实现如下:

    /**
     * test if clazz is a wrapper class
     * <p>
     * which has Constructor with given class type as its only argument
     * 
     * 如果扩展点实现的构造函数中只有一个参数,且这个参数的类型为它本身时,这个扩展类会被认为是wrapper类
     */
    private boolean isWrapperClass(Class<?> clazz) {
        try {
            clazz.getConstructor(type);
            return true;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

在Dubbo中包装类都是以Wrapper结尾,如下随即列一个源码:

/**
 * ListenerProtocol
 */
public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;
    //在构造参数中传入一个自己的实例 
    public ProtocolFilterWrapper(Protocol protocol) {
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }

    private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
        ...
        return last;
    }

    @Override
    public int getDefaultPort() {
        return protocol.getDefaultPort();
    }

    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        if (UrlUtils.isRegistry(invoker.getUrl())) {
            return protocol.export(invoker);
        }
        return protocol.export(buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER));
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        if (UrlUtils.isRegistry(url)) {
            return protocol.refer(type, url);
        }
        return buildInvokerChain(protocol.refer(type, url), REFERENCE_FILTER_KEY, CommonConstants.CONSUMER);
    }

    @Override
    public void destroy() {
        protocol.destroy();
    }

    @Override
    public List<ProtocolServer> getServers() {
        return protocol.getServers();
    }

}


这就是一个典型的Wrapper类,它本身实现 Protocol 接口,然后它的构造函数中又必须传入一个Protocol 实例;它里面实现的Protocol 的方法里面实际调用的是构造函数中传如的Protocol 实例的实现;

  • 自动加载
    假如一个扩展点里面的成员属性是另外一个扩展点,并且这个属性有setter方法;那么Dubbo会自动注入对应的扩展点的实例;这个实现在ExtensionLoader#injectExtension(T instance)方法中实现,详细实现如下:
private T injectExtension(T instance) {

        if (objectFactory == null) {
            return instance;
        }

        try {
            /**
             *  实现IOC和AOP的机制,获取到类中所有的setter方法。
             *  1 如果setter方法不是私有的和基础数据类型,
             *  并且没有注解{@link DisableInject}注解,就自动给相关属性注入默认的实现类
             *  2 如果setter方法属性是一个接口,并且次接口有多个实现类,则会更加{@link Adaptive}注解
             *    自适应加载配置的默认实现
             */
            for (Method method : instance.getClass().getMethods()) {
                if (!isSetter(method)) {
                    continue;
                }
                /**
                 * Check {@link DisableInject} to see if we need auto injection for this property
                 */
                if (method.getAnnotation(DisableInject.class) != null) {
                    continue;
                }
                Class<?> pt = method.getParameterTypes()[0];
                if (ReflectUtils.isPrimitives(pt)) {
                    continue;
                }

                try {
                    String property = getSetterProperty(method);
                    Object object = objectFactory.getExtension(pt, property);
                    if (object != null) {
                        method.invoke(instance, object);
                    }
                } catch (Exception e) {
                    logger.error("Failed to inject via method " + method.getName()
                            + " of interface " + type.getName() + ": " + e.getMessage(), e);
                }

            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }

到这里还会有一个问题,假如这个扩展点的属性存在多个实现怎么办? 这个就由下面这个自适应特性来实现。

  • 自适应
    在Dubbo中,自适应的接口方法或接口实现会使用@Adaptive标识。
  1. 标注接口方法时, 通过URL中的参数来动态的决定要使用哪一个具体的实现(方法中本身就有URL对象,或者调用的时候获取调用的URL对象来进行匹配)。
  2. 标注扩展点实现时,会默认这个扩展点实现就是这个接口的实现
    直接上源码
    注解@Adaptive的源码(注意到,只有一个参数,是一个字符串数据),如果英文水平好,直接看注解就知道了其中的实现原理,具体的实现在ExtensionLoader#injectExtension中有体现:
/**
 * Provide helpful information for {@link ExtensionLoader} to inject dependency extension instance.
 *
 * @see ExtensionLoader
 * @see URL
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    /**
     * Decide which target extension to be injected. The name of the target extension is decided by the parameter passed
     * in the URL, and the parameter names are given by this method.
     * <p>
     * If the specified parameters are not found from {@link URL}, then the default extension will be used for
     * dependency injection (specified in its interface's {@link SPI}).
     * <p>
     * For example, given <code>String[] {"key1", "key2"}</code>:
     * <ol>
     * <li>find parameter 'key1' in URL, use its value as the extension's name</li>
     * <li>try 'key2' for extension's name if 'key1' is not found (or its value is empty) in URL</li>
     * <li>use default extension if 'key2' doesn't exist either</li>
     * <li>otherwise, throw {@link IllegalStateException}</li>
     * </ol>
     * If the parameter names are empty, then a default parameter name is generated from interface's
     * class name with the rule: divide classname from capital char into several parts, and separate the parts with
     * dot '.', for example, for {@code org.apache.dubbo.xxx.YyyInvokerWrapper}, the generated name is
     * <code>String[] {"yyy.invoker.wrapper"}</code>.
     *
     * @return parameter names in URL
     */
    String[] value() default {};

}

使用样例代码,这里使用Codec2接口,这是Dubbo消息编码解码的核心接口。

package org.apache.dubbo.remoting;

import org.apache.dubbo.common.extension.Adaptive;
import org.apache.dubbo.common.extension.SPI;
import org.apache.dubbo.remoting.buffer.ChannelBuffer;

import java.io.IOException;

/**
 * Dubbo编解码器顶层抽象接口
 */
@SPI
public interface Codec2 {

    @Adaptive({Constants.CODEC_KEY})
    void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException;

    @Adaptive({Constants.CODEC_KEY})
    Object decode(Channel channel, ChannelBuffer buffer) throws IOException;

    enum DecodeResult {
        NEED_MORE_INPUT, SKIP_SOME_INPUT
    }

}

SPI接口使用@SPI注解,然后接口方法使用@Adaptive注解,这里面只传了一个参数;
当要获取Codec2的扩展点时,
首先会从URL的参数中找到key为这参数(Constants.CODEC_KEY)的值value,然后以这个value作为扩展点的实例的名称去加载这个实例。
具体实现在org.apache.dubbo.remoting.transport.AbstractEndpoint#getChannelCodec(URL url)中有体现:

 protected static Codec2 getChannelCodec(URL url) {
        //从URL参数中,以Constants.CODEC_KEY去找对应的值,最为默认实现类的名称
        String codecName = url.getParameter(Constants.CODEC_KEY, "telnet");
        if (ExtensionLoader.getExtensionLoader(Codec2.class).hasExtension(codecName)) {
            return ExtensionLoader.getExtensionLoader(Codec2.class).getExtension(codecName);
        } else {
            return new CodecAdapter(ExtensionLoader.getExtensionLoader(Codec.class).getExtension(codecName));
        }
    }

假如@Adaptive中有多个参数时,首先会以第一个参数作为key去查找,如果没有找到对应的实现类,就以第二个参数作为key去找,以此类推…
这个特性虽然在使用的过程中非常灵活,但是,对于一个接口,只能激活一个实现。假如一个接口需要同时激活多个扩展点,这个就显得有点不够用了!因此dubbo还提供了下面这个特性自动激活来实现这个业务场景

  • 自动激活

在dubbo中,使用@Activate注解来标识扩展点的实现类。先看注解源码:

/**
 * @Activate,是一个用于自动激活给定条件的某些扩展点实现的注解
 * 例子:  当Filter有多个实现时,@Activate可以用来加载某些Filter扩展
 * <ol>
 * <li>{@link Activate#group()} specifies group criteria. Framework SPI defines the valid group values.
 * <li>{@link Activate#value()} specifies parameter key in {@link URL} criteria.
 * </ol>
 * SPI provider can call {@link ExtensionLoader#getActivateExtension(URL, String, String)} to find out all activated
 * extensions with the given criteria.
 *
 * @see SPI
 * @see URL
 * @see ExtensionLoader
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
    /**
     * Activate the current extension when one of the groups matches. The group passed into
     * {@link ExtensionLoader#getActivateExtension(URL, String, String)} will be used for matching.
     *
     * @return group names to match
     * @see ExtensionLoader#getActivateExtension(URL, String, String)
     */
    String[] group() default {};

    /**
     * Activate the current extension when the specified keys appear in the URL's parameters.
     * <p>
     * For example, given <code>@Activate("cache, validation")</code>, the current extension will be return only when
     * there's either <code>cache</code> or <code>validation</code> key appeared in the URL's parameters.
     * </p>
     *
     * @return URL parameter keys
     * @see ExtensionLoader#getActivateExtension(URL, String)
     * @see ExtensionLoader#getActivateExtension(URL, String, String)
     */
    String[] value() default {};

    /**
     * Relative ordering info, optional
     * Deprecated since 2.7.0
     *
     * @return extension list which should be put before the current one
     */
    @Deprecated
    String[] before() default {};

    /**
     * Relative ordering info, optional
     * Deprecated since 2.7.0
     *
     * @return extension list which should be put after the current one
     */
    @Deprecated
    String[] after() default {};

    /**
     * Absolute ordering info, optional
     *
     * @return absolute ordering info
     */
    int order() default 0;
}

通过源码注解可以知道,实现这个功能核心代码在ExtensionLoader#getActivateExtension(URL, String, String)中,那么下面我们就来看一下这一块代码:

 /**
     * This is equivalent to {@code getActivateExtension(url, url.getParameter(key).split(","), null)}
     *
     * @param url   url
     * @param key   url parameter key which used to get extension point names
     * @param group group
     * @return extension list which are activated.
     * @see #getActivateExtension(org.apache.dubbo.common.URL, String[], String)
     */
    public List<T> getActivateExtension(URL url, String key, String group) {
        String value = url.getParameter(key);
        return getActivateExtension(url, StringUtils.isEmpty(value) ? null : COMMA_SPLIT_PATTERN.split(value), group);
    }

    /**
     * Get activate extensions.
     * 获取所有激活的扩展点
     * @param url    url
     * @param values extension point names
     * @param group  group
     * @return extension list which are activated
     * @see org.apache.dubbo.common.extension.Activate
     */
    public List<T> getActivateExtension(URL url, String[] values, String group) {
        //需要激活的扩展点
        List<T> exts = new ArrayList<>();
        //扩展点的名称,即注解中第二个参数中指定的值
        List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
        /**
         *在dubbo的扩展点的配置,如果配置的扩展点的名称以{@link REMOVE_VALUE_PREFIX}开头,就不会被激活
         */
        if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
            /**
             * 将扩展点类加载到缓存中{@link cachedClasses}
             */
            getExtensionClasses();
            //遍历缓存中的激活的扩展点
            for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
                String name = entry.getKey();
                Object activate = entry.getValue();

                String[] activateGroup, activateValue;

                if (activate instanceof Activate) {
                    //获取@Activate的group参数
                    activateGroup = ((Activate) activate).group();
                    //获取@Activate的value参数
                    activateValue = ((Activate) activate).value();
                } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                    //这个主要是为了兼容Dubbo前期的版本
                    activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                    activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
                } else {
                    continue;
                }
                //匹配group ,value ,来决定是否激活这个扩展点
                if (isMatchGroup(group, activateGroup)
                        && !names.contains(name)
                        && !names.contains(REMOVE_VALUE_PREFIX + name)
                        && isActive(activateValue, url)) {
                    exts.add(getExtension(name));
                }
            }
            exts.sort(ActivateComparator.COMPARATOR);
        }
        List<T> usrs = new ArrayList<>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (!name.startsWith(REMOVE_VALUE_PREFIX)
                    && !names.contains(REMOVE_VALUE_PREFIX + name)) {
                if (DEFAULT_KEY.equals(name)) {
                    if (!usrs.isEmpty()) {
                        exts.addAll(0, usrs);
                        usrs.clear();
                    }
                } else {
                    usrs.add(getExtension(name));
                }
            }
        }
        if (!usrs.isEmpty()) {
            exts.addAll(usrs);
        }
        return exts;
    }

     /**
     * 匹配group
     * @param group 服务的group
     * @param groups 注解@Activate中的group字符串数组
     * @return true 匹配成功,false失败
     */
    private boolean isMatchGroup(String group, String[] groups) {
        //如果group是空,默认为要激活
        if (StringUtils.isEmpty(group)) {
            return true;
        }
        if (groups != null && groups.length > 0) {
            for (String g : groups) {
                if (group.equals(g)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 匹配key
     * @param keys {@link @Active}注解中配置的参数,一个或者多个
     * @param url SPI实现的核心对象URL
     * @return true 表示是自动激活扩展点,false不是
     */
    private boolean isActive(String[] keys, URL url) {
        //如果keys没有指定,默认表示要激活
        if (keys.length == 0) {
            return true;
        }
        for (String key : keys) {
            // @Active(value="key1:value1, key2:value2")
            String keyValue = null;
            if (key.contains(":")) {
                String[] arr = key.split(":");
                key = arr[0];
                keyValue = arr[1];
            }

            for (Map.Entry<String, String> entry : url.getParameters().entrySet()) {
                String k = entry.getKey();
                String v = entry.getValue();
                if ((k.equals(key) || k.endsWith("." + key))
                        && ((keyValue != null && keyValue.equals(v)) || (keyValue == null && ConfigUtils.isNotEmpty(v)))) {
                    return true;
                }
            }
        }
        return false;
    }

2.2.4 SPI扩展点的缓存

①. 扩展点Class缓存 ,Dubbo SPI在获取扩展点时,会优先从缓存中读取,如果缓存中不存在则加载配置文件,根据配置将Class缓存到内存中,并不会直接初始化。
②. 扩展点实例缓存 ,Dubbo不仅会缓存Class,还会缓存Class的实例。每次取实例的时候会优先从缓存中取,取不到则从配置中加载并缓存到内存中。这个就是典型的使用空间换时间的做法。也是Dubbo性能强劲的原因之一

以上只列举了主要的,这里直接上源码:

    /**
     * 扩展类加载器缓存,就是扩展点ExtendsLoader实例缓存; key=扩展接口 value=扩展类加载器
     */
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
    /**
     * 扩展实例存入内存中缓存起来; key=扩展类 ; value=扩展类实例
     */
    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
  
    /**
     * 扩展名缓存 key=扩展类 ; value=扩展名
     */
    private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
    /**
     * 扩展点Class缓存
     */
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
    /**
     * 默认激活的扩展点的缓存
     */
    private final Map<String, Object> cachedActivates = new ConcurrentHashMap<>();
    /**
     * 扩展点实例缓存 key=扩展点名 value=扩展对象
     */
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
    /**
     * 自适应的扩展点实例缓存
     */
    private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
    /**
     * 配置有自动激活的接口的Class缓存
     */
    private volatile Class<?> cachedAdaptiveClass = null;
    /**
     *默认缓存的名称
     */
    private String cachedDefaultName;
    /**
     * Wrapper类缓存
     */
    private Set<Class<?>> cachedWrapperClasses;

这个将在下一节研读ExtensionLoader源码的时候会做详细介绍

2.3 ExtensionLoader原理解析

文章链接

2.4 ExtensionLoader相关技术源码解析

文章链接

3 整体设计原理

前面两章讲了Dubbo框架的架构设计和核心的实现技术细节,从这一章开始将深入解读Dubbo各个模块的实现原理和关系。

3.1 总体设计

Dubbo官网原文链接

这是dubbo项目中Consumer调用Providers接口服务的整体流程分析
在这里插入图片描述

发布了12 篇原创文章 · 获赞 1 · 访问量 655

猜你喜欢

转载自blog.csdn.net/zhaodongchao1992/article/details/103295584
今日推荐