从用法到源码,一篇文章让你精通Dubbo的SPI机制

前言

我们之前说过SPI机制,不仅谈到过SPI与API的区别,也讲了JAVA中的SPI机制实例。其实一切都是为了今天作铺垫,没错,今天我们要来讲讲Dubbo的重要设计------Dubbo-SPI机制。这一次直接爆肝,从示例到源码,力求讲清讲透。事不宜迟,现在就开始我们的学习吧.....


一、Dubbo为什么要自己实现SPI

其实这个问题应该分成两部分:

  1. Dubbo为什么要用SPI机制
  2. Dubbo为什么不用原生的JAVA-SPI实现

1. Dubbo为什么需要SPI机制

要回答这个问题,我们必须先复习一遍SPI机制的特点与好处:==解耦合、扩展性强、兼容性好==

但我们如果仅使用Dubbo而不做任何改造的话,SPI的作用其实就相当于策略模式,一个接口内置了多个实现,可以根据入参或配置选择一个实现类。如果仅是如此,Dubbo的SPI机制显得就完全多余,可以由设计模式来替代了。

==所以。Dubbo使用SPI机制的主要原因是为了实现可插拔的扩展性。== 具体来说,Dubbo 的 SPI 机制不仅可以使内置的几种实现类能灵活选用,还可以让用户通过配置文件或者注解的方式,自定义实现某个接口的实现类 ,然后在运行时自动通过 SPI 机制来加载并实例化对应的实现类。这样可以大大提高 Dubbo 的灵活性和可扩展性,同时也方便了 Dubbo 的用户进行自定义定制。==在 Dubbo 中,所有内部实现和第三方实现都是平等的,用户可以基于自身业务需求,替换 Dubbo 提供的原生实现。==

2. 为什么不用原生的JAVA-SPI实现

知道了Dubbo的SPI机制主要是为了可插拔的扩展性,那为什么不直接用JAVA自带的SPI机制呢,主要其实还是JAVA-SPI无法满足Dubbo的设计意图:

  1. JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源
  2. 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  3. 原生的SPI,获得实现类后,不支持dubbo的SPI机制增加了对IOC、AOP的支持,一个扩展点可以直接通过setter注入到其他扩展点。

二、Dubbo 到底哪些地方定了SPI

1. 官网介绍

我们说Dubbo 是为了支持可插拔的扩展,那么我们先来看看,Dubbo到底有哪些地方能支持扩展?我们在官网看到了下图:

  • 协议与编码扩展:通信协议、序列化编码协议等
  • 流量管控扩展:集群容错策略、路由规则、负载均衡、限流降级、熔断策略等
  • 服务治理扩展:注册中心、配置中心、元数据中心、分布式事务、全链路追踪、监控系统等
  • 诊断与调优扩展:流量统计、线程池策略、日志、QoS 运维命令、健康检查、配置加载等

2. 代码查阅

我们以其中的协议 Protocol 为例来看看。可以看出Protocol 为其定义在rpc包下的一个接口,而其内置的实现类数量也很多。有通用的Http协议、自研的Dubbo协议,java内置的Rmi协议,当然也支持其他框架的协议如thrift、grpc。

再比如让调用方、被调用方注册的注册中心,也有多种容器可选,如常见的Redis、Zookeeper、Nacos以及自己的Dubbo注册中心 可以说,Dubbo几乎是每个关键部件都提供了扩展的功能。我们过往会有很多公司有自研框架,自研协议。这种场景使用Dubbo,就只需要在有限的几个地方对接好其 SPI,就能很轻松的用上Dubbo,这对于开发者和架构来说,是个非常实用的设计。

三、Dubbo - SPI的使用及原理

1. JAVA 与 SPI 使用方式对比

我们先复习一遍JAVA 的原生SPI使用方式

  • JAVA 的原生使用
  1. 在jar包的 META-INF/services/ 下填入我们对SPI的实现类的全限定名,如 com.mysql.jdbc.Driver
  2. 在代码中使用 ServiceLoader loader = ServiceLoader.load(Driver.class); 就能获得一个加载了所有驱动实现类的对象
  • Dubbo中的使用. Dubbo中的使用较为复杂,我们先来看它最基本的用法
  1. 如果我们自定义了RPC协议,那就在 META-INF/dubbo/org.apache.dubbo.rpc.Protocol 文件里填上我们的协议类的简称和它的全限定名,如myprotocol=com.zhanfu.samples.protocol.MyProtocol
  2. 在代码中使用 Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myprotocol"); 就能准确获取到我们指定的协议
  3. 在代码中使用 Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); 就能获取到自适应指定的的协议

可以看到,Dubbo 实现 SPI机制的核心是 ExtensionLoader,它取代了 JDK 自带的 ServiceLoader,我们慢慢来看其用法及实现原理

2. 配置文件

(1)配置规则

  • Dubbo 规定的三个配置目录分别为 META-INF/services/META-INF/dubbo/internal/META-INF/dubbo/,我们注意到 META-INF/services/ 其实就是原生JAVA - SPI 使用的目录、META-INF/dubbo/internal/则是Duubo自己内部实现的一些扩展配置,其实三个目录本身没有功能的区别,默认是全部都扫描的,一般我们建议将配置文件放在META-INF/dubbo/
  • 配置文件的文件名:必须是扩展点的全限定名,比如我们想使用自定义的协议,即protocol,我们就需要在上述某个目录下加上名为 org.apache.dubbo.rpc.Protocol 的文件
  • 文件内容:采用"简称 = 类名"的形式,一行一个,比如 http=org.apache.dubbo.rpc.protocol.http.HttpProtocol
  • 示例:下图就是Dubbo类型转换器的扩展配置

(2)加载配置与解析的原理

参考过去的JAVA原生的ServiceLoader,以及 getExtensiongetAdaptiveExtension 两个方法,我们不难得出,扩展加载器至少有这么几个功能:

  1. 加载并保存各个扩展组件
  2. 能按照简称获得指定的扩展实现类
  3. 能提供默认的扩展实现类

需要注意的是,==一个ExtensionLoader实例只包含一个扩展点,比如"协议"扩展点,那么该实例中,就只会加载"协议"接口的实现类 ==

其实其中的 1 和 2,相当于原生SPI的略微改动,原生JAVA的SPI 会在启动后实例化所有扩展实现类,并保存。而 Dubbo 则是启动后保存<简称 ,类信息>的Map,你给定某个简称,我再为你实例化某个实现类

那么其具体如何做的,我们直接来看其源码实现(默认实现类又是什么呢?我们又该如何指定默认实现类呢?这些问题请看@SPI部分)

java 复制代码
// 按照指定简称,返回实现类
public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    // 当传入的名称为"true"时,获取默认实现类   
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
            	// 为指定简称创建实现类
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}
private T createExtension(String name) {
	// 先获取指定简称在配置文件中对应的类
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
        	// 实例化实现类
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 如果该实现类内引用了其他扩展点,则自动为其注入扩展点,类似于Spring体系下的IOC
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        // 如果设定了装饰器类,则把实现类扔进这个装饰器,再把装饰器实例返回,如果有多个装饰器类,就会造成层层套壳
        // 与Spring中的AOP类似,但此处 wrapperClass 存在 ConcurrentHashSet,无法指定套壳的顺序
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            for (Class<?> wrapperClass : wrapperClasses) {
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                type + ")  could not be instantiated: " + t.getMessage(), t);
    }
}
// 获取加载扩展点<简称,类>的映射
private Map<String, Class<?>> getExtensionClasses() {
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

// 从配置文件中加载扩展点的信息
private Map<String, Class<?>> loadExtensionClasses() {
    // 获取本扩展点的默认实现
    cacheDefaultExtensionName();
    Map<String, Class<?>> extensionClasses = new HashMap<>();
    // 遍历策略,加载所有目录下指定的扩展点的信息。
    // 注意,只会扫描文件末尾是扩展点名的文件,比如"协议"扩展点,只会扫描类似 org.apache.dubbo.rpc.Protocol 文件
    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }
    return extensionClasses;
}

其中策略有三个实现类

分别对应着下列目录

  • META-INF/services/
  • META-INF/dubbo/internal/
  • META-INF/dubbo/

所以,至此,我们已经明白了其实说到底,还是从上述三个目录中获取某个扩展点的所有实现情况。 而其具体从配置文件解析的过程,源码如下

java 复制代码
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                       boolean overridden) throws NoSuchMethodException {
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                type + ", class line: " + clazz.getName() + "), class "
                + clazz.getName() + " is not subtype of interface.");
    }
    // 实现类上有@Adaptive注解,表示这个实现类就为自适应实现类,详见第3小节
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz, overridden);
    // 实现类上有带一个同类型入参的构造方法,表示这个实现类就为装饰器类,详见第2小节
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
    // 普通的实现类
        clazz.getConstructor();
        if (StringUtils.isEmpty(name)) {
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }

        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                cacheName(clazz, n);
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

可以看到,都是实现类,却也有不同的情况。可以是普通实现类,可以是装饰器类,也可以是自适应实现类,后两者我们在下面来讲。

2. wrapper 与 setter 特性

(1)如何使用

wrapper: 我们自定义的某个实现类内,如果有构造方法的入参是传入一个同类型的对象,那么即可认定这个实现类其实是一个装饰器,其实际功能由内置的那个对象来解决,该实现类本身仅相当于一个外壳,执行一些其他功能,如打印日志等。这实际上与Spring里面的功能增强类似

java 复制代码
public class DubboProtocolWrapper implements Protocol {
    private Protocol protocol;
    // 构造方法入参是一个Protocol类型,将会被视作一个装饰器
    public DubboProtocolWrapper(Protocol protocol) {
        this.protocol = protocol;
    }
    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
    	log.debug("现在开始要导出了");
        return protocol.export(invoker);
    }
    // Protocol 接口的其他方法
    ......
}

Setter: 我们自定义的某个实现类内,如果有方法是以"set"作为前缀,比如是 setProtocol ,并且方法有一个入参且没有 @DisableInject 注解,那么就会判断该类入参是否也是一个SPI,如果是就会委托一个 ExtensionFactory 为你去找其实现类并注入,至于寻找哪个实现类,则与你ExtensionFactory的类型有关(没错,ExtensionFactory是个接口,其本身也遵循SPI接口的设计,Dubbo也为你准备了三个实现类)。这一步骤其实与Spring 的 IOC 十分相似

java 复制代码
public class MyCluster implements Cluster{
	private Protocol protocol;
	// 当本类被dubbo实例化时,会执行该方法,主动为我们寻找protocol的实现类并注入
	public void setProtocol(Protocol protocol) {
		this.protocol = protocol;
	}
    // Cluster 接口的其他方法
    ......
}

(2)实现原理

在实例化扩展点的代码中,我们可以看到有以下两个处理:

  • wrapper 如果扩展点实现类有拷贝构造函数,则认为是包装类。包装类的作用是对其他扩展点实现类进行包装,通过包装类可以把所有扩展点的公共逻辑移到包装类,类似AOP。
  • setter 扩展点实现类的成员如果为其它扩展点类型,ExtensionLoader 在会自动注入依赖的扩展点。ExtensionLoader 通过扫描扩展点实现类的所有set方法来判定其成员。
java 复制代码
//实例化扩展点时,会判断有没有wrapper装饰器,有装饰器,需要拿装饰器套壳上去,并返回装饰器实例
private T createExtension(String name) {
        ......
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            for (Class<?> wrapperClass : wrapperClasses) {
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
        ......
}
//实例化扩展点时,如果实例里有其他扩展点,则也许注入扩展点
private T injectExtension(T instance) {
     if (objectFactory == null) {
         return instance;
     }

     try {
         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;
 }

    /**
     * return true if and only if:
     * <p>
     * 1, public
     * <p>
     * 2, name starts with "set"
     * <p>
     * 3, only has one parameter
     */
    private boolean isSetter(Method method) {
        return method.getName().startsWith("set")
                && method.getParameterTypes().length == 1
                && Modifier.isPublic(method.getModifiers());
    }

3. 注解 @SPI @Adaptive 和 @Activate

(1)@SPI 注解

  • 使用方式 @SPI注解使用在接口上,作用时为该接口指定默认实现类 ,其属性 value 是默认实现的简称,因为是直接标在Dubbo的接口上的,除非我们修改Dubbo的源码,否则这个值就是Dubbo 内置好的(当然你也可以自己定义一个接口,给接口加上该注解)。比如下图,默认的RPC协议就是"dubbo"协议。

  • 实现原理

java 复制代码
// 加载某个扩展点的所有配置,所谓配置,其实就是<简称,类名>这种配置
private Map<String, Class<?>> loadExtensionClasses() {
	// 找到并设置本扩展点的默认实现
    cacheDefaultExtensionName();
    Map<String, Class<?>> extensionClasses = new HashMap<>();

    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }
    return extensionClasses;
}

private void cacheDefaultExtensionName() {
	// 如果指定扩展点接口带@SPI注解,且注解中有简称,则把该简称作为默认实现
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation == null) {
         return;
    }
    String value = defaultAnnotation.value();
    if ((value = value.trim()).length() > 0) {
        String[] names = NAME_SEPARATOR.split(value);
        if (names.length > 1) {
            throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
                        + ": " + Arrays.toString(names));
        }
        if (names.length == 1) {
            cachedDefaultName = names[0];
        }
    }
}

(2)@Adaptive 注解

  • 使用方式@Adaptive注解一般在实现类上,表明该类自适应,自适应是什么意思呢?就是其某些方法自己并不实现,而是根据调用该方法时的入参,再去选另一个实现类来执行,相当于一个套壳,但和上面的装饰器类 wrapper 对比,上面的wrapper 会指定某固定类来执行方法,其本身执行别的功能,而本套壳Adaptive类,则并不固定使用哪个实现类来真正执行方法,所以叫自适应 @Adaptive如果没有出现在实现类上,那么Duubo会在我们指定要自适应实现类 的时候,给我们自己创建个自适应实现类 ,该实现类的模板为SPI接口本身,此时所有的方法都没有实现,仅有源码方法上有@Adaptive的部分方法才会帮你编译点方法内容,帮你解析下入参,从入参里解析出另一个实现类来执行

  • 实现原理@Adaptive注解在实现类上时,会在实现类被解析时,就存储起来

java 复制代码
// 扫描所有目录下的配置时,会对提到的各实现类进行解析(注意,仅仅是分析类信息,并没有实例化)
private volatile Class<?> cachedAdaptiveClass = null;

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                           boolean overridden) throws NoSuchMethodException {
        ......
        // 如果类上有 @Adaptive 注解,则表明该类为自适应类, 会存储进变量 cachedAdaptiveClass 中
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            cacheAdaptiveClass(clazz, overridden);
        }
        ......
}

如果没有指定现成的带@Adaptive的实现类,那就懒加载,直到有人要获取该扩展点的自适应实现类时再进行创建

java 复制代码
private volatile Class<?> cachedAdaptiveClass = null;

// 获取本扩展点的自适应实现类
private Class<?> getAdaptiveExtensionClass() {
    getExtensionClasses();
    // 判断有没有指定的自适应实现类,如果没有则新建。
    // 在前面扫描的时候,如果扫到了某个实现类上标注了@Adaptive,此时就会有自适应类
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // 如果没有现成的自适应类,dubbo则会为我们创建
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

private Class<?> createAdaptiveExtensionClass() {
	// 根据SPI接口的信息,自编一个所谓的自适应实现实例。注意此处的cachedDefaultName,这代表着如果自适应没适应成功,还可以使用兜底的实现类来执行
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    ClassLoader classLoader = findClassLoader();
    org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    return compiler.compile(code, classLoader);
}

// 创建自适应实现类时,帮其完善方法
private String generateMethodContent(Method method) {
    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    // 如果接口方法上没有 @Adaptive注解,直接在方法体内写一个Unsupported异常
    if (adaptiveAnnotation == null) {
        return generateUnsupported(method);
    } else {
     	// 如果接口方法上有 @Adaptive注解
     	
        int urlTypeIndex = getUrlTypeIndex(method);

        // 如果方法入参不含有URL类型,方法体内写抛异常
        if (urlTypeIndex != -1) {
            // Null Point check
            code.append(generateUrlNullCheck(urlTypeIndex));
        } else {
            // did not find parameter in URL type
            code.append(generateUrlAssignmentIndirectly(method));
        }
		// 此处实现较为繁琐,我们直接看其最后生成的类,进行反编译
        String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
        boolean hasInvocation = hasInvocationArgument(method);
        code.append(generateInvocationArgumentNullCheck(method));
        code.append(generateExtNameAssignment(value, hasInvocation));
        // check extName == null?
        code.append(generateExtNameNullCheck(value));
        code.append(generateExtensionAssignment());
        // return statement
        code.append(generateReturnAndInvocation(method));
    }
    return code.toString();
}

如我们上述提到的 Protocol ,其接口是这样的,有两个方法带了@Adaptive

将其编造的自适应实现类,反编译,得到

(2)@Activate 注解

  • 使用方式 @Activate注解用于标记一个扩展点实现类在什么条件下可用 。通过该注解,我们可以指定这个扩展点的执行顺序、条件等,从而实现对扩展点的控制 @Activate注解可以用在四个地方:在扩展点接口上。在扩展点实现类上。在扩展点实现类的构造方法上。在扩展点实现类的构造方法参数上。 @Activate注解需要指定三个属性:group:表示扩展点所属的分组。如果不指定,则默认为全局分组,即所有扩展点都属于该分组。value:表示是否需要激活或禁用扩展点简称,允许填多个。order:表示扩展点的执行顺序,值小的先执行。如果不指定,则默认为0。 如上图,Filter的一个实现类CacheFilter,类上就标注了@Aactivate 注解,当我们用下面的方法去取实现类时,就会返回CacheFilter
java 复制代码
public class Consumer {
    public static void main(String[] args) {
        // 获取扩展点工厂
        ExtensionLoader<TestExtension> extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
        // 获取Filter的扩展点实现类:
        // 1. 此处我们没有指明group,就不会因为group的原因筛除任何实现类,
        // 2. 但我们又指定了"cache", 因此简称为 cache 的Filter的实现类一定会加入返回结果列表里
        // 3. 我们指定了url,意味着对于其他的实现类,都要经过url校验,校验匹配才会加入返回结果列表里
        List<TestExtension> extensions = extensionLoader.getActivateExtension(URL.valueOf("test://localhost/test?key1=value1&key2=value2"), "cache");
        // 调用扩展点方法
        for (TestExtension extension : extensions) {
            extension.test();
        }
    }
}
  • 原理解析
java 复制代码
// 三个重载方法,入参各不相同
public List<T> getActivateExtension(URL url, String key) {
    return getActivateExtension(url, key, null);
}

public List<T> getActivateExtension(URL url, String[] values) {
    return getActivateExtension(url, values, null);
}

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);
}

public List<T> getActivateExtension(URL url, String[] values, String group) {
    List<T> activateExtensions = new ArrayList<>();
    // 把values里提到的简称都集合起来
    List<String> names = values == null ? new ArrayList<>(0) : asList(values);
    if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
        getExtensionClasses();
        // 遍历所有带有@Activate的实现类
        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 和 value 信息
                activateGroup = ((Activate) activate).group();
                activateValue = ((Activate) activate).value();
            } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
            } else {
                continue;
            }
            // 当遍历到一些没有明确想要,也没有明确排除的实现类,要进行判断:
            // 它必须满足组的要求,必须满足url的校验,才能算作校验通过
            if (isMatchGroup(group, activateGroup)
                    && !names.contains(name)
                    && !names.contains(REMOVE_VALUE_PREFIX + name)
                    && isActive(activateValue, url)) {
                activateExtensions.add(getExtension(name));
            }
        }
        // 按照order排序
        activateExtensions.sort(ActivateComparator.COMPARATOR);
    }
    List<T> loadedExtensions = 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 (!loadedExtensions.isEmpty()) {
                    activateExtensions.addAll(0, loadedExtensions);
                    loadedExtensions.clear();
                }
            } else {
                loadedExtensions.add(getExtension(name));
            }
        }
    }
    if (!loadedExtensions.isEmpty()) {
        activateExtensions.addAll(loadedExtensions);
    }
    return activateExtensions;
}

// 通过url 和 @Activate 里的value属性值来判断是否匹配
// 假设,现在入参的url 为 "test://localhost/test?myname=zhanfu&age=18"
// 而我们有一个扩展点实现类上注解了
// @Activate(
//      value = {"myname:zhanfu"},
//      group = {"test"},
//      order = 1
// )
// 此时两者都指定了myname这个属性,且属性值都为zhanfu,这就算匹配上了
private boolean isActive(String[] keys, URL url) {
    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;
}

四、总结

现在我们已经知道了,dubbo-spi 实现了比 java-spi 更详尽的功能,随之而来的,肯定也是更高的学习成本。总结一下:

如果你实现了某个扩展点,配置文件需放在这三个目录下

  • META-INF/services/
  • META-INF/dubbo/internal/
  • META-INF/dubbo/

然后使用 ExtensionLoader.getExtensionLoader 得到扩展点加载器后,有三种获得实现类的方式

  • extensionLoader.getExtension(name) : 获取指定实现类
  • extensionLoader.getAdaptiveExtension : 获取自适应实现类
  • extensionLoader.getActivateExtension(url, values, group):按筛选条件获取一批实现类

另外 dubbo-spi 还有两个重要特性,与Spring容器相似,就是dubbo-spi支持

  • 功能增强,允许构建装饰器类(与代理类似)来执行额外功能,真正的功能调用其他类实现
  • 控制反转,会在构建一个实现类时,自动帮你注入其他实现类

上文我们已经对Dubbo的SPI机制进行了详尽的介绍,它的概念和基础使用还是十分简单的。但关于几个注解理解起来有一定复杂度,不过万事开头难,倒也不必惧怕,作为Dubbo的基础能力,如果本章内容不能牢牢掌握,则难以灵活使用和配置Dubbo,因此强烈建议进行收藏,而后时常复习,相信很快就融会贯通了

相关推荐
向阳121812 小时前
Dubbo负载均衡
java·运维·负载均衡·dubbo
一叶飘零_sweeeet2 天前
Dubbo 构建高效分布式服务架构
分布式·架构·dubbo
webfunny20205 天前
前端埋点系统之如何用heatmap.js画网页热力图
前端·javascript·dubbo
菜鸟起航ing7 天前
Apache Dubbo (RPC框架)
rpc·apache·dubbo
飞升不如收破烂~7 天前
包括 Nginx、Gateway、Nacos、Dubbo、Sentinel、RocketMQ 和 Seata 的调用链路描述:
nginx·gateway·dubbo
纳贤猫NXM.COM8 天前
从0到1,解读安卓ASO优化!
android·python·django·flask·dubbo·tornado·dash
羽羽Ci Ci9 天前
模态框登录bootstrap
前端·bootstrap·dubbo
Java Fans11 天前
使用Python自动化高德地图/百度地图导航并提取公里数
python·自动化·dubbo
百度Geek说14 天前
ClickHouse在百度MEG数据中台的落地和优化
clickhouse·百度·dubbo
TracyCoder12324 天前
Dubbo快速入门(二):第一个Dubbo程序(附源码)
dubbo