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 功能圆满了。