dubbo源码实现之SPI 和自适应扩展点

API和SPI

API (应用程序接口编程)直接被应用开发人员使用,SPI(服务提供者接口编程) 被框架扩展人员使用,两者本质都是面向接口进行编程

两者的区别是什么呢?

区别是一个将接口放在调用者包中,一个将实现放在实现者包中

 

SPI常见的例子是:插件模式的插件。如:

  1. 数据库驱动 Driver
  2. 日志 Log
  3. Dubbo

SPI的使用场景

调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略,也就是对框架的自定义实现做扩展

要使用Java SPI,需要遵循如下约定:

1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;

2、接口实现类所在的jar包放在主程序的classpath中;

3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;

4、SPI的实现类必须携带一个不带参数的构造方法;

dubbo 中使用SPI

我们知道SPI是用来扩展实现的,只要根据SPI的规则创建扩展点的实现类,并且在指定文件下配置实现类,在程序运行的时候JVM就可以加载扩展点的实现类,

Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。

Dubbo 改进了 JDK 标准的 SPI 的以下问题:

 

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

 

dubbo中的扩展点是一个很重要的概念,我们可以使用扩展点替换掉dubbo大部分的默认加载

 

dubbo提供了很多新的扩展点的功能:

1.扩展点自动包装

自动包装扩展点的 Wrapper 类。ExtensionLoader 在加载扩展点时,如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类

2.扩展点自动装配

加载扩展点时,自动注入依赖的扩展点。加载扩展点时,扩展点实现类的成员如果为其它扩展点类型,ExtensionLoader 在会自动注入依赖的扩展点。ExtensionLoader 通过扫描扩展点实现类的所有 setter 方法来判定其成员。即 ExtensionLoader 会执行扩展点的拼装操作

这里带来另一个问题,ExtensionLoader 要注入依赖扩展点时,如何决定要注入依赖扩展点的哪个实现。在这个示例中,即是在多个WheelMaker 的实现中要注入哪个

这个问题在下面一点 扩展点自适应中说明

 

3.ExtensionLoader 注入的依赖扩展点是一个 Adaptive 实例,直到扩展点方法执行时才决定调用是哪一个扩展点实现。

Dubbo 使用 URL 对象(包含了Key-Value)传递配置信息,扩展点方法调用会有URL参数(或是参数有URL成员)

这样依赖的扩展点也可以从URL拿到配置信息,所有的扩展点自己定好配置的Key后,配置信息从URL上从最外层传入。URL在配置传递上即是一条总线

Dubbo SPI实现原理

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下:

optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.spi.Bumblebee

对比 java SPI的配置:

org.apache.spi.OptimusPrime org.apache.spi.Bumblebee

因为使用键值对进行配置,所以我们可以使用key获取指定的扩展点

Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性

 

Dubbo SPI的自适应机制

自适应拓展机制原理:

自适应扩展机制的原理就是生成一个代理类,此代理类根据方法调用参数中的url解析出具体需要调用的实现类,从Dubbo的IOC容器中找到对应的实例,调用实例对应的方法即可

 

(1)从上面的原理中我们可以看出有一个关键:url,如果被调用的方法里面没有url就无法找到实际需要调用的实现类,因此dubbo要求所有的需要使用自适应扩展的方法,其参数中必须可以找到url,可以直接将url作为参数,也可以使用封装了url的类

 

(2)除了url之外,还有一个关键点:动态字节码的生成,从原理的描述中我们可以看到,dubbo需要生成的是一个特殊的代理类,直接使用java的代理机制是不行的,因此就需要dubbo自行编写代理类的源码。这就涉及到了编译机制

首先 Dubbo 会为拓展接口生成具有代理功能的代码。然后通过 javassist 或 jdk 编译这段代码,得到 Class 类。最后再通过反射创建代理类,代码生成工具类:AdaptiveClassCodeGenerator

 

动态字节码生成

动态字节码生成中,最复杂的部分就是获取adaptive注解和SPI注解的值了,上面我们知道生成的动态类需要做的最关键的步骤就是,从url中解析出需要调用的具体实现类,也就是从url中获取某个key对应的值

例如:我们以protocol扩展点为例:

@SPI("dubbo")

public interface Protocol {

@Adaptive

<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

}

我们要为protocol扩展点生成代理类,并且在代理类中的export()方法中从url中获取参数protocol的值,其值可能有redis,dubbo,WebService等,根据值我们可以从dubbo的IOC容器中获取对应的protocol实例,然后就可以调用该实例的export()方法

 

上面流程中有两个关键点需要注意:

1.一是我们如何确定要从url中获取哪个key的值?

也就是说我们怎么知道要获取protocol的值?这里就需要用到注解Adaptive了,其value就是用来告诉我们需要去获取什么key的,例如:

@SPI("javassist")

public interface ProxyFactory {

@Adaptive({"proxy"})

<T> T getProxy(Invoker<T> invoker) throws RpcException;

}

上面的ProxyFactory扩展点的getProxy()方法使用了注解Adaptive,其value属性为proxy,也就是说,其动态类需要从url中获取的key是proxy,当注解Adaptive没有设置value属性的时候,我们可以直接获取被代理类的类名,例如protocol接口

2.二是如果我们没有设置key的值,怎么办?

当我们没有设置url中对应的key的值的时候,注解SPI就起作用了,我们看到了所有的扩展点都使用了SPI注解,并且设置了注解的value属性,这个属性值是用来做什么的呢?

其实这个属性值是用来设置从url中获取key的默认值的,例如:上面的ProxyFactory扩展点需要从url中获取的key是proxy,其默认值就是javassist,而另一个protocol扩展点的默认值是dubbo

 

激活扩展点:

在URL中配置了响应的参数就会加载对应的扩展点,例如

URL url=new URL("","",0);

// url=url.addParameter("metrics","metrics");

List<Filter> list=ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(url,"metrics");

System.out.println(list.size());

上面的代码默认会有10个filter,当我们向url里面添加参数cache,就会激活MetricsFilter扩展点,集合就有了11个元素

 

分析服务发布过程中实现自适应扩展点的原理:

自适应扩展点的主要功能:

在运行期间根据url中的配置信息,来动态的从Dubbo的IOC容器中取出对应的实现类

服务发布的过程中动态切换协议的需求:

例如:我们在定义一个服务的时候是可以使用protocol参数设置此服务使用什么协议进行发布的,每个服务都可以使用不同的协议发布,这就带来了服务发布过程中自动切换协议的需求,dubbo就是利用自适应扩展点来解决的

 

dubbo是如何利用自适应扩展点来解决这个问题的呢?

这就需要深入到服务发布的源码中了,我们知道自适应扩展点的两个关键点是获取url动态字节码编译代理类,上面我们已经了解了动态字节码编译代理类是根据注解来实现的,url是dubbo强制所有的自适应扩展点的参数都必须提供的,并且,如果在自适应扩展点无法获取到url是会报错的

 

那么这里就有一个问题了:

dubbo是如何保证所有的自适应扩展点方法都可以获取到url参数的?

很明显是通过代码保证的,或者说在dubbo源码里面会有一条流程来保证当执行到自适应扩展点的方法的时候,参数url一定是存在的

这里我们就来看看服务发布过程中,协议protocol的export方法自适应扩展点是如何保证参数url的获取的

 

协议protocol的export方法自适应扩展点是如何保证参数url的获取的?

1.一个dubbo:service对应一个ServiceBean ,在服务发布的时候ServiceBean 的onApplicationEvent方法,会收到一个事件通知,这个方法里面做了两个事情

(1)判断服务是否已经发布过

(2)如果没有发布,则调用调用 export 进行服务发布的流程(这里就是入口)

2.export方法会进入ServiceConfig 的export 方法,此方法会检查服务的各种配置,然后会调用doExport方法,在此方法中任然是配置的检查,然后会调用doExportUrls方法

3.doExportUrls方法:

(1) 记载所有配置的注册中心地址

(2)遍历所有配置的协议,protocols

(3)针对每种协议发布一个对应协议的服务

4.针对协议发布服务的方法是:doExportUrlsFor1Protocol,我们以 Dubbo 协议为例

在此方法中首先将service的各种配置组装成一个url,然后将url和接口信息等封装成一Invoker,然后就调用协议的export方法:protocol.export(wrapperInvoker)

 

到这里我们终于执行到了协议export方法自适应扩展点的位置,通过上面的流程我们可以看到service的配置已经被封装到了url中,url又再次被封装进了Invoker中,当然url中也记录了service配置的protocol信息

在执行export方法的时候,protocol的代理类Protocol$Adaptive 会从url中获取protocol配置信息,然后从dubbo的IOC容器中获取对应的协议实例,让协议实例发布服务

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值