SPI(Service Provider Interface),是 Java 内置的一种服务提供发现机制,可以用来提高框架的扩展性,主要用于框架的开发中,比如 Dubbo,不同框架中实现略有差异,但核心机制相同,而 Java 的 SPI 机制可以为接口寻找服务实现。SPI 机制将服务的具体实现转移到了程序外,为框架的扩展和解耦提供了极大的便利。,得益于 SPI 优秀的能力,为模块功能的动态扩展提供了很好的支撑。,本文会先简单介绍 Java 内置的 SPI 和 Dubbo 中的 SPI 应用,重点介绍分析 Spring 中的 SPI 机制,对比 Spring SPI 和 Java 内置的 SPI 以及与 Dubbo SPI 的异同。,
,Java 内置的 SPI 通过 java.util.ServiceLoader 类解析 classPath 和 jar 包的 META-INF/services/ 目录 下的以接口全限定名命名的文件,并加载该文件中指定的接口实现类,以此完成调用。,先通过代码来了解下 Java SPI 的实现,① 创建服务提供接口:,② 创建服务提供接口的实现类,实现类 1:,实现类 2:,③ 在项目 META-INF/services/ 目录下创建 jdk.spi.DataBaseSPI 文件,
,jdk.spi.DataBaseSPI:,④ 运行代码:,JdkSpiTest#main(),⑤ 运行结果:,上述实现即为使用 Java 内置 SPI 实现的简单示例,ServiceLoader 是 Java 内置的用于查找服务提供接口的工具类,通过调用 load () 方法实现对服务提供接口的查找 (严格意义上此步并未真正的开始查找,只做初始化),最后遍历来逐个访问服务提供接口的实现类。,上述访问服务实现类的方式很不方便,如:无法直接使用某个服务,需要通过遍历来访问服务提供接口的各个实现,到此很多同学会有疑问:,在分析源码之前先给出答案:两个都是的;Java 内置的 SPI 机制只能通过遍历的方式访问服务提供接口的实现类,而且服务提供接口的配置文件也只能放在 META-INF/services/ 目录下。,从源码中可以发现:,所以 Java 内置的 SPI 机制思想是非常好的,但其内置实现上的不足也很明显。,Dubbo SPI 沿用了 Java SPI 的设计思想,但在实现上有了很大的改进,不仅可以直接访问扩展类,而且在访问的灵活性和扩展的便捷性都做了很大的提升。,(1) 扩展点,一个 Java 接口,等同于服务提供接口,需用 @SPI 注解修饰。,(2) 扩展,扩展点的实现类。,(3) 扩展类加载器:ExtensionLoader,类似于 Java SPI 的 ServiceLoader,主要用来加载并实例化扩展类。一个扩展点对应一个扩展加载器。,(4) Dubbo 扩展文件加载路径,Dubbo 框架支持从以下三个路径来加载扩展类:,Dubbo 框架针对三个不同路径下的扩展配置文件对应三个策略类:,三个路径下的扩展配置文件并没有特殊之处,一般情况下:,(5) 扩展配置文件,和 Java SPI 不同,Dubbo 的扩展配置文件中扩展类都有一个名称,便于在应用中引用它们。,如:Dubbo SPI 扩展配置文件:,先通过代码来演示下 Dubbo SPI 的实现。,(1) 创建扩展点 (即服务提供接口),扩展点:,(2) 创建扩展点实现类,扩展类 1:,扩展类 2,(3) 在项目 META-INF/dubbo/ 目录下创建 dubbo.spi.DataBaseSPI 文件:,
,dubbo.spi.DataBaseSPI:,PS: 文件内容中,等号左边为该扩展类对应的扩展实例名称,右边为扩展类 (内容格式为一行一个扩展类,多个扩展类分为多行)。,(4) 运行代码:,DubboSpiTest#main():,(5) 运行结果:,从上面的代码实现直观来看,Dubbo SPI 在使用上和 Java SPI 比较类似,但也有差异。,相同:,不同:,以上述的代码实现作为源码分析入口,了解下 Dubbo SPI 是如何实现的。,ExtensionLoader,(1) 通过 ExtensionLoader.getExtensionLoader (Classtype) 创建对应扩展类型的扩展加载器。,ExtensionLoader#getExtensionLoader():,getExtensionLoader () 方法中有三点比较重要的逻辑:,再看下 new ExtensionLoader (type) 源码:,ExtensionLoader#ExtensionLoader():,重点:构造方法为私有类型,即外部无法直接使用构造方法创建 ExtensionLoader 实例。,每次初始化 ExtensionLoader 实例都会初始化 type 和 objectFactory ,type 为扩展点类型;objectFactory 为 ExtensionFactory 类型。,(2) 使用 getExtension () 获取指定名称的扩展类实例 getExtension 为重载方法,分别为 getExtension (String name) 和 getExtension (String name, boolean wrap),getExtension (String name) 方法最终调用的还是 getExtension (String name, boolean wrap) 方法。,ExtensionLoader#getExtension():,Holder 类:这里用来存放指定扩展实例。,③ 使用 createExtension () 创建扩展实例,ExtensionL// 部分createExtension代码 private T createExtension(String name, boolean wrap) { // 先调用getExtensionClasses()解析扩展配置文件,并生成内存缓存, // 然后根据扩展实例名称获取对应的扩展类 Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { // 根据扩展类生成实例并对实例做包装(主要是进行依赖注入和初始化) // 优先从内存中获取该class类型的实例 T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { // 内存中不存在则直接初始化然后放到内存中 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } // 主要是注入instance中的依赖 injectExtension(instance); ...... }oader#createExtension():,createExtension () 方法:创建扩展实例,方法中 EXTENSION_INSTANCES 为 ConcurrentMap 类型的内存缓存,先从内存中取,内存中不存在重新创建;其中一个核心方法是 getExtensionClasses ():,ExtensionLoader#getExtensionClasses():,cachedClasses 为 Holder<map<string, class>> 类型的内存缓存,getExtensionClasses 中会优先读内存缓存,内存中不存在则采用同步的方式解析配置文件,最终在 loadExtensionClasses 方法中解析配置文件,完成从扩展配置文件中读出扩展类:,ExtensionLoader#loadExtensionClasses():,源码中的 strategies 即 static volatile LoadingStrategy [] strategies 数组,通过 Java SPI 从 META-INF/services/ 目录下加载配置文件完成初始化,默认包含三个类:,分别对应 dubbo 的三个目录:,上述的源码分析只是对 Dubbo SPI 做了简要的介绍,Dubbo 中对 SPI 的应用很广泛,如:序列化组件、负载均衡等都应用了 SPI 技术,还有很多 SPI 功能未做分析,比如:自适应扩展、Activate 活性扩展等 等,感兴趣的同学可以更深入的研究。,Spring SPI 沿用了 Java SPI 的设计思想,但在实现上和 Java SPI 及 Dubbo SPI 也存在差异,Spring 通过 spring.handlers 和 spring.factories 两种方式实现 SPI 机制,可以在不修改 Spring 源码的前提下,做到对 Spring 框架的扩展开发。,(1) DefaultNamespaceHandlerResolver,类似于 Java SPI 的 ServiceLoader,负责解析 spring.handlers 配置文件,生成 namespaceUri 和 NamespaceHandler 名称的映射,并实例化 NamespaceHandler。,(2) spring.handlers,自定义标签配置文件;Spring 在 2.0 时便引入了 spring.handlers,通过配置 spring.handlers 文件实现自定义标签并使用自定义标签解析类进行解析实现动态扩,内容配置如:,(3) SpringFactoriesLoader,类似于 Java SPI 的 ServiceLoader,负责解析 spring.factories,并将指定接口的所有实现类实例化后返回。,(4) spring.factories,Spring 在 3.2 时引入 spring.factories,加强版的 SPI 配置文件,为 Spring 的 SPI 机制的实现提供支撑,内容配置如:,(5) 加载路径,Java SPI 从 / META-INF/services 目录加载服务提供接口配置,而 Spring 默认从 META-INF/spring.handlers 和 META-INF/spring.factories 目录加载配置,其中 META-INF/spring.handlers 的路径可以通过创建实例时重新指定,而 META-INF/spring.factories 固定不可变。,首先通过代码初步介绍下 spring.handlers 实现。,(1) spring.handlers SPI,① 创建 NameSpaceHandler,MysqlDataBaseHandler:,OracleDataBaseHandler:,② 在项目 META-INF / 目录下创建 spring.handlers 文件:,
,文件内容:,spring.handlers,③ 运行代码:,SpringSpiTest#main(),④ 运行结果:,上述代码通过解析 spring.handlers 实现对自定义标签的动态解析,以 NameSpaceURI 作为 key 获取具体的 NameSpaceHandler 实现类,这里有别于 Java SPI,其中:,DefaultNamespaceHandlerResolver 是 NamespaceHandlerResolver 接口的默认实现类,用于解析自定义标签。,(2) 源码分析,下面从上述代码开始深入源码了解 spring handlers 方式实现的 SPI 是如何工作的。,① DefaultNamespaceHandlerResolver.resolve () 方法本身是根据 namespaceUri 获取对应的 namespaceHandler 对标签进行解析,核心源码:,DefaultNamespaceHandlerResolver#resolve(),看完 resolve 方法的源码,再看下 resolve 方法在 Spring 中调用场景,大致可以了解 spring.handlers 的使用场景:,
,可以看到 resolve () 主要用在标签解析过程中,主要被在 BeanDefinitionParserDelegate 的 parseCustomElement 和 decorateIfRequired 方法中调用。,② resolve () 源码中核心逻辑之一便是调用的 getHandlerMappings (),在 getHandlerMappings () 中实现对各个 jar 包中的 META-INF/spring.handlers 文件的解析,如:,DefaultNamespaceHandlerResolver#getHandlerMappings(),源码中 this.handlerMappings 是一个 Map 类型的内存缓存,存放解析到的 namespaceUri 以及 NameSpaceHandler 实例。,getHandlerMappings () 方法体中的实现使用了线程安全方式,增加了同步逻辑。,通过阅读源码可以了解到 Spring 基于 spring.handlers 实现 SPI 逻辑相对比较简单,但应用却比较灵活,对自定义标签的支持很方便,在不修改 Spring 源码的前提下轻松实现接入,如 Dubbo 中定义的各种 Dubbo 标签便是很好的利用了 spring.handlers。,Spring 提供如此灵活的功能,那是如何应用的呢?下面简单了解下 parseCustomElement ()。,resolve 作为工具类型的方法,被使用的地方比较多,这里仅简单介绍在 BeanDefinitionParserDelegate.parseCustomElement () 中的应用。,BeanDefinitionParserDelegate#parseCustomElement(),parseCustomElement 作为解析标签的中间方法,再看下 parseCustomElement 的调用情况:,
,在 parseBeanDefinitions () 中被调用,再看下 parseBeanDefinitions 的源码:,DefaultBeanDefinitionDocumentReader#parseBeanDefinitions(),到此就很清晰了,调用前判断是否为 Spring 默认标签,不是默认标签调用 parseCustomElement 来解析,最后调用 resolve 方法。,(3) 小节,Spring 自 2.0 引入 spring.handlers 以后,为 Spring 的动态扩展提供更多的入口和手段,为自定义标签的实现提供了强力支撑。,很多文章在介绍 Spring SPI 时都重点介绍 spring.factories 实现,很少提及很早就引入的 spring.handlers,但通过个人的分析及与 Java SPI 的对比,spring.handlers 也是一种 SPI 的实现,只是基于 xml 实现。,相比于 Java SPI,基于 spring.handlers 实现的 SPI 更加的灵活,无需遍历,直接映射,更类似于 Dubbo SPI 的实现思想,每个类指定一个名称 (只是 spring.handlers 中是以 namespaceUri 作为 key,Dubbo 配置中是指定的名称作为 key)。,同样先以测试代码来介绍 spring.factories 实现 SPI 的逻辑。,(1) spring.factories SPI,① 创建 DataBaseSPI 接口,接口:,② 创建 DataBaseSPI 接口的实现类,MysqlDataBaseImpl:,MysqlDataBaseImpl:,③ 在项目 META-INF / 目录下创建 spring.factories 文件:,
,文件内容:,spring.factories,④ 运行代码,SpringSpiTest#main(),⑤ 运行结果,从上述的示例代码中可以看出 spring.facotries 方式实现的 SPI 和 Java SPI 很相似,都是先获取指定接口类型的实现类,然后遍历访问所有的实现。但也存在一定的差异:,配置上:,实现上:,(2) 源码分析,我们还是从测试代码开始,了解下 spring.factories 的 SPI 实现源码,细品 spring.factories 的实现方式。,① SpringFactoriesLoader 测试代码入口直接调用 SpringFactoriesLoader.loadFactories () 静态方法开始解析 spring.factories 文件,并返回方法参数中指定的接口类型,如测试代码里的 DataBaseSPI 接口的实现类实例。,SpringFactoriesLoader#loadFactories(),源码中 loadFactoryNames () 是另外一个比较核心的方法,解析 spring.factories 文件中指定接口的实现类的全限定名,实现逻辑见后续的源码。,经过源码中第 2 步解析得到实现类的全限定名后,在第 3 步通过 instantiateFactory () 方法逐个实例化实现类。,再看 loadFactoryNames () 源码是如何解析得到实现类全限定名的:,SpringFactoriesLoader#loadFactoryNames(),源码中第 2 步获取所有 jar 包中 META-INF/spring.factories 文件路径,以枚举值返回。,源码中第 3 步开始遍历 spring.factories 文件路径,逐个加载解析,整合 factoryClass 类型的实现类名称。,获取到实现类的全限定名集合后,便根据实现类的名称逐个实例化,继续看下 instantiateFactory () 方法的源码:,SpringFactoriesLoader#instantiateFactory(),实例化方法是私有型 (private) 静态方法,这个有别于 loadFactories 和 loadFactoryNames。,实例化逻辑整体使用了反射实现,比较通用的实现方式。,通过对源码的分析,Spring factories 方式实现的 SPI 逻辑不是很复杂,整体上的实现容易理解。,Spring 在 3.2 便已引入 spring.factories,那 spring.factories 在 Spring 框架中又是如何使用的呢?先看下 loadFactories 方法的调用情况:,
,从调用情况看 Spring 自 3.2 引入 spring.factories SPI 后并没有真正的利用起来,使用的地方比较少,然而真正把 spring.factories 发扬光大的,是在 Spring Boot 中, 简单了解下 SpringBoot 中的调用。,② getSpringFactoriesInstances () getSpringFactoriesInstances () 并不是 Spring 框架中的方法,而是 SpringBoot 中 SpringApplication 类里定义的私有型 (private) 方法,很多地方都有调用,源码如下:,SpringApplication#getSpringFactoriesInstance(),在 getSpringFactoriesInstances () 中调用了 SpringFactoriesLoader.loadFactoryNames () 来加载接口实现类的全限定名集合,然后进行初始化。,SpringBoot 中除了 getSpringFactoriesInstances () 方法有调用,在其他逻辑中也广泛运用着 SpringFactoriesLoader 中的方法来实现动态扩展,这里就不在一一列举了,有兴趣的同学可以自己去发掘。,(3) 小节,Spring 框架在 3.2 引入 spring.factories 后并没有有效的利用起来,但给框架的使用者提供了又一个动态扩展的能力和入口,为开发人员提供了很大的自由发挥的空间,尤其是在 SpringBoot 中广泛运用就足以证明 spring.factories 的地位。spring.factories 引入在 提升 Spring 框架能力的同时也暴露出其中的不足:,介绍完 Spring 中 SPI 机制相关的核心源码,再来看看项目中自己开发的轻量版的分库分表 SDK 是如何利用 Spring 的 SPI 机制实现分库分表策略动态扩展的。,基于项目的特殊性并没有使用目前行业中成熟的分库分表组件,而是基于 Mybatis 的插件原理自己开发的一套轻量版分库分表组件。为满足不同场景分库分表要求,将其中分库分表的相关逻辑以策略模式进行抽取分离,每种分库分表的实现对应一条策略,支持使用方对分库分表策略的动态扩展,而这里的动态扩展就利用了 spring.factories。,首先给出轻量版分库分表组件流程图,然后我们针对流程图中使用到 Spring SPI 的地方进行详细分析。,
,说明:,通过上述的流程图可以看到,分库分表 SDK 通过 spring.factories 支持动态加载分库分表策略以兼容不同项目的不同使用场景。,其中分库分表部分的策略类图:,
,其中:ShardingStrategy 和 DBTableShardingStrategy 为接口;BaseShardingStrategy 为默认实现类;DefaultStrategy 和 CountryDbSwitchStrategy 为 SDK 中基于不同场景默认实现的分库分表策略。,在项目实际使用时,动态扩展的分库分表策略只需要继承 BaseShardingStrategy 即可,SDK 中初始化分库分表策略时通过 SpringFactoriesLoader.loadFactories () 实现动态加载。,SPI 技术将服务接口与服务实现分离以达到解耦,极大的提升程序的可扩展性。,本文重点介绍了 Java 内置 SPI 和 Dubbo SPI 以及 Spring SPI 三者的原理和相关源码;首先演示了三种 SPI 技术的实现,然后通过演示代码深入阅读了三种 SPI 的实现源码;其中重点介绍了 Spring SPI 的两种实现方式:spring.handlers 和 spring.factories,以及使用 spring.factories 实现的分库分表策略加载。希望通过阅读本文可以让读者对 SPI 有更深入的了解。
© 版权声明
文章版权归作者所有,未经允许请勿转载。