前言
Dubbo是一个分布式、高性能、透明化的 RPC 服务框架,作用是提供服务自动注册、自动发现等高效服务治理方案,所以我决定看看源码探究一下这是为什么。
SPI
简单例子:
//接口
public interface TestService {
String getName();
}
//实现类
public class TestServiceImpl implements TestService {
@Override
public String getName() {
return "test";
}
}
然后在resources/META-INF/services目录下新建和++该接口路径相同名称的文件++
文件名:xx.xx.xx.TestService
文件内容:xx.xx.xx.TestServiceImpl
public static void main(String[] args) {
ServiceLoader<DriverService> serviceLoader = ServiceLoader.load(TestService.class);
for (TestService testService: serviceLoader){
System.out.println(testService.getName());
}
}
根据文件内配置的实现类,上面代码就会输出test,这样的好处是可以动态的替换不同实现类
其核心思想有点类似于策略模式,策略模式有一个实现方法就是:编写一个接口和几个不同的实现类,将实现类放入HashMap中,这样可以根据不同的key来调用不同的实现类。
虽然能够实现动态替换实现类,但是JAVA内置的SPI有许多缺点:
- 不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
- 加载不到实现类时抛出并不是真正原因的异常,错误很难定位。
Dubbo-SPI
首先下载一下dubbo源码:源码地址。 源码中有个demo项目,以下所有代码都在demo项目中编写
dubbo-spi和spi都是差不多的思想,但是实现细节不同,看一下示例代码:
@SPI("test")
public interface SpiTest {
String getName();
@Adaptive
int getAge(URL url);
@Adaptive(value = "country")
String getCountry(URL url);
@Adaptive({"province", "city"})
String getAddress(URL url);
}
这是一个接口,标记上了SPI标签
public class SpiImpl implements SpiTest{
@Override
public String getName() {
return "test";
}
@Override
public int getAge(URL url) {
return 123;
}
@Override
public String getCountry(URL url) {
return "GuangZhou";
}
@Override
public String getAddress(URL url) {
return "中国";
}
}
实现类 然后再resouces下面创建文件
文件内容:test=org.apache.dubbo.demo.provider.SpiImpl
//测试代码
public static void main(String[] args) {
ExtensionLoader<SpiTest> loader = ExtensionLoader.getExtensionLoader(SpiTest.class);
SpiTest spiTest = loader.getAdaptiveExtension();
URL url = URL.valueOf("?country=test");
System.out.println(spiTest.getCountry(url));
}
运行上面代码会出现:GuangZhou
首先解释一下上面使用到的两个注解的作用:
-
@SPI("test") SPI是一个标记,而test值对应了test=org.apache.dubbo.demo.provider.SpiImpl,注意加粗的部分,表示实现类的名称;而这个值并不是强制使用test这个实现类,而是一个默认值,如果没有其他实现类的时候,默认使用这个类
-
@Adaptive(value = "country") 注意这个URL的参数:country URL url = URL.valueOf("?country=test"); 每一个SPI方法必须构造一个URL参数,country表示的是url中的parameter
如果再写一个实现类:test1=org.apache.dubbo.demo.provider.SpiImpl1
url为:URL url = URL.valueOf("?country=test1") 那么会执行SpiImpl1这个实现类的方法
根据多次测试和文档总结这两个注解以及参数的含义:
-
@SPI 该注解可以用到类、接口和枚举类上,在Dubbo框架中都是在接口上,表示该接口是一个Dubbo SPI接口,意味着是一个自适应的扩展点。内部的参数用于指定默认值,即没有指定任何实现类的时候的默认调用。
-
@Adaptive 该注解可以用于类、接口、枚举类和方法上,在dubbo中大多数用于方法。
-
用于类:当这个接口用于类上时,表示该类直接作用默认实现,所有的实现类中只能有一个@Adaptive注解,不然会抛出异常。
-
value参数:value参数是一个数组,用于对URL解析出的key进行匹配,如果没有匹配到会使用驼峰规则匹配。
-
根据使用流程看源码
看一下ExtensionLoader这个类
几个目录地址:
"META-INF/services/"
"META-INF/dubbo/"
"META-INF/dubbo/internal/"
表示了静态文件存放的路径,需要放入这几个路径dubbo才能找得到
剩下的就是一堆cache,先不管,看一下主要方法
前面的部分验证传入的class是不是符合标准:接口类型和@SPI标注,然后尝试从缓存中拿出ExtensionLoader,如果没有就新建一个
根据SpiTest spiTest = loader.getAdaptiveExtension(); 看一下getAdaptiveExtension()方法
使用了synchronized+double-check,首先检查是否已加载过了,经过缓存双重检查,没有就去创建。
最主要还是createAdaptiveExtensionClassCode这个方法
这一大堆代码是字节码编辑到StringBuilder中,然后交给Compiler编译,Compiler也使用了SPI技术,有两种主流编译技术:JDK和javassist,dubbo默认使用的是AdaptiveCompiler固定实现,使用javassist作为默认编译器。
直接看一下createAdaptiveExtensionClassCode后编译的代码:
getName方法参数没有URL,所以是不能调用的,直接异常
Adaptive参数就被解析出来了,然后还是会调用ExtensionLoader的getExtension方法去获取实现类。
可以看出来,ExtensionLoader使用了模板对原接口进行了包装,以及对URL参数的判断,然后调用getExtension根据参数值去获取真实的实现类。
看一下getExtension代码
同样也是检查缓存,如果没有就创建
查看createExtension方法
第一次调用会进入getExtensionClasses方法,然后会进入injectExtension反射set开头的方法去注入参数。
检查cache,没有就进入loadExtensionClasses
真正进入了遍历目录方法了,先获取SPI上有没有参数,如果有就把数组的第一个参数当作默认的name
通过urls = classLoader.getResources(fileName)方法获取文件url,继续向下loadResource
将key和value分离,使用后Class.forName加载,然后继续向下loadClass
对class的一些判断,然后放入cache中,到此为止所有的MATE-INF下的文件所配置的类全部被加载到了内存中。
注意:isWrapperClass(clazz)这个方法
这个代码的含义就是,判断是不是包装类,就是构造方法里面含有这个类的Wrapper类,会将这个类放入到一个Set里,类似于AOP的操作包装。可以说这就是SPI里面的简易AOP。
具体例子:
public class SpiTestImplWrapper implements SpiTest {
private final SpiTest test;
public TestServiceImplWrapper(SpiTest test){
this.test = test;
}
@Override
public String getName() {
return "wrapper:"+testService.getName();
}
....其他方法
}
然后将这个类同样写入配置里面,当系统找到这个TestServiceImplWrapper的时候,会将原来的TestServiceImpl包装到TestServiceImplWrapper中,如果有多个这样的包装类,就封装成多个,类似于AOP的多层嵌套。
在dubbo很多地方都是用了wrapper
以上就是dubbo-spi的操作,可以看出来整理思想和实现还是非常简单的,dubbo-spi是dubbo的一个地基,在源码中随处可见,了解spi的原理有助于阅读以后的代码。
dubbo-spi流程图:
阶段流程图
获取接口实例
调用方法