面试常问的dubbo的spi机制到底是什么?(上)

2022-07-27 13:36:52 浏览数 (1)

前言

一、什么是spi机制?

二、java的spi机制 -- ServiceLoader

三、spring的spi机制 -- SpringFactoriesLoader

四、dubbo的spi机制 -- ExtensionLoader 源码剖析

总结

前言

dubbo是一款微服务开发框架,它提供了 RPC通信 与 微服务治理 两大关键能力。作为spring cloud alibaba体系中重要的一部分,随着spring cloud alibaba在国内活跃起来,dubbo也越来越深受各大公司的青睐。本文就来对dubbo的spi机制源码进行剖析,看一看dubbo的spi到底有哪些特性和功能。

一、什么是spi机制?

SPI (Service Provider Interface),主要用于扩展的作用。举个例子来说,假如有一个框架有一个接口,他有自己默认的实现类,但是在代码运行的过程中,你不想用他的实现类或者想扩展一下他的实现类的功能,但是此时你又不能修改别人的源码,那么此时该怎么办?这时spi机制就有了用武之地。一般框架的作者在设计这种接口的时候不会直接去new这个接口的实现类,而是在Classpath路径底下将这个接口的实现类按作者约定的格式写在一个配置文件上,然后在运行的过程中通过java提供的api,从所有jar包中读取所有的这个指定文件中的内容,获取到实现类,用这个实现类,这样,如果你想自己替换原有的框架的实现,你就可以按照作者规定的方式配置实现,这样就能使用你自己写的实现类了。

spi机制其实体现了设计思想中的解耦思想,方便开发者对框架功能进行扩展。

二、java的spi机制 -- ServiceLoader

java中最常见的spi机制应用就是数据库驱动的加载,java其实就是定义了java语言跟数据库交互的接口,但是具体的实现得交给各大数据库厂商来实现,那么java怎么知道你的数据库厂商的实现了?这时就需要spi机制了,java好约了定在 Classpath 路径下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后内容是该数据库厂商的实现的接口的全限定名,这样数据库厂商只要按照这个规则去配置,java就能找到。

我以mysql来举例,看一下mysql是怎么实现的。

内容

java是通过ServiceLoader类来实现读取配置文件中的实现类的。大家有兴趣可以看一下里面的代码,其实就是读取到每个jar包底下的文件,读取里面的内容。

三、spring中的spi机制 -- SpringFactoriesLoader

相信spring大家都不陌生,在spring扩展也是依赖spi机制完成的,只不过spring对于扩展文件约定在Classpath 路径下的 META-INF目录底下,所有的文件名都是叫spring.factories,文件里的内容是一个以一个个键值对的方式存储的,键为类的全限定名,值也为类的全限定名,如果有多个值,可以用逗号分割,有一点得注意的是,键和值本身约定并没有类与类之间的依赖关系(当然也可以有,得看使用场景的约定),也就是说键值可以没有任何关联,键仅仅是一种标识,代表一种场景,最常见的自动装配的注解,@EnableAutoConfiguration,也就是代表自动装配的场景,当你需要你的类被自动装配,就可以以这个注解的权限定名键,你的类为名,这样springboot在进行自动装配的时候,就会拿这个键,找到你写的实现类来完成自动装配。

这里我贴出了自动装配时加载类的源码。

这里其实就是通过@EnableAutoConfiguration的全限定名从spring.factories中加载这个键对应的所有的实现类的名称,这样就能拿到所有需要自动装配的类的全限定名了。

mybatis整合spring的自动装配功能文件

内容

mybatis也是按照spring的规则来配置的。大家有空可以去看MybatisAutoConfiguration这个实现类,里面有mybatis是如何跟spring整合的内容。

SpringFactoriesLoader的应用场景还有很多,大家可以去看一下SpringBoot中的启动引导类SpringApplication,里面多次使用到了这个SpringFactoriesLoader这个类来获取各种实现。

四、dubbo的spi机制 -- ExtensionLoader源码剖析

本文是基于dubbo3.0.4版本源码剖析。

讲完了java和spring的中的spi机制,接下来进入本文的主题,dubbo的spi机制到底是什么?它与java自带的有何区别?为什么不用java的spi机制?

ExtensionLoader是dubbo的spi机制所实现的类,通过这个类来加载接口所有实现类,获取实现类的对象。同时每一个接口都会有一个自己的ExtensionLoader。

1)java的spi机制的缺点?

从我们分析java的spi机制可以看出,java约定了文件名为接口的名称,内容为实现。不知道大家有没有想过这里面有个很严重的问题,就是虽然我获取到了所有的实现类,但是无法对实现类进行分类,也就是说我无法确定到底该用哪个实现类,并且java的spi机制会一次性给所有的实现类创建对象,如果这个对象你根本不会使用,那么此时就会白白浪费资源,也就是说无法做到按需加载。

所以,dubbo就自己实现了一套spi机制,不仅解决了以上的痛点,同时也加入了更多的特性。

2)dubbo的配置文件约束。

dubbo会从四个目录读取文件META-INF/dubbo/internal/ 、META-INF/dubbo/ 、META-INF/services/、META-INF/dubbo/external/,文件名为接口的全限定名,内容为键值对,键为短名称(可以理解为spring中的对象的名称),值为实现类。

3)@SPI 注解的约束

dubbo中所有的扩展接口,都需要在接口上加@SPI注解,不然在创建ExtensionLoader的时候,会报错。代码体现在这里

顺便说说ExtensionDirector的作用,在3.0.3以前的版本,是没有这个类的,但是在之后的版本为了实现一些新的特性,就抽象出来了这个类,通过这个类来获取每个接口对应的ExtensionLoader

4)实现类的加载

先说各种特性之前,先说一下这些实现类是如何加载的,类的加载是非常重要的一个环节,与后面的spi特性有重要的关系。

类加载默认都是先调用getExtensionClasses这个方法的,当cachedClasses没有的时,才会去加载实现类,然后再把实现类放到cachedClasses中。真正实现加载的是loadExtensionClasses 方法,接下来我们详细看这个方法的源码。

checkDestroyed();

方法没什么东西,其实就是一个检查的作用。

cacheDefaultExtensionName();

缓存默认实现类的短名称。其实很简单,就是从@SPI注解中取出名称,就是默认的实现类的名称,缓存起来,ExtensionLoader有个getDefaultExtension方法,其实就是通过这个短名称对应的实现类的对象。

接下来会遍历LoadingStrategy,根据LoadingStrategy加载指定目录的文件。

我们先来看看LoadingStrategy的实例是怎么加载的。我们进入loadLoadingStrategies方法,

惊讶的发现竟然是使用了java的spi机制加载LoadingStrategy,那我们就去Classpath 路径下的 META-INF/services/路径下找这个LoadingStrategy接口的全限定名的文件,看看有哪些实现。有四个实现,也就是会按照这四个的加载策略来读取实现类。其中有个方法directory,就是指定加载的目录,这也就是我们前面说的那几个dubbo会加载的目录,其实是从这个方法返回的,你可以自己去看看这四个实现类对于这个方法的实现。其实我们也可以实现这个接口,指定我们自己想加载的目录。

这里会循环加载每个目录,我们进去loadDirectory方法。

这个其实就是拿出LoadingStrategy来调用重载的loadDirectory方法。

这里注意会调用两次loadDirectory,下面的那个其实是适配以前老版本的,不用关心。

接下来进去重载的loadDirectory方法。

可以看出,fileName就是LoadingStrategy所指定的目录 接口的全限定名,这里就解释了为什么实现类需要写在类全限定名的文件里。其实就是从每个jar底指定的目录类全限定名为名称的文件,得到每个jar底下的文件的URL。然后遍历每个URL,加载类,我们进入loadResource方法来看看具体是怎么加载的。

通过URL打开一个输入流,然后读取文件内容,取出每一行,以 = 进行分割(因为规定的是以键值对存的),键就是短名称,值就是实现类的名称,然后再进入loadClass方法,这个方法很重要,其实是对实现类进行一个分类,后面dubbo的特性实现的前提就是对这些实现类的分类操作。

标红的两处是这个意思

如果你加了@Adaptive注解,那么就将赋值到cachedAdaptiveClass属性上。我们叫这个类为自适应类。什么是自适应,其实说白了这个类本身并没有实际的意义,它是根据你的入参动态来实现找到真正的实现类来完成调用。getAdaptiveExtension其实就是获取到这个自适应实现类对应的对象。

如果你的实现类是有一个该类型为参数的构造方法,那么就将这个实现类放到cachedWrapperClasses中,并且我们称这个类为包装类,什么叫包装,其实跟静态代理有点像,就是将目标对象进行代理,可以增强功能。

这处标红的意思是判断是不是实现类是不是加了@Activate注解,是的话就将短名称和注解放入cachedActivates中,我们称这类实现类为自动激活的类,所谓的自动激活,就是可以根据你的入参,动态选择实现一批符合条件的实现类

saveInExtensionClass就是将这个实现类放入extensionClasses中,该目录下的实现类就加载完成了。

接下来会继续循环,加载不同的目录底下,都会进行分类,并放到extensionClasses中。

当LoadingStrategy循环玩之后,最后将extensionClasses放入cachedClasses中,此时就完成了对于指定目录下实现类的加载和分类。

至此,实现类的加载和分类就完成了。

由于篇幅有限,dubbo的spi机制的其它各种特性和功能,比如依赖注入、自适应、自动包装等功能,我会再写一篇文章跟大家分享。

总结

本文最开始先介绍了什么是spi机制,然后分析了java的spi机制和spring的spi机制,最后我们进入本文的主题,dubbo的spi机制,我们讲了实现类的加载,加载实现类的时候会对实现类进行分类。由于篇幅有限,dubbo的spi机制的其它的特性功能我将在下篇文章继续分享。

0 人点赞