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

Dubbo SPI和Java SPI

Coding的哔哔叨叨 2021-01-23
826


今天我们花长篇来理解下Dubbo的SPI机制。Dubbo的成功离不开它所采用的的微内核设计+SPI扩展,使得有特殊需求的开发者可以自定义扩展做定制的二次开发。

要理解SPI一定是要从最根本Java SPI机制讲起,一定要看完,一定会有收获。


SPI简介

Coding的哔哔叨叨



SPI即Service Provider Interface
,是一种服务发现机制。

在实践中我们连接数据库会配置数据库驱动,如:driverClassName=com.mysql.jdbc.Drive
,现在市面上各种数据库五花八门,不同的数据库有不同的底层协议,我们想通过java程序访问数据库,总不能接一种数据库,写一种数据库访问协议吧。所以,java制定了一套接口,用来约束这些数据库,方便统一的面向接口编程。数据库厂商们根据接口来开发他们对应的实现。

那么问题来了,真正使用的时候到底用哪个实现呢?从哪里找到实现类呢?


这时候 Java SPI 机制就派上用场了,不知道到底用哪个实现类和找不到实现类,我们告诉它不就完事了么。大家都约定好将实现类的配置写在一个地方,然后到时候都去哪个地方查一下不就知道了吗?

比如一接口有3个实现类,那么在系统运行时,这个接口到底该选择哪个实现类?

这就需要SPI,根据指定或默认的配置,找到对应的实现类,加载进来,然后使用该实现类实例

在系统实际运行的时候,会加载配置,用实现A2实例化一个对象来提供服务。

别人用这个接口,然后用你的jar包,就会在运行时通过你的jar包的那个配置文件找到这个接口该用哪个实现类这是JDK提供的功能。

举例:工程P,有个接口A,接口A在工程P中没有实现类,系统运行时怎么给接口A选个实现类呢?我们可以导入一个jar包,在jar包资源目录(resource)下的META-INF/services/
下,放上一个文件,文件名即接口名(接口A的全限定类名),文件内容为:com.example.service.实现类A2
让工程P来依赖此jar包,然后在系统运行时,对于接口A,就会扫描依赖的jar包,看看有没有META-INF/services
文件夹。如果有,再看看有没有名为接口A的文件,如果有,在里面找一下指定的接口A的实现是你的jar包里的哪个类。


Java SPI

Coding的哔哔叨叨

Java SPI 就是这样做的,约定在 Classpath的 META-INF/services/
目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。

通过jar包给某个接口提供实现,然后就在jar包的META-INF/services/
目录下放一个以接口全限定名命名的文件,指定接口的实现是这个jar包里的某个类。这样当引用了这个 jar 包的时候,就会去找这个 jar 包的 META-INF/services/
目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化。


比如mysql的Driver里配置,如下图:

当系统运行遇到我们配置的JDBC的驱动,就会从我们配置的驱动的那个jar暴力去加载具体的实现类。

Java SPI源码分析

Coding的哔哔叨叨

我们知道Dubbo 并没有用 Java 实现的 SPI,而是自定义 SPI,为啥不用,接下来我们进行一个对比,看看Java SPI有什么不方便的地方或者劣势,这样我们才能更清楚dubbo SPI的优势。

Java SPI举例:
    public interface JobInterface { 
    void doJob();
    }
    public class HomeJob implements JobInterface{    
    @Override    
    public void doJob() {
            System.out.println("I'm Home Job");    
         }
    }
    public class  SchoolJob implements JobInterface{
        @Override
        public void doJob() {
            System.out.println("I'm School Job");    
        }
    }
    public class JavaSPI {
        public static void main(String[] args) {
           ServiceLoader<JobInterface> load = ServiceLoader.load(JobInterface.class);
           Iterator<JobInterface> iterator = load.iterator();
              while (iterator.hasNext()){   
    JobInterface job = iterator.next();
                  job.doJob();        
               }    
         }
    }
    结果输出:

    从上面我的示例中可以看到ServiceLoader.load()
    其实就是 Java SPI 入口,我们来看看到底做了什么操作。

    先找当前线程绑定的ClassLoader
    ,如果没有就用 SystemClassLoader
    ,然后清除实现类缓存,再创建一个 LazyIterator

    那么重点就是LazyIterator
    了,从上面举例代码中可以看到,我们调用了 hasNext() 来做实例循环,通过 next() 得到一个实例。而 LazyIterator 其实就是 Iterator
    的实现类。我们来看看它到底干了啥。

    不管 if 还是 else ,重点都是红框里的代码方法,接下来就进入重要时刻了,先来看hasNextService方法:

    可以看到 hasNextService这个方法其实就是在约定好的地方找到接口对应的文件,然后加载文件并且解析文件里面的内容。
    再来看一下 nextService()

    从上面源码中我们可以分析得到,nextService()方法就是通过文件里填写的全限定名加载类,并且创建其实例放入缓存之后返回实例。

    Java SPI 的工作原理就是源码分析的内容,约定一个目录,根据全限定接口名(即文件名)去约定目录下找到文件,解析文件得到实现类的全限定名,然后循环加载实现类和创建其实例。

    我再用一张图来带大家过一遍。

    • Java SPI 有啥缺点?

    相信大家也能总结个大概了,Java SPI 在查找扩展实现类的时候遍历对应的SPI配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是代码里面又不用它,这就产生了资源的浪费。所以说 Java SPI 无法按需加载实现类


    Dubbo SPI

    Coding的哔哔叨叨

    良好的扩展性对于一个框架而言尤其重要,框架顾名思义就是搭好核心架子,给予用户简单便捷的使用,同时也需要满足他们定制化的需求

    Dubbo 就依靠 SPI 机制实现了插件化功能,几乎将所有的功能组件做成基于 SPI 实现,并且默认提供了很多可以直接使用的扩展点,实现了面向功能进行拆分的对扩展开放的架构

    Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。让我们想一下按需加载的话首先你得给个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化即可。

    Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类,通过 ExtensionLoader,可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下:

    Dubbo 就是这样设计的,配置文件里面存放的是键值对,配置内容如下。

    比如dubbo的protocol协议,在服务运行时判断,选用Protocol接口的哪个实现类,它会去找配置的Protocol,将配置的Protocol实现类,加载进JVM,将其实例化。

    微内核,可插拔,大量的组件,Protocol负责RPC调用的东西,有需要的话,我们也可以实现自己的RPC调用组件,实现Protocol接口,自己实现类即可。

    ExtensionLoader.getExtensionLoader(...) 这行代码在Dubbo里大量使用,对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态根据配置去找到对应的实现类。如果你没配置,那就走默认的实现。并且 Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有个自适应扩展机制。

    我们先来看一下 Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。

    • META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。

    • META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。

    • META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。

    在Dubbo自己的jar里,在/META_INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol
    文件中,可以看到dubbo支持的众多协议。

    Protocol
    接口定义中我们可以可以看到注解@SPI(“dubbo”)
    ,通过@SPI注解提供默认实现类,实现类是通过将dubbo
    作为默认key去配置文件里找到的,配置文件名为接口全限定名,然后通过dubbo
    作为key可以找到默认的实现类org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

    1.Dubbo的默认网络通信协议,就是dubbo协议,用的是DubboProtocol。

    2.在 Java 的 SPI 配置文件里每一行只有一个实现类的全限定名,在 Dubbo的 SPI配置文件中是 key=value 的形式,我们只需要对应的 key 就能加载对应的实现。


    Dubbo源码分析

    Coding的哔哔叨叨

    从上面的示例代码我们知道 ExtensionLoader
    是重点,它是类似 Java SPI 中 ServiceLoader 的存在,我们拿DubboProtocol
    的加载来举例。

    从源码中可以看到,第一步是先获取一个 ExtensionLoader
    ,然后再通过 ExtensionLoader.getExtension(name)
    得到指定的实现类实例。


    我们就先看下 getExtensionLoader()
    做了什么。
    通过分析代码可以看到,getExtensionLoader()
    就是通过一系列判断然后从缓存里面找是否已经存在这个类型的 ExtensionLoader 
    ,如果没有就新建一个塞入缓存。最后返回接口类对应的 ExtensionLoader 

    接下来我们看下getExtension()
     方法,这个方法实际就是从类对应的 ExtensionLoader 
    中通过名字找到实例化完的实现类。

    其中重点是 createExtension()
    方法。

    整体逻辑还是很清晰的,先找实现类,判断缓存是否有实例,没有就反射建个实例,然后执行 set 方法依赖注入。如果有找到包装类的话,再包一层。
    到这步为止我先画个图,大家捋一捋。

    • 接下来的问题是 getExtensionClasses()
      是怎么找的呢?


    • injectExtension()
      如何注入的呢(其实就是set方法注入)?

    • 为什么需要包装类呢?

    接下来我们一个一个解释。

    getExtensionClasses()
    这个方法里也是先去缓存中找,如果缓存是空的,那么调用 loadExtensionClasses()
    ,我们就来看下这个方法。

    而 loadDirectory
    里面就是根据类名和指定的目录,找到文件先获取所有的资源,然后一个一个去加载类,然后再通过loadClass
    去做一下缓存操作。

    loadClass
    之前已经加载了类,loadClass
    只是根据类上面的情况做不同的缓存。分别有 Adaptive
     、WrapperClass
     和普通类这三种,普通类又将Activate
    记录了一下。至此对于普通的类来说整个 SPI 过程完结了。

    不像 Java 原生的 SPI 那样去遍历加载对应的服务类,只需要通过 key 去寻找,并且寻找的时候会先从缓存的对象里去取,如果取不到就利用反射创建一个实体类。

    若想动态替换默认实现类,需使用@Adaptive
    注解。Protocol
    接口中,有两个方法添加了@Adaptive
    注解,就是说那俩接口方法会被代理实现。

    比如Protocol
    接口有俩@Adaptive
    注解的方法,在运行时会针对Protocol
    生成代理类,该代理类的那俩方法中会有代理代码,代理代码会在运行时动态根据url中的protocol来获取key(默认是dubbo),也可以自己指定,如果指定了别的key,那么就会获取别的实现类的实例。通过这个url中的参数不同,就可以控制动态使用不同的组件实现类。

    @Adaptive
    解我们我们下面会进行详细分析。


    Dubbo @Adaptive注解-自适应扩展

    Coding的哔哔叨叨

    自适应扩展是什么意思呢,我们不妨假象一个场景,根据配置加载SPI扩展,但是在启动时,不想让扩展被加载,而是想要在具体请求时,根据参数来动态选择对应的扩展实现,dubbo怎么做到的呢?

    Dubbo 通过一个代理机制实现了自适应扩展
    ,就是给我们想扩展的接口生成一个代理类,可以通过JDK 或者 javassist 编译生成的代理类代码,然后通过反射创建实例。

    这个实例里面的实现是根据方法的请求参数生成的扩展类,然后通过 ExtensionLoader.getExtensionLoader(type.class).getExtension(从参数得来的name)
    ,来获取真正的实例来调用。

    看个官网的例子:

    现在大家应该对自适应扩展有了一定的认识了,我们再来看下源码,到底怎么做的。

    @Adaptive就是自适应扩展相关的注解,可以修饰类和方法上,在修饰类的时候不会生成代理类,因为这个类就是代理类,修饰在方法上的时候会生成代理类,下面我们分别进行分析。

    • Adaptive注解作用在类上

    ExtensionFactory
     举例,它有三个实现类,
    其中一个实现类就被标注了 Adaptive 注解。
    ExtensionLoader
    构造的时候就会去通过getAdaptiveExtension
    获取指定的扩展类的 ExtensionFactory
    我们再来看下 AdaptiveExtensionFactory
     的实现。
    可以看到先缓存了所有实现类,然后在获取的时候通过遍历找到对应的 Extension。
    再来看 getAdaptiveExtension

    到这里其实已经和上文分析的 getExtensionClasses
    loadClass
    Adaptive
    特殊判断相呼应上了。

    • Adaptive 注解在方法上

    注解在方法上需要动态拼接代码,然后动态生成类,我们以 Protocol 为例子来看一下。

    Protocol
    接口上有两个方法注解了 Adaptive
     ,因此它会走 createAdaptiveExtensionClass
    方法的逻辑,上面我们getAdaptiveExtensionClass方法中分析过。
    具体在里面如何生成代码的我就不再深入了,有兴趣的自己去看吧,我就把成品解析一下,就差不多了。
    可以看到会生成包,也会生成 import
    语句,类名就是接口加个$Adaptive
    ,并且实现这接口,没有标记 Adaptive
    注解的方法调用的话直接抛错。
    我们再来看一下标注了注解的方法,我就拿 export 举例。
    就像我前面说的那样,根据请求的参数,即 URL 得到具体要调用的实现类名,然后再调用 getExtension
     获取。
    整个自适应扩展流程如下。

    Dubbo WrapperClass-AOP

    Coding的哔哔叨叨

    包装类是因为一个扩展接口可能有多个扩展实现类,而这些扩展实现类会有一个相同的或者公共的逻辑,如果每个实现类都写一遍代码就重复了,并且比较不好维护。

    因此就搞了个包装类,Dubbo 里帮你自动包装,只需要某个扩展类的构造函数只有一个参数,并且是扩展接口类型,就会被判定为包装类,然后记录下来,用来包装别的实现类。

    简单又巧妙,这就是 AOP 了。

    Dubbo InjectExtension-IOC

    Coding的哔哔叨叨

    直接看代码,很简单,就是查找 set 方法,根据参数找到依赖对象则注入。

    这就是 IOC。

    Dubbo Activate注解

    Coding的哔哔叨叨

    这个注解简单说下,拿 Filter 举例,Filter 有很多实现类,在某些场景下需要其中的几个实现类,而某些场景下需要另外几个,而 Activate
    注解就是标记这个用的。

    它有三个属性,

    • group
      表示修饰在哪个端,是 provider 还是 consumer。

    • value
      表示在 URL参数中出现此value才会被激活。

    • order
      表示实现类的顺序。


    Dubbo SPI总结

    Coding的哔哔叨叨

    先放个上述过程完整的图。

    本文主要涉及的知识点有,什么是SPI,然后用一个简单的示例进行了说明;分析了java SPI源码,得知:Java SPI 会一次加载和实例化所有的实现类
    。而 Dubbo SPI
    则自己实现了 SPI,可以通过名字实例化指定的实现类,并且实现了 IOC 、AOP 与 自适应扩展 SPI


    不积跬步,无以至千里。
    文章有帮助的话,点个转发、在看呗
    谢谢支持哟 (*^__^*)

    END


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

    评论