【爆肝两万字 收藏向】从用法到源码,一篇文章让你精通Dubbo的SPI机制(2)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 【爆肝两万字 收藏向】从用法到源码,一篇文章让你精通Dubbo的SPI机制

2. wrapper 与 setter 特性

(1)如何使用

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

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 十分相似

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方法来判定其成员。

//实例化扩展点时,会判断有没有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 注解

使用方式

a3aebf526fa04cef8f8abf5f21040611.png

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

f12b0264c4b841c2b456ab3f3c9aaf71.png


c49f4463320b40368d023ecf5a4260b7.png

实现原理

// 加载某个扩展点的所有配置,所谓配置,其实就是<简称,类名>这种配置
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类,则并不固定使用哪个实现类来真正执行方法,所以叫自适应

0c516f557e8045b3ab5c7f11e43dc30d.png

②@Adaptive如果没有出现在实现类上,那么Duubo会在我们指定要自适应实现类的时候,给我们自己创建个自适应实现类,该实现类的模板为SPI接口本身,此时所有的方法都没有实现,仅有源码方法上有@Adaptive的部分方法才会帮你编译点方法内容,帮你解析下入参,从入参里解析出另一个实现类来执行

8878c7236a114e2ab27eaadc32cdd2c5.png


实现原理

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

// 扫描所有目录下的配置时,会对提到的各实现类进行解析(注意,仅仅是分析类信息,并没有实例化)
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的实现类,那就懒加载,直到有人要获取该扩展点的自适应实现类时再进行创建

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


c4b01b22e6bb4958b44b5d5de216f773.png

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



39c35c22a88b48d19bd010b9cc38d978.png

(2)@Activate 注解

2659b26e19b7463e98d777cd1b22e1c4.png


使用方式

@Activate注解用于标记一个扩展点实现类在什么条件下可用。通过该注解,我们可以指定这个扩展点的执行顺序、条件等,从而实现对扩展点的控制

@Activate注解可以用在四个地方:在扩展点接口上。在扩展点实现类上。在扩展点实现类的构造方法上。在扩展点实现类的构造方法参数上。

@Activate注解需要指定三个属性:group:表示扩展点所属的分组。如果不指定,则默认为全局分组,即所有扩展点都属于该分组。value:表示是否需要激活或禁用扩展点简称,允许填多个。order:表示扩展点的执行顺序,值小的先执行。如果不指定,则默认为0。

2e6fed4a411040258de60167770775dc.png

如上图,Filter的一个实现类CacheFilter,类上就标注了@Aactivate 注解,当我们用下面的方法去取实现类时,就会返回CacheFilter

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

原理解析

// 三个重载方法,入参各不相同
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,因此强烈建议进行收藏,而后时常复习,相信很快就融会贯通了


目录
相关文章
|
2月前
|
Dubbo Java 应用服务中间件
Dubbo服务暴露机制解密:深入探讨服务提供者的奥秘【九】
Dubbo服务暴露机制解密:深入探讨服务提供者的奥秘【九】
73 0
|
2月前
|
缓存 Dubbo Java
Dubbo 第三节_ Dubbo的可扩展机制SPI源码解析
Dubbo会对DubboProtocol对象进⾏依赖注⼊(也就是⾃动给属性赋值,属性的类型为⼀个接⼝,记为A接⼝),这个时候,对于Dubbo来说它并不知道该给这个属性赋什么值,换句话说,Dubbo并不知道在进⾏依赖注⼊时该找⼀个什么的的扩展点对象给这个属性,这时就会预先赋值⼀个A接⼝的⾃适应扩展点实例,也就是A接⼝的⼀个代理对象。在调⽤getExtension去获取⼀个扩展点实例后,会对实例进⾏缓存,下次再获取同样名字的扩展点实例时就会从缓存中拿了。Protocol是⼀个接。但是,不是只要在⽅法上加了。
|
2月前
|
设计模式 JSON Dubbo
超越接口:探索Dubbo的泛化调用机制
超越接口:探索Dubbo的泛化调用机制
101 0
|
2月前
|
Dubbo 网络协议 应用服务中间件
分布式微服务框架dubbo原理与机制
分布式微服务框架dubbo原理与机制
|
2月前
|
XML 缓存 Dubbo
Dubbo的魔法之门:深入解析SPI扩展机制【八】
Dubbo的魔法之门:深入解析SPI扩展机制【八】
48 0
|
2月前
|
XML 负载均衡 Dubbo
了解Dubbo配置:优先级、重试和容错机制的秘密【五】
了解Dubbo配置:优先级、重试和容错机制的秘密【五】
99 0
|
2月前
|
Dubbo Java 应用服务中间件
微服务框架(十七)Dubbo协议及编码过程源码解析
  此系列文章将会描述Java框架Spring Boot、服务治理框架Dubbo、应用容器引擎Docker,及使用Spring Boot集成Dubbo、Mybatis等开源框架,其中穿插着Spring Boot中日志切面等技术的实现,然后通过gitlab-CI以持续集成为Docker镜像。   本文为Dubbo协议、线程模型、和其基于Netty的NIO异步通讯机制及源码
|
2月前
|
Dubbo Java 应用服务中间件
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
|
2月前
|
Dubbo Java 应用服务中间件
阿里巴巴资深架构师深度解析微服务架构设计之SpringCloud+Dubbo
软件架构是一个包含各种组织的系统组织,这些组件包括Web服务器,应用服务器,数据库,存储,通讯层),它们彼此或和环境存在关系。系统架构的目标是解决利益相关者的关注点。
|
2月前
|
Dubbo Cloud Native 应用服务中间件
【阿里云云原生专栏】云原生环境下的微服务治理:阿里云 Dubbo 与 Nacos 的深度整合
【5月更文挑战第25天】阿里云Dubbo和Nacos提供微服务治理的强大工具,整合后实现灵活高效的治理。Dubbo是高性能RPC框架,Nacos则负责服务发现和配置管理。整合示例显示,通过Nacos注册中心,服务能便捷注册发现,动态管理配置。简化部署,提升适应性,但也需注意服务稳定性和策略规划。这种整合为云原生环境的微服务架构带来强大支持,未来应用前景广阔。
223 2