Dubbo第三天

5. SPI 机制原理

因为dubbo 框架是建立的 SPI 机制上,因此在探寻 dubbo 框架源码前,我们需要先把 SPI 机制了解透彻。

5.1 java spi 机制

SPI 全称 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。

可以看到,SPI 的本质,其实是帮助程序,为某个特定的接口寻找它的实现类。而且哪些实现类的会加载,是个动态过程(不是提前预定好的)。

有点类似 IOC 的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以 SPI 的核心思想就是解耦。

比较常见的例子:

 数据库驱动加载接口实现类的加载:

    JDBC 加载不同类型数据库的驱动

 日志门面接口实现类加载:

    SLF4J 加载不同提供商的日志实现类

 Spring:

    Spring 中大量使用了 SPI,比如:对 servlet3.0 规范对

    ServletContainerInitializer 的实现、自动类型转换 Type Conversion

    SPI(Converter SPI、Formatter SPI)等

5.1.1 使用介绍

要使用 Java SPI,需要遵循如下约定:

 1、当服务提供者提供了接口的一种具体实现后,在 jar 包的META-INF/services 目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;

 2、接口实现类所在的 jar 包放在主程序的 classpath 中;

 3、主程序通过 java.util.ServiceLoder 动态装载实现模块,它通过扫描META-INF/services 目录下的配置文件找到实现类的全限定名,把类加载到 JVM;

 4、SPI 的实现类必须携带一个不带参数的构造方法。

示例:先定义一个接口(代码示例见源码包)

public interface InfoService {

    Object sayHello(String name) ;

}

再定义一系列它的实现,第一个实现:

public class InfoServiceAImpl implements InfoService {

    @Override

    public Object sayHello(String name) {

    System.out.println(name+",你好,调通了 A 实现!");

    return name+",你好,调通了 A 实现!";

    }

}

第二个实现:

public class InfoServiceBImpl implements InfoService {

    @Override

    public Object sayHello(String name) {

    System.out.println(name+",你好,调通了 B 实现!");

    return name+",你好,调通了 B 实现!";

    }

}

...等等实现

至此,整个 java SPI 的机制使用介绍完毕。

5.1.2 核心功能类

需要指出的是,java 之所以能够顺利根据配置加载这个实现类,完全依赖于jdk 内的一个核心类:

5.2 Dubbo SPI 机制

在上一节中,可以看到,java spi 机制非常简单,就是读取指定的配置文件,将所有的类都加载到程序中。而这种机制,存在很多缺陷,比如:

1. 所有实现类无论是否使用,直接被加载,可能存在浪费;

2. 不能够灵活控制什么时候什么时机,匹配什么实现,功能太弱Dubbo 基于自己的需要,增强了这套 SPI 机制,下面介绍 Dubbo 中的 SPI用法。

5.2.1  标签@SPI 用法

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们就可以按需加载指定的实现类。另外,需要在接口上标注 @SPI 注解。表明此接口是 SPI 的扩展点:

@SPI("b")

public interface InfoService {

    Object sayHello(String name) ;

    Object passInfo(String msg, URL url) ;

}

dubbo 的配置文件夹路径:

配置文件内容:

测试功能,指定加载某个实现:

测试结果:

5.2.2  标签@Activate 用法

Dubbo 的 SPI 机制虽然对原生 SPI 有了增强,但功能还远远不够。

在工作中,某种时候存在这样的情形,需要同时启用某个接口的多个实现类,如 Filter 过滤器。我们希望某种条件下启用这一批实现,而另一种情况下启用那一批实现,比如:希望的 RPC 调用的消费端和服务端,分别启用不同的两批 Filter,怎么处理呢?

    这时我们的条件激活注解@Activate,就派上了用场。

Activate 注解表示一个扩展是否被激活(使用),可以放在类定义和方法上,dubbo 用它在 spi 扩展类定义上,表示这个扩展实现激活条件和时机。它有两个设置过滤条件的字段,group,value 都是字符数组。 

用来指定这个扩展类在什么条件下激活。

下面以 com.alibaba.dubbo.rpc.filter 接口的几个扩展来说明。

@Activate(group = {Constants.PROVIDER, Constants.CONSUMER})

 public class testActivate1 implements Filter {

}

//表示如果过滤器使用方(通过 group 指定)属于 Constants.PROVIDER(服务提供方)或者 Constants.CONSUMER(服务消费方)就激活使用这个过滤器

//再看这个扩展

@Activate(group = Constants.PROVIDER, value = Constants.TOKEN_KEY)

public class testActivate2 implements Filter {

}

//表示如果过滤器使用方(通过 group 指定)属于 Constants.PROVIDER(服务提供方)并且 URL 中有参数 Constants.TOKEN_KEY(token)时就激活使用这个过滤器

详细用法见源码 demo。

5.2.3 javassist 动态编译

在 SPI 寻找实现类的过程中,getAdaptiveExtension 方法得到的对象,只是个接口代理对象,此代理对象是由临时编译的类来实现的。在此,先说明一个 javassist 动态编译类的两种用法:

通过创建 class 模型对象设置 class 属性,然后生成 Class:

1. CtClass-->CtField-->CtMethod

2. Class clazz = ctClass.toClass()

直接编译拼凑好的定义 class 的字符串,来生成 class:

JavassistCompiler.compile("public class DemoImpl

implements DemoService {...}",ClassLoader());


5.2.4 标签@Adaptive 用法

我们在前面演示了 dubbo SPI 的使用,但是有一个问题,扩展点对应的实现类不能在程序运行时动态指定,就是 extensionLoader.getExtension 方法写死了扩展点对应的实现类,不能在程序运行期间根据运行时参数进行动态改变。

而我们希望在程序使用时,对实现类进行懒加载,并且能根据运行时情况来决定,应该启用哪个扩展类。为了解决这个问题,dubbo 引入了 Adaptive注解,也就是 dubbo 的自适应机制。

先看下面的示例:

public void test3(){

    ExtensionLoader loader =

    ExtensionLoader.getExtensionLoader(InfoService.class);

    InfoService adaptiveExtension = loader.getAdaptiveExtension();

    URL url = URL.valueOf("test://localhost/test?info.service=a");

    System.out.println(adaptiveExtension.passInfo("james", url));

}

我们的 InfoService 的 passInfo 方法参数内,有一个 URL 的参数。URL 中附带了信息 info.service=a,希望调用 a 实现。a 实现的配置如下:

这初看起来非常矛盾,都已经在调用 InfoService 对象了,怎么还有机会来选择调用哪个 InfoService 对象呢?

其实重点就在于,现在的 InfoService 的调用对象 adaptiveExtension ,在当前,还只是个代理类,因此我们还有在代理内选择哪个目标实现的机会。    

我们运行代码,会发现还真就是调用的 A 实现类使用重点,URL 的格式:info.service=a 的参数名格式,是接口类 InfoService 的驼峰大小写拆分。

5.2.5 Dubbo SPI 的依赖注入

Dubbo SPI 的核心实现类为 ExtensionLoader,此类的使用几乎遍及 Dubbo 的整个源码体系。是大家以传统方式读源码的严重障碍。

ExtensionLoader 有三个重要的入口方法,分别与@SPI、@Activate、@Adaptive 注解对应。

getExtension 方法,对应加载所有的实现

getActivateExtension 方法,对应解析加载@Activate 注解对应的实现

getAdaptiveExtension 方法,对应解析加载@Adaptive 注解对应的实现

其中,@Adaptive 注解作的自适应功能,还涉及到了代理对象(而 Dubbo 的代理机制,有两种选择,jdk 动态代理和 javassist 动态编译类)。我们将后后续篇章对此进行说明。

Dubbo 的 SPI 机制,除上以上三种注解的用法外,还有一个重要的功能依赖注入对于 spring 这个强大的 IOC 工具,依赖注入大家一定都很了妥!在 Dubbo 自动生成 SPI 的扩展实例的时候也会发生依赖注入的场景,举一个具体的例子。

现有一个接口扩展点:

@SPI("peter")

public interface OrderService {

    String getDetail(String id, URL url);

}

而其实现类中,引入了另一个扩展点接口对象 infoService:

public class OrderServiceImpl implements OrderService {

    private InfoService infoService;

    public void setInfoService(InfoService infoService) {

    this.infoService = infoService;

}

@Override

public String getDetail(String name, URL url) {

    infoService.passInfo(name,url);

    System.out.println(name+",订单处理成功!");

    return name+",你好,订单处理成功!";

    }

}

此时,假如我们的 OrderService 在加载时会发生什么呢?

@Test

public void iocSPI() {

    //获取 OrderService 的 Loader 实例

    ExtensionLoader extensionLoader =

    ExtensionLoader.getExtensionLoader(OrderService.class);

    //取得默认拓展类

    OrderService orderService = extensionLoader.getDefaultExtension();

    URL url = URL.valueOf("test://localhost/test?info.service=a");

    orderService.getDetail("peter",url);

}

结果如下:

可以看到,dubbo 自动生成了实例,并注入了依赖之中。

这是什么机理呢,看这个扩展接口:

这是 dubbo 的一个扩展点工厂接口,只有一个方法,根据 class 和 name 查找实现。

这个接口,是一个扩展点,接下来看看此接口的实现类:

可以看到,有两个实现类:

一个适配类(adaptive,接口的默认实现的是AdaptiveExtensionFactory 在内部持有了所有的 factory 实现工厂,即后两个实现类)。

一个为 SPI 工厂(依赖类是扩展接口时发挥作用),一个为 Spring 工厂(依赖的是 springbean 时发挥作用)。

于是,当我们需要为某个生成的对象注入依赖时,直接调用此对象即可。

至此,整套 DubboSPI 的 IOC 功能圆满了。

你可能感兴趣的:(Dubbo第三天)