暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【Dubbo专题】Dubbo SPI 之 Adaptive 自适应类

非普通程序员 2018-09-25
276

翻看 Dubbo
的源码,不难发现,框架到处都在用 SPI
机制进行扩展。这是由于 Dubbo
框架对各种层做了很多的实现方式,然后由用户自己去选择具体的实现方式。比如 Protocol
Dubbo
提供的实现就有 dubbo
hessian
thrift
redis
inJvm
等等。这么多的实现方式, Dubbo
是如何做到动态的切换的呢?

接口自适应与 @Adaptive 注解

Dubbo
没有采用 JDK
提供的 SPI
机制,而是自己实现了一套,增强了很多功能。具体细节可以浏览网上其他博客。

Dubbo
充分利用面向对象思想,每一个组件内引入其他组件都是以接口的形式进行依赖,动态的 inject 实现类。所以这种思想上也用到了 AOP
DI
的思想。 我们定义一个接口 Registry
,假定它的功能是将本地服务暴露到注册中心,以及从注册中心获取可用服务,属于 RPC
框架中的服务注册与发现组件。

  1. @SPI("zookeeper")

  2. public interface Registry {

  3.    /**

  4.     * 注册服务

  5.     */

  6.    @Adaptive()

  7.    String register(URL url, String content);

  8.    /**

  9.     * 发现服务

  10.     */

  11.    @Adaptive()

  12.    String discovery(URL url, String content);

  13. }

  • @SPI
     注解标注这是一个可扩展的组件,注解内的内容后文详细介绍。

  • 该接口定义了两个方法 register
    和 discovery
    ,分别代表注册服务和发现服务。两个方法中都有 URL
     参数,该类是 Dubbo
     内置的类,代表了 Dubbo
     整个执行过程中的上下文信息,包括各类配置信息,参数等。 content
     代表备注信息。

  • @Adaptive()
    注解表明这两个方法都是自适应方法,具体作用后文分析。

下面分别是两个实现类 ZookeeperRegistry
EtcdRegistry

ZookeeperRegistry

  1. public class ZookeeperRegistry implements Registry {

  2.    private Logger logger = LoggerFactory.getLogger(ZookeeperRegistry.class);


  3.    @Override

  4.    public String register(URL url, String content) {

  5.        logger.info("服务: {} 已注册到zookeeper上,备注: {}", url.getParameter("service"), content);


  6.        return "Zookeeper register already! ";

  7.    }


  8.    @Override

  9.    public String discovery(URL url, String content) {

  10.        logger.info("zookeeper上发现服务: {} , 备注: {}", url.getParameter("service"), content);


  11.        return "Zookeeper discovery already! ";

  12.    }

  13. }

EtcdRegistry

  1. public class EtcdRegistry implements Registry {


  2.    private Logger logger = LoggerFactory.getLogger(ZookeeperRegistry.class);


  3.    @Override

  4.    public String register(URL url, String content) {

  5.        logger.info("服务: {} 已注册到 Etcd 上,备注: {}", url.getParameter("service"), content);


  6.        return "Etcd register already! ";

  7.    }


  8.    @Override

  9.    public String discovery(URL url, String content) {

  10.        logger.info("Etcd 上发现服务: {} , 备注: {}", url.getParameter("service"), content);


  11.        return "Etcd discovery already! ";

  12.    }

  13. }

我们可以将服务注册信息注册到老牌注册中心 zookeeper
上,或者使用新兴流行轻量级注册中心 etcd
上。

配置扩展点实现信息到 Resource
 目录下

Resource
下的 META-INF.dubbo
下新建 以 Registry
全限定名为名的文件,配置实现类信息,以 key-value
的形式。(这里要注意与 JDK
默认的 SPI
机制的区别)

文件内内容如下:

  1. etcd=com.maple.spi.impl.EtcdRegistry

  2. zookeeper=com.maple.spi.impl.ZookeeperRegistry

测试 DubboSPI
 自适应类

  1. public class Main {

  2.    public static void main(String[] args) {

  3.        URL url = URL.valueOf("test://localhost/test")

  4.                      .addParameter("service", "helloService");


  5.        Registry registry = ExtensionLoader.getExtensionLoader(Registry.class)

  6.                                           .getAdaptiveExtension();

  7.        String register = registry.register(url, "maple");


  8.        System.out.println(register);

  9.    }

  10. }

该程序首先通过 Registry
接口得到它专属的 ExtensionLoader
实例,然后调用 getAdaptiveExtension
拿到该接口的自适应类。 Dubbo
会判断是否有实现类(即实现了 Registry
接口) 上有注解 @Adaptive
,如果没有就会动态生成。本例子将会动态生成。

直接运行程序结果如下,程序最终选择的实现类是 ZookeeperRegistry
,控制台结果如下:

  1. 09-20 23:43:13 323 main INFO - 服务: helloService 已注册到zookeeper上,备注: maple

  2. Zookeeper register already!

上面代码我们没有看到任何实现类的信息, DubboSPI
机制会为动态的去调用实现类。

我们重点分析 getAdaptiveExtension
方法找到的是 Registry
的自适应类,可以理解为是 Registry
的一个 适配器和代理类。如果该适配器类不存在, Dubbo
会通过动态代理方式在运行时自动生成一个自适应类。

打开 DEBUG
日志,在控制台我们看到了 Dubbo
生成的类的源码如下:

  1. package com.maple.spi;


  2. import com.alibaba.dubbo.common.extension.ExtensionLoader;


  3. public class Registry$Adaptive implements com.maple.spi.Registry {

  4.    public java.lang.String register(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {

  5.        if (arg0 == null) throw new IllegalArgumentException("url == null");

  6.        com.alibaba.dubbo.common.URL url = arg0;

  7.        String extName = url.getParameter("registry", "zookeeper");

  8.        if (extName == null)

  9.            throw new IllegalStateException("Fail to get extension(com.maple.spi.Registry) name from url(" + url.toString() + ") use keys([registry])");

  10.        com.maple.spi.Registry extension = (com.maple.spi.Registry) ExtensionLoader.getExtensionLoader(com.maple.spi.Registry.class).getExtension(extName);

  11.        return extension.register(arg0, arg1);

  12.    }


  13.    public java.lang.String discovery(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {

  14.        if (arg0 == null) throw new IllegalArgumentException("url == null");

  15.        com.alibaba.dubbo.common.URL url = arg0;

  16.        String extName = url.getParameter("registry", "zookeeper");

  17.        if (extName == null)

  18.            throw new IllegalStateException("Fail to get extension(com.maple.spi.Registry) name from url(" + url.toString() + ") use keys([registry])");

  19.        com.maple.spi.Registry extension = (com.maple.spi.Registry) ExtensionLoader.getExtensionLoader(com.maple.spi.Registry.class).getExtension(extName);

  20.        return extension.discovery(arg0, arg1);

  21.    }

  22. }

看代码,该适配器类的作用是类似于 AOP
的功能,再调用具体的实现类之前,先通 ExtensionLoader.getExtensionLoader(Registry.class).getExtension(extName);
根据 extName
loader
具体的实现类,然后再去调用实现类的相应的方法。

分析上面代码中的一句:

  1. String extName = url.getParameter("registry", "zookeeper");

extName
可以通过 url
进行传递,默认值为 zookeeper
, 该默认值即为我们定义的接口上的注解 @SPI
里的内容,上文我们定义的 @SPI("zookeeper")
,所以这里的默认值为 zookeeper
, 当 url
中没有对应的参数时,我们会去拿默认值。

我们可以修改 Main
测试程序,增加 key
registry
parameter

  1. public static void main(String[] args) {

  2.        URL url = URL.valueOf("test://localhost/test").addParameter("service", "helloService")

  3.                .addParameter("registry","etcd");


  4.        Registry registry = ExtensionLoader.getExtensionLoader(Registry.class).getAdaptiveExtension();


  5.        String register = registry.register(url, "maple");


  6.        System.out.println(register);


  7.    }

URL
中增加 Key
,并设置值为 etcd
,运行程序,结果如下:

  1. 09-20 23:44:00 009 main INFO - 服务: helloService 已注册到 Etcd 上,备注: maple

  2. Etcd register already!

实现类已经切换为 EtcdRegistry
了。

@Adaptive 注意细节

@Adaptive 源码

  1. public @interface Adaptive {

  2.    String[] value() default {};

  3. }

细心的读者已经发现了 @Adaptive
value
这个属性。上文在接口方法上定义的 @Adaptive
是没有设置值的。如果没有定义值, Dubbo
默认会使用一种策略生成。这种策略是将类名定义的驼峰法则转换为小写,并以 .
号区分。 例如上文的接口名为 Registry
,那么这个 Key
值就是 registry
。如果接口名为 HelloWorld
Key
值就为 hello.world

当然如果 @Adaptive
是有值的话,优先按里面的这个值来作为 Key
,例如 Dubbo
框架中的接口 RegistryFactory
,该接口的自适应类将会从 URL
protocol
key
来找实现类的 extName

  1. @SPI("dubbo")

  2. public interface RegistryFactory {

  3.   @Adaptive({"protocol"})

  4.    Registry getRegistry(URL url);

  5. }

总结

DubboAdaptive
模式在整个框架中运用十分广泛,如果用户没有在 URL
中进行自定义, Dubbo
默认会去加载扩展点接口上 @SPI
标注的内容,如果此注解没有值,那么我们就必须要在 URL
中进行值的传递了。如果我们想覆盖 Dubbo
默认的实现策略。可以通过在 URL
中增加 key-value
的形式来改变。

这样一个组件充分体现了设计模式,对修改关闭,对扩展开放的原则。当我们想自己实现 Dubbo
中的某个组件时,我们完全可以通过 DubboAdaptive
来动态的切换程序使用我们提供的组件。

彻底理解 DubboSPI
模式,以及 Adaptive
自适应类, Activate
激活类等,是看 Dubbo
源码的基础。如果我们想十分流畅的去分析 Dubbo
内部其他组件的实现机制,第一道要跨过的坎便是 DubboSPI


文章转载自非普通程序员,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论