专栏首页Java艺术深入理解Dubbo源码(二),分析Java SPI与Dubbo SPI的实现源码

深入理解Dubbo源码(二),分析Java SPI与Dubbo SPI的实现源码

关注“Java艺术”一起来充电吧!

我在上一篇说了句:为什么我能短短几个晚上的时间就能看懂。dubbo不是这么容易完全看懂的,实际上我从国庆之前就开始一点点去了解dubbo,当前我所说的看懂,也只是看懂了大体的,一些细节上的实现都没去看。你想下,dubbo支持的协议有那么多,我们只要挑选一个去看就好了,方法很重要。

SPI是什么,在学习dubbo之前我也没听过

SPI全称是Service Provider Interface,直译就是服务提供者接口,是一种服务发现机制,是Java的一个内置标准,允许不同的开发者去实现某个特定的服务。SPI 的本质是将接口实现类的全限定名配置在文件中,由服务加载器读取配置文件,加载实现类,实现在运行时动态替换接口的实现类。理论总是枯燥的。

为什么要掌握SPI。Dubbo官方文档的源码导读部分介绍到,如果大家想要学习Dubbo的源码,SPI机制务必弄懂。可想而知,学习SPI对阅读Dubbo源码的重要性。是否有点危言耸听,我来解释下,SPI机制在Dubbo中的地位。

Dubbo整个框架是由一个个组件构成的,每个组件实现的是不同分层的逻辑,Dubbo就是通过SPI机制加载所有的组件。Dubbo总体分为业务层、RPC层、Remote层,而RPC层又可细分为代理层、注册中心层、集群负载层、监视器层、协议层,Remote层又可细分为信息交换层、传输层、序列化层。每一个细分的层都使用了SPI机制,实现灵活的组装使用,这也是Dubbo框架的优秀设计思想。

Dubbo的SPI并非使用Java提供的SPI,完全是自己实现的一套SPI机制,并对其进行了增强,如通过字节码实现动态代理类。我们先来了解下Java的SPI,再学习Dubbo的SPI,就能分辨出它们的不同之处。

一个使用Java SPI的例子

先来个Hello Word认识一下SPI。随便定义一个接口,比如LoginService,假设我们有多种登录方式。

如果用Shiro框架实现系统的用户权限管理,则提供一个ShiroLoginService。

如果我不想搞那么麻烦,我就直接使用Spring MVC的拦截器实现用户授权验证,那么就提供一个SpringLoginService。

我想通过修改配置文件的方式而不修改代码实现权限验证框架的切换,如何实现呢。用SPI,通过运行时从配置文件中读取实现类,加载使用配置的实现类。首先,需要在resources目录下新建一个目录META-INF,并在META-INF目录下创建services目录,用来存放配置文件。为什么一定要是services目录呢,一会看java的实现源码就知道了。

配置文件名为接口LoginService全类名,文件中写入使用的实现类的全类名。只要是在META-INF/services目录下,只要文件名是接口的全类名,那么编写配置文件内容的时候,IDEA就会自动提示有哪些实现类,很强大的IDEA。

[com.wujiuye.spi.LoginService配置文件内容]
com.wujiuye.spi.ShiroLoginService

编写个main方法试下Java提供的SPI。

ServiceLoader就是java提供的SPI机制的实现,调用load传入接口名获取到一个ServiceLoader实例,此时配置文件中注册的实现类是还没有加载到JVM的,只有通过iterator遍历获取的时候,才会去加载实现类,并实例化实现类。

好了,我们不需要关心例子输出的是什么,想必看都看出来了。需要说明的是,例子中配置的只有一个实现类,但其实我们是可以配置N多个的,并且iterator遍历的顺序就是配置文件中注册的实现类的顺序。

多实现类,如果非要想一个适用的业务场景的话,拦截器、过滤器等可插拔的设计模式,使用SPI加载是最好不过了。又或者一个画图程序,定义一个形状接口,实现类可以有矩形、三角形等,后期又加了圆形,就可以通过配置的方式支持圆形,完全不用修改任何代码。这样说,你能想到SPI的强大了吗?

Java SPI的实现源码分析

从例子中可以看出,我们分析Java SPI的实现源码需要从ServiceLoader的load方法入手,看源码首先就是要找到入口。但是分析Dubbo与Spring整合的源码的时候入口就没有那么好找了,即便找到了入口,由于Dubbo的多组件架构,也很容易迷路,其实这些都算容易的,最难的是看多线程框架的源码,除非有指南针,否则很容易就迷失在代码的海洋里,典型的代表就是Netty。

ServiceLoader的源码是很容易理解的,就是根据传入的接口,获取到接口的全类型名,将前缀"/META-INF/services"与类型名“com.wujiuye.spi.LoginService”拼接,就能定位到配置文件,然后就是获取类加载器,类加载器就是当前线程的上下文类加载器,根据类加载器获取资源文件,读取配置文件中的字符串,解析字符串,将解析出来的实现类全类名添加到一个数组,返回一个ServiceLoader实例,然后在遍历迭代器的时候再通过Class.forName加载类,最后实例化实现类,就是这么简单。

第一步是拿到当前线程的类加载器,调用ServiceLoader.load(Class<S> service,ClassLoader classLoader)方法实例化ServiceLoader。所以说,我们也可以自己指定类加载器。

接着看ServiceLoader的构造方法都做了些什么工作。

构建方法中判断如果类加载器为空,则使用系统类加载器,然后调用reload方法创建一个懒加载迭代器LazyIterator。所以我们调用ServiceLoader对象的iterator方法获取到的迭代器就是这个LazyIterator。

拿到迭代器后,接着我们会遍历迭代器,看下hashNext方法。

第一次调用hashNext方法configs是为空的,重点看第一条红色处,获取接口的全类名与前缀拼接拿到文件的路径。

private static final String PREFIX = "META-INF/services/";

这也就说明了为什么配置文件一定是放在“META-INF/services”目录下。

这里还会再做一次类加载器的检测,如果类加载器为空,则使用系统类加载器获取资源。最后是解析获取注册的所有实现类的类名,pending是一个迭代器Iterator<String>。解析过程就不看了,无非就是获取文件流,从流中读取文件内容,根据换行符获取实现类类名。

最后看下调用迭代器的next方法获取一个实现类的过程。

图中第一处画红色的地方,就是加载实现类,第二处红线就是通过Class的newInstance方法获取new一个对象。cast方法只是做强制类型转换。整个源码再简单不过了。

Dubbo SPI实现源码分析

建议看下官方文档写的Dubbo SPI源码分析,写得比较详细。这里我想用我的方式去介绍Dubbo SPI。传送门:https://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html

我之前下载了Dubbo的2.7.0版本的源码,所以我就基于Dubbo2.7.0介绍了。SPI相关的源码在dubbo-common模块下,extension包下,extension顾名思义,就是扩展,该包就是Dubbo扩展点机制实现的核心代码。

Dubbo的SPI实现是ExtensionLoader这个类,作用跟Java SPI的ServiceLoader一样。实现配置文件的读取解析,生成实例。但Dubbo的SPI需要在接口上添加注解@SPI。

Dubbo SPI的配置文件与Java SPI配置文件的写法不同,Dubbo是通过key-value的格式为接口配置实现类的,但这并不意味着一个接口只能有一个实现类。

比如

@SPI("dubbo")
public interface Protocol {
}

Protocol接口的@SPI注解value指定了dubbo,但这并不意味着只能使用

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

的配置,而是默认使用dubbo作为key(name),而dubbo=xxx的配置是指定dubbo协议的实现类,是的,在这个例子中,key只是用来限制dubbo协议只能配置一种实现类的。但是我们可以使用不同的协议,在调用ExtensionLoader获取Protocol实现类时,会根据你配置的协议作为key,从META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol配置文件中取得key指定的实现类。

与其说dubbo整个项目的源码神奇,不如说我一直很讨厌maven,因为它的配置太多太难懂,Dubbo这种项目中包含模块模块中又包含模块的,我看不懂maven的build配置的插件是怎么编译。比如Dubbo的RPC层。

不过我知道的是,多个模块编译后资源目录下相同的文件名的文件内容会合并到一起,所以dubbo协议的实现模块的org.apache.dubbo.rpc.Protocol只配置

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

hession协议的实现模块的org.apache.dubbo.rpc.Protocol只配置

hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol

编译后合并在一起就是

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
hessian=org.apache.dubbo.rpc.protocol.hessian.HessianProtocol

接着我们说下@Activate注解和@Adaptive注解。

@Activate自动激活

被@Activate注解注释的扩展点默认被激活启用,还可以通过注解的value指定该扩展点在什么条件下被自动激活。比如扩展点Filter的多个实现类:MonitorFilter和FutureFilter。(父类ListenableFilter实现了Filter扩展点)

@Activate(group = {PROVIDER, CONSUMER})
public class MonitorFilter extends ListenableFilter {

MonitorFilter在生产者和消费者环境下都默认激活。

@Activate(group = CommonConstants.CONSUMER)
public class FutureFilter extends ListenableFilter {

FutureFilter只在消费者端环境下被激活。

获取实现类不能再是使用ExtensionLoader的

public T getExtension(String name) // name就是配置文件中的key

方法,而是

 public List<T> getActivateExtension(URL url, String[] values) 

当然,这是dubbo为适配不同用途做的扩展功能,为支持一个扩展点有多个实现类需要同时启用的场景。这是Dubbo SPI实现Java SPI的支持一个接口多个实现类同时生效的特性,称之为自动激活。同时做了增强,即可以指定在某个条件下才激活。

@Activate注解是注释在扩展点(接口)的实现类上面的,所有该扩展点被@Activate注释的实现类都会在指定条件下自动激活。

@Adaptive自适应扩展

@Adaptive注解在方法上则该方法会被增强,这就是Dubbo自适应扩展机制,最重要的三个字:自适应。所以我们重点关注:什么是自适应,怎么实现,实现的目的是什么?带着问题去看。

在 Dubbo 中,很多拓展都是通过SPI机制进行加载的,比如 协议Protocol、负载均衡LoadBalance等。有些拓展希望在方法被调用时,根据运行时参数进行加载(这就是目的)。自适应拓展机制的实现逻辑比较复杂,Dubbo会使用javassist为拓展接口生成具有代理功能的代码,然后通过jdk编译这段代码得到Class类(这就是怎么实现的)。最后再通过反射创建代理类。

一个接口中有多个方法被@Adaptive注释时,Dubbo会遍历所有方法,对被@Adaptive注释的方法生成代理代码,所以,同一个接口的多个@Adaptive方法都在同一个代理类中,生成代码的是org.apache.dubbo.common.extension.AdaptiveClassCodeGenerator这个类,使用javassist生成字节码,这比asm容易多了,很容易看懂。

有使用@Adaptive注解的类,需要使用getAdaptiveExtension方法获取实现类实例,你会看到getAdaptiveExtension不需要指定name,因为一个接口只能有一个动态代理实现类,这个代理类是在运行时才生成的,而根据name去SPI取实现类是在代理中完成的。

public T getAdaptiveExtension() 

@Adaptive注解的value属性,是配置根据URL的哪个参数来作为name,通过SPI获取实现类(这就是自适应)。比如value=protocol时,使用javasist生成的动态代理实现的方法中,会生成url.getProtocol方法获取属性值。没错,@Adaptive注解的value就是用来指定调用URL参数对象的哪个get方法的。@Adaptive注解的方法必须有URL参数,或者像Invoker这种提供getUrl方法的,继承Node接口的接口都有这个方法,这是在Node接口中定义的。

那dubbo的SPI自适应扩展主要用来做什么呢?注意,这里是重点。

前面说的,被@Adaptive注解的方法在运行时会通过javassist生成动态代理类,而这个动态代理类做的事情就是根据运行时随时可能会变化的参数,动态通过SPI加载具体的实现类,然后再调用实现类的方法。

比如,rpc远程调用时,会在url上携带参数,如调用的目标XxxService的yy方法,而服务提供者XxxService有多个实现类,那么就可以在url上指定使用哪个实现类(配置文件中该实现类的key),然后再通过SPI获取到该实现类的实例。

Javassist生成的代码,就是拿到方法的URL参数,从URL中获取动态配置的参数,然后通过SPI加载具体的实现类,最后调用实现类的方法。所以,先判断方法URL参数是否为null,如果为null抛出异常,否则从URL中获取参数,如果没有获取到,也是抛出异常,如果获取到,就通过SPI获取实例。

举例:

比如XxxService自适应扩展的实现类为AdaptiveXxxService,这是通过运行时javassist生成的。

public class AdaptiveXxxService implements XxxService {
    public Object yy(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }  
      // 1.从 URL 中获取 XxxService 名称
        String xxxServiceName = url.getParameter("xxxServiceName");
        if (xxxServiceName == null) {
            throw new IllegalArgumentException("xxxServiceName == null");
        }        
        // 2.通过 SPI 加载具体的 XxxService
        XxxService xxxService = ExtensionLoader
            .getExtensionLoader(XxxService.class)
            .getExtension(xxxServiceName);
        
        // 3.调用目标方法
        return xxService.yy(URL url);
    }
}

一个远程调用实例,URL如下。

dubbo://127.0.0.1:9000/XxxService?xxxServiceName=default

假设XxxService的配置文件内容如下

default=DefaultXxxService

那么通过SPI自适应机制就能获取到DefaultXxxService实例,最终调用的是DefaultXxxService的方法。

Dubbo协议层Protocol就用到了这个特性,比如调用url是dubbo://127.0.0.1:9000/com.wujiuye.spi.XxxService.....,那么协议就是dubbo(key=dubbo),SPI自适应扩展机制会生成Protocol的代理类。调用export发布服务,与调用refer调用服务,都是走自适应扩展机制,根据协议动态从SPI中获取实现类的实例。

@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 SPI最难理解的也就这些内容了,我不分析具体源码,因为官方分析的已经很到位了。

总结

相信通过本篇的学习,大家都能够掌握什么是SPI了,看完后面试不再需要背什么是SPI,直接说源码。Dubbo框架相对来说,是一个比较容易入门与学习源码的框架,因为有中文文档,对我这种英盲而言,真的太爽了。至今为止,Netty是我读过源码的框架中最难读懂的一个框架。

每学习一个框架,都能从中学到不同的设计思想,不得不佩服Dubbo的设计者,正是分层的设计架构与Dubbo SPI的支持、恰到好处的设计模式的使用,让Dubbo具有非常高的扩展性。

本文分享自微信公众号 - Java艺术(javaskill),作者:wujiuye

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java锁事之Unsafe、CAS、AQS知识点总结

    Unsafe、CAS、AQS是我们了解Java中除synchronized之外的锁必须要掌握的重要知识点。CAS是一个比较和替换的原子操作,AQS的实现强依赖C...

    Java艺术
  • Dubbo自适应随机负载均衡策略的实现

    写了几篇dubbo源码分析的文章,感觉有些枯燥,而且阅读量也不是很好,我想了许久,怎样表达才能让读者看完后能对dubbo有更多的了解。甚至能让未使用过dubbo...

    Java艺术
  • Java全局异常处理,你不知道的骚操作(含hotspot源码分析)

    我本来是想优化项目中的请求日记打印的,就是把接收到一个请求到处理完成的整个调用链路(单个应用的),包括请求与响应数据,以及调用每个方法的传参,以及调用链路上抛出...

    Java艺术
  • 【三剑客之一】Dubbo 遇到初恋

    很多时候,其实我们使用这个技术的时候,可能都是因为项目需要,所以,我们就用了,但是,至于为什么我们需要用到这个技术,可能自身并不是很了解的,但是,其实了解技术的...

    好好学java
  • 阿里 RPC 框架 DUBBO 初体验

    最近研究了一下阿里开源的分布式RPC框架dubbo,楼主写了一个 demo,体验了一下dubbo的功能。

    haifeiWu
  • 新的可视化帮助更好地了解Spark Streaming应用程序

    之前,我们展示了在Spark1.4.0中新推出的可视化功能,用以更好的了解Spark应用程序的行为。接着这个主题,这篇博文将重点介绍为理解Spark Strea...

    CSDN技术头条
  • dubbo(三)服务运行容器Container

    Dubbo中的其中一个角色,服务运行容器Container。他是一个独立的容器,如果项目比较轻,没用到Web特性,因此不想用Tomcat等Web容器,则可以使...

    虞大大
  • 熊掌号文章校验未通过 1 类错误的原因分析及使用技巧。

    半夜更文,纯粹是为了更文而更文。最近撸一个项目代码,有点烦躁,导致博客断更超过一周了,虽然自定的月更目标完成了,但这么长时间断更还是不合适的。

    世纪访客
  • 传感器数据处理1:里程计运动模型及标定

    小飞侠xp
  • mysql虚拟列(Generated Columns)及JSON字段类型的使用

    mysql 5.7中有很多新的特性,但平时可能很少用到,这里列举2个实用的功能:虚拟列及json字段类型

    菩提树下的杨过

扫码关注云+社区

领取腾讯云代金券