从源码分析Dubbo的SPI机制

本文涉及的产品
云解析DNS,个人版 1个月
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 从源码分析Dubbo的SPI机制

Java SPI


在进行分析Dubbo SPI机制之前,我们还是从我们熟悉的java spi机制入手,其实在我们平常使用的开发框架中, 处处都是使用了SPI机制,比如我们使用的JDBC,日志框架等,我们可以根据配置集成我们需要的数据库例如mysql、oracle 等,下面从一个简单的例子来看一下Java  SPI;


Java Spi  demo:

public interface Tea {
    String getTeaName();
}
public class GreenTea implements Tea {
    @Override
    public String getTeaName() {
        return "green";
    }
}
public class RedTea implements Tea {
    @Override
    public String getTeaName() {
        return "red";
    }
}
import java.util.ServiceLoader;
public class JavaSpiTest {
    public static void main(String[] args) {
        ServiceLoader<Tea> load = ServiceLoader.load(Tea.class);
        for (Tea tea : load) {
            System.out.println(tea.getTeaName());
        }
    }
}

还有一个最重要的文件:文件路径为 resources/MET-INF/services。文件名称为接口的全限定名,在本demo中就是com.tuling.javaspi.Tea。文件内容如下:


com.tuling.javaspi.GreenTea
com.tuling.javaspi.RedTea


20210204121547423.png

这样我们在文件中写入Tea接口的具体实现类,就可以得到不同的实现,所以JDBC只需要暴露一个标准接口,具体的实现有各自的厂商自定义。然后用户项目中进行对应jar的和配置即可。这种 机制给各个框架提供了非常好可 扩展机制。


缺点


通过上面的代码我们会发现一个缺点,就是我们不能实现按需加载,也就说系统会一次性将文件中的所有的实现类都实例化了,这在性能和资源上都是一种浪费。


思考:我们如何实现按需加载呢?


思路:如果我们在文件中配置成key=value的规则,但时候我们在获取的时候,传入key 就可以实现了。


Dubbo SPI

20190728183400981.png

熟悉Dubbo的童鞋们都知道,Dubbo提供了极高的可扩展机制,所以说SPI贯穿了整个dubbo的核心 。


下面我们先来从一张图整体认识一下,Dubbo的SPI机制整体源码流程图:


20210204144324610.png


上图是小编在读SPI机制源码的时候根据自己的总结画出的源码中核心的方法流转图,各位小可爱可以根据上面的流程图阅读源码,效率会更高一些。


SPI核心代码详解


在我们看Dubbo的源码的时候经常看到上面的这种代码的写法,他们分别对应自适应扩展点(默认扩展点)、指定名称的扩展点、激活扩展点,在这里我们先对这些概念有一个认识,后面我们会详细介绍

ExtensionLoader.getExtensionLoader(xxx.class).getAdaptiveExtension();
ExtensionLoader.getExtensionLoader(xxx.class).getExtension(name);
ExtensionLoader.getExtensionLoader(xxx.class).getActivateExtension(url, key);

例如:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

Protocol接口,在运行的时候dubbo会判断一下应该选用这个Protocol接口的哪个实现类来实例化对象。


它会去找你配置的Protocol,将你配置的Protocol实现类加载到JVM中来,然后实例化对象,就用你配置的那个Protocol实现类就可以了。


上面那行代码就是dubbo里面大量使用的,就是对很多组件,都是保留一个接口和多个实现,然后在系统运行的时候动态的根据配置去找到对应的实现类。如果你没有配置,那就走默认的实现类。

@SPI("dubbo")  
public interface Protocol {  
    int getDefaultPort();  
    @Adaptive  
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;  
    @Adaptive  
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;  
    void destroy();  
} 

在dubbo自己的jar中,在META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol文件中:

filter=org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=org.apache.dubbo.rpc.support.MockProtocol
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=org.apache.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=org.apache.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol
http=org.apache.dubbo.rpc.protocol.http.HttpProtocol
org.apache.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=org.apache.dubbo.rpc.protocol.thrift.ThriftProtocol
native-thrift=org.apache.dubbo.rpc.protocol.nativethrift.ThriftProtocol
memcached=org.apache.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=org.apache.dubbo.rpc.protocol.redis.RedisProtocol
rest=org.apache.dubbo.rpc.protocol.rest.RestProtocol
xmlrpc=org.apache.dubbo.xml.rpc.protocol.xmlrpc.XmlRpcProtocol
registry=org.apache.dubbo.registry.integration.RegistryProtocol
qos=org.apache.dubbo.qos.protocol.QosProtocolWrapper

所以这就看到了dubbo的SPI机制默认是怎么玩的了,其实就是Protocol接口,@SPI(“dubbo”) 说的是,通过 SPI 机制来提供实现类,实现类是通过 dubbo 作为默认 key 去配置文件里找到的,配置文件名称与接口全限定名一样的,通过 dubbo 作为 key 可以找到默认的实现类就是 org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol


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


比如这个 Protocol 接口搞了俩 @Adaptive 注解标注了方法,在运行的时候会针对 Protocol 生成代理类,这个代理类的那俩方法里面会有代理代码,代理代码会在运行的时候动态根据 url 中的 protocol 来获取那个 key,默认是 dubbo,你也可以自己指定,你如果指定了别的 key,那么就会获取别的实现类的实例了。


核心类、核心接口


ExtensionLoader


ExtensionLoader表示某个接口的扩展点加载器,可以用来加载某个扩展点的实例。在ExtensionLodader中 除开有有几个非常重要的属性


   1、Class<?> type:表示当前ExtensionLoader实例是哪个接口的扩展点加载器


   2、ExtensionFactory objectFactory:扩展点工厂(对象工厂),可以获得某个对象


ExtensionLoader和ExtensionFactory的区别在于:


ExtensionLoader最终所得到的对象是Dubbo SPI机制产生的

ExtensionFactory最终所得到的对象可能是Dubbo SPI机制所产生的,也可能是从Spring容器中所获得的对象

前面我们介绍了三种获得扩展点的方式,我们现在来介绍另一个非常重要的方法:


createExtension(String name)方法


在调用createExtension(String name)方法去创建一个扩展点实例时,要经过以下几个步骤:


根据name找到对应的扩展点实现

根据实现类生成一个实例,把实现类和对应生成的实例进行缓存

对生成出来的实例进行依赖注入(给实例的属性进行赋值)

对依赖注入后的实例进行AOP(Wrapper),把当前接口类的所有的Wrapper全部一层一层包裹在实例对象上,没包裹个Wrapper后,也会对Wrapper对象进行依赖注入

返回最终的Wrapper对象

20210204153246637.png


getExtensionClasses()方法


getExtensionClasses()是用来加载当前接口所有扩展点实现类的,返回一个Map。之后可以从这个map中按照指定的那么获取对应的扩展点实现类。


当把当前接口的所有实现类都记载出来以后也会进行缓存,下次需要的时候直接从缓存中拿。


Dubbo在加载一个接口的扩展点时,思路是这样的:


1、根据接口的全限定名去META-INF/dubbo/internal/目录下寻找对应的文件,调用loadResource方法进行加载


2、根据接口的全限定名去META-INF/dubbo/目录下寻找对应的文件,调用loadResource方法进行加载


3、根据接口的全限定名去META-INF/services/目录下寻找对应的文件,调用loadResource方法进行加载


loadResource方法


loadResource方法就是完成对文件内容的解析,按行进行解析,回解析出“=”两边的内容,“=”左边的内容就是扩展点的name,右边的内容就是扩展点实现类,并且会利用ExtensionLoader类的类加载器来加载扩展点实现。然后调用loadClass方法对name和扩展点实例进行详细的解析,并且最终把他们放到map中。


loadClass()方法


loadClass方法会做下面几件事情:


1、当前扩展点实现类上是否存在@Adaptive注解,如果存在则把类认为是当前接口的默认自适应类(接口代理类),并把该类存到cachedAdaptiveClass属性上。


2、当前扩展点实现是否一个当前接口的Wrapper类,如何判断是否是Wrapper类?就是看当前类中是否存在一个构造方法,该构造方法只有一个参数,参数类型就为接口类型,如果存在这一个这样的构造方法,那么这个类就是该接口类的Wrapper类,如果是,则把该类添加到cachedWrapperClasses中去,cachedWrapperClasses是一个set。


3、如果不是自适应类,或者也不是Wrapper类,则判断时是否存在name,如果不存在name,那么就会报错。


4、如果有多个name,则判断一下当前扩展点实现类上是否存在@Adaptive注解, 如果存在则把该类添加到cachedActivates中,cachedWrapperClasses是一个map。


5、最后,遍历多个name,把每个name和对应的实现类存到extensionClasses中去,extensionClasses就是上文所提到的map。


至此,加载类就走完了。


回到createExtension(String name)方法中的逻辑,当前这个接口的所有扩展点实现类都扫描完了之后,就可以根据用户所指定的名字,找到对应的实现类了,然后进行实例化,然后进行IOC(依赖注入)和AOP。


Dubbo中的IOC


1、根据当前 实例的类,找到这个类中的setter方法,进行依赖注入


2、先分析出setter()方法的参数类型pt


3、在截取出setter方法所对应的属性名property


4、调用objectFactory.getExtension(pt, property)得到一个对象,这里就会从Spring容器 或通过DubboSpi机制得到一个对象,比较特殊的是,如果是通过DubboSpi机制得到的对象,是pt这个类型的一个自适应对象(代理对象)。


5、再反射调用setter方法进行注入。


Dubbo中的AOP


dubbo中也实现了一套非常简单的AOP,就是利用Wrapper,如果一个接口的扩展点中包含了多个Wrapper类,那么在实例化完某个扩展点后,就会利用这些Wrapper类对这个实例进行包裹,比如:现在有一个DubboProtocol的实例,同时对于Protocol这个接口还有很多的Wrapper,比如ProtocolFilterWrapper、ProtocolListenerWrapper,那么,当对DubboProtocol的实例完成了IOC之后,就会先调用new ProtocolFilterWrapper(DubboProtocol实例)生成一个新的Protocol的实例,再对此实例进行IOC,完了之后,会再调用new ProtocolListenerWrapper(ProtocolFilterWrapper实例)生成一个新的Protocol的实例,然后进行IOC,从而完成DubboProtocol实例的AOP。


小结


至此,Dubbo中关于SPI机制实现的基本原理都介绍完了,其中流程图中涉及到的核心方法都进行了解析,当然源码中还有一些其他的实现方法,感兴趣的读者可以继续进行分析。

目录
相关文章
|
2月前
|
Dubbo Java 应用服务中间件
Dubbo服务暴露机制解密:深入探讨服务提供者的奥秘【九】
Dubbo服务暴露机制解密:深入探讨服务提供者的奥秘【九】
72 0
|
2月前
|
缓存 Dubbo Java
Dubbo 第三节_ Dubbo的可扩展机制SPI源码解析
Dubbo会对DubboProtocol对象进⾏依赖注⼊(也就是⾃动给属性赋值,属性的类型为⼀个接⼝,记为A接⼝),这个时候,对于Dubbo来说它并不知道该给这个属性赋什么值,换句话说,Dubbo并不知道在进⾏依赖注⼊时该找⼀个什么的的扩展点对象给这个属性,这时就会预先赋值⼀个A接⼝的⾃适应扩展点实例,也就是A接⼝的⼀个代理对象。在调⽤getExtension去获取⼀个扩展点实例后,会对实例进⾏缓存,下次再获取同样名字的扩展点实例时就会从缓存中拿了。Protocol是⼀个接。但是,不是只要在⽅法上加了。
|
2月前
|
XML Dubbo Java
【Dubbo3技术专题】回顾Dubbo2.x的技术原理和功能实现及源码分析(温故而知新)(二)
【Dubbo3技术专题】回顾Dubbo2.x的技术原理和功能实现及源码分析(温故而知新)
39 2
|
2月前
|
XML 监控 Dubbo
【Dubbo3技术专题】回顾Dubbo2.x的技术原理和功能实现及源码分析(温故而知新)(一)
【Dubbo3技术专题】回顾Dubbo2.x的技术原理和功能实现及源码分析(温故而知新)
78 1
|
2月前
|
设计模式 JSON Dubbo
超越接口:探索Dubbo的泛化调用机制
超越接口:探索Dubbo的泛化调用机制
89 0
|
2月前
|
Dubbo 网络协议 应用服务中间件
分布式微服务框架dubbo原理与机制
分布式微服务框架dubbo原理与机制
|
2月前
|
Dubbo Java 应用服务中间件
Dubbo 第四节: Spring与Dubbo整合原理与源码分析
DubboConfigConfigurationRegistrar的主要作⽤就是对propties⽂件进⾏解析并根据不同的配置项项⽣成对应类型的Bean对象。
101 0
|
2月前
|
XML 缓存 Dubbo
Dubbo的魔法之门:深入解析SPI扩展机制【八】
Dubbo的魔法之门:深入解析SPI扩展机制【八】
48 0
|
2月前
|
XML 负载均衡 Dubbo
了解Dubbo配置:优先级、重试和容错机制的秘密【五】
了解Dubbo配置:优先级、重试和容错机制的秘密【五】
90 0
|
2月前
|
Dubbo Java 应用服务中间件
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用
微服务学习 | Springboot整合Dubbo+Nacos实现RPC调用