Dubbo源码分析(1)—— Dubbo-SPI原理与实现

547 阅读6分钟

前言

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代码 image.png

同样也是检查缓存,如果没有就创建

查看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)这个方法

image.png

这个代码的含义就是,判断是不是包装类,就是构造方法里面含有这个类的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流程图:

阶段流程图

获取接口实例

调用方法