剖析 SPI 在 Spring 中的应用

一、概述

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内置的SPI通过java.util.ServiceLoader类解析classPath和jar包的META-INF/services/目录 下的以接口全限定名命名的文件,并加载该文件中指定的接口实现类,以此完成调用。

2.1 Java SPI

先通过代码来了解下Java SPI的实现

① 创建服务提供接口
package jdk.spi;// 接口public interface DataBaseSPI {
  
      public void dataBaseOperation();}
② 创建服务提供接口的实现类

  • MysqlDataBaseSPIImpl

实现类1​​​​​​​

package jdk.spi.impl; import jdk.spi.DataBaseSPI; public class MysqlDataBaseSPIImpl implements DataBaseSPI {
  
       @Override    public void dataBaseOperation() {
  
          System.out.println("Operate Mysql database!!!");    }}
  • OracleDataBaseSPIImpl

实现类2​​​​​​​

package jdk.spi.impl; import jdk.spi.DataBaseSPI; public class OracleDataBaseSPIImpl implements DataBaseSPI {
  
       @Override    public void dataBaseOperation() {
  
          System.out.println("Operate Oracle database!!!");    }}

③ 在项目META-INF/services/目录下创建jdk.spi.DataBaseSPI文件

jdk.spi.DataBaseSPI

​​​​​​​

jdk.spi.impl.MysqlDataBaseSPIImpljdk.spi.impl.OracleDataBaseSPIImpl
④ 运行代码: 

JdkSpiTest#main()​​​​​​​

package jdk.spi; import java.util.ServiceLoader; public class JdkSpiTest {
  
       public static void main(String args[]){
  
          // 加载jdk.spi.DataBaseSPI文件中DataBaseSPI的实现类(懒加载)        ServiceLoader<DataBaseSPI> dataBaseSpis = ServiceLoader.load(DataBaseSPI.class);        // ServiceLoader实现了Iterable,故此处可以使用for循环遍历加载到的实现类        for(DataBaseSPI spi : dataBaseSpis){
  
              spi.dataBaseOperation();        }    }}
⑤ 运行结果:​​​​​​​
Operate Mysql database!!!Operate Oracle database!!!
2.2 源码分析

上述实现即为使用Java内置SPI实现的简单示例,ServiceLoader是Java内置的用于查找*服*务提供接口的工具类,通过调用load()方法实现对服务提供接口的查找(严格意义上此步并未真正的开始查找,只做初始化),后遍历来逐个访问服务提供接口的实现类。  

上述访问服务实现类的方式很不方便,如:无法直接使用某个服务,需要通过遍历来访问服务提供接口的各个实现,到此很多同学会有疑问:

  • Java内置的访问方式只能通过遍历实现吗?

  • 服务提供接口必须放到META-INF/services/目录下?是否可以放到其他目录下?

在分析源码之前先给出答案:两个都是的;Java内置的SPI机制只能通过遍历的方式访问服务提供接口的实现类,而且服务提供接口的配置文件也只能放在META-INF/services/目录下。

ServiceLoader部分源码​​​​​​​

public final class ServiceLoader<S> implements Iterable<S>{
  
      // 服务提供接口对应文件放置目录    private static final String PREFIX = "META-INF/services/";     // The class or interface representing the service being loaded    private final Class<S> service;     // 类加载器    private final ClassLoader loader;     // The access control context taken when the ServiceLoader is created    private final AccessControlContext acc;     // 按照初始化顺序缓存服务提供接口实例    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();     // 内部类,实现了Iterator接口    private LazyIterator lookupIterator;}

 从源码中可以发现:

  • ServiceLoader类本身实现了Iterable接口并实现了其中的iterator方法,iterator方法的实现中调用了LazyIterator这个内部类中的方法,解析完服务提供接口文件后终结果放在了Iterator中返回,并不支持服务提供接口实现类的直接访问。

  • 所有服务提供接口的对应文件都是放置在META-INF/services/目录下,final类型决定了PREFIX目录不可变更。

所以Java内置的SPI机制思想是非常好的,但其内置实现上的不足也很明显。

三、Dubbo SPI

Dubbo SPI沿用了Java SPI的设计思想,但在实现上有了很大的改进,不仅可以直接访问扩展类,而且在访问的灵活性和扩展的便捷性都做了很大的提升

3.1 基本概念
① 扩展点

一个Java接口,等同于服务提供接口,需用@SPI注解修饰。

② 扩展

扩展点的实现类。

③ 扩展类加载器:ExtensionLoader

类似于Java SPI的ServiceLoader,主要用来加载并实例化扩展类。一个扩展点对应一个扩展加载器。

④ Dubbo扩展文件加载路径

Dubbo框架支持从以下三个路径来加载扩展类:

  • META-INF/dubbo/internal

  • META-INF/dubbo

  • META-INF/services

Dubbo框架针对三个不同路径下的扩展配置文件对应三个策略类:

  • DubboInternalLoadingStrategy

  • DubboLoadingStrategy

  • ServicesLoadingStrategy

三个路径下的扩展配置文件并没有特殊之处,一般情况下:

  • META-INF/dubbo对开发者开放

  • META-INF/dubbo/internal 用来加载Dubbo内部的扩展点

  • META-INF/services 兼容Java SPI

⑤ 扩展配置文件

和Java SPI不同,Dubbo的扩展配置文件中扩展类都有一个名称,便于在应用中引用它们。

如:Dubbo SPI扩展配置文件​​​​​​​

#扩展实例名称=扩展点实现类adaptive=org.apache.dubbo.common.compiler.support.AdaptiveCompilerjdk=org.apache.dubbo.common.compiler.support.JdkCompilerjavassist=org.apache.dubbo.common.compiler.support.JavassistCompiler
3.2 Dubbo SPI

 先通过代码来演示下 Dubbo SPI 的实现。

① 创建扩展点(即服务提供接口)

扩展点​​​​​​​

package dubbo.spi; import org.apache.dubbo.common.extension.SPI; @SPI  // 注解标记当前接口为扩展点public interface DataBaseSPI {
  
      public void dataBaseOperation();}
② 创建扩展点实现类
  • MysqlDataBaseSPIImpl 

扩展类1​​​​​​​

package dubbo.spi.impl; import dubbo.spi.DataBaseSPI; public class MysqlDataBaseSPIImpl implements DataBaseSPI {
  
       @Override    public void dataBaseOperation() {
  
          System.out.println("Dubbo SPI Operate Mysql database!!!");    }}
  • OracleDataBaseSPIImpl 

扩展类2​​​​​​​

package dubbo.spi.impl; import dubbo.spi.DataBaseSPI; public class OracleDataBaseSPIImpl implements DataBaseSPI {
  
       @Override    public void dataBaseOperation() {
  
          System.out.println("Dubbo SPI Operate Oracle database!!!");    }}
③在项目META-INF/dubbo/目录下创建dubbo.spi.DataBaseSPI文件:

dubbo.spi.DataBaseSPI​​​​​​​

#扩展实例名称=扩展点实现类mysql = dubbo.spi.impl.MysqlDataBaseSPIImploracle = dubbo.spi.impl.OracleDataBaseSPIImpl

PS:文件内容中,等号左边为该扩展类对应的扩展实例名称,右边为扩展类(内容格式为一行一个扩展类,多个扩展类分为多行)

④ 运行代码:

DubboSpiTest#main()​​​​​​​

package dubbo.spi; import org.apache.dubbo.common.extension.ExtensionLoader; public class DubboSpiTest {
  
       public static void main(String args[]){
  
          // 使用扩展类加载器加载指定扩展的实现        ExtensionLoader<DataBaseSPI> dataBaseSpis = ExtensionLoader.getExtensionLoader(DataBaseSPI.class);        // 根据指定的名称加载扩展实例(与dubbo.spi.DataBaseSPI中一致)        DataBaseSPI spi = dataBaseSpis.getExtension("mysql");        spi.dataBaseOperation();                 DataBaseSPI spi2 = dataBaseSpis.getExtension("oracle");        spi2.dataBaseOperation();    }}
⑤ 运行结果:​​​​​​​
Dubbo SPI Operate Mysql database!!!Dubbo SPI Operate Oracle database!!!

从上面的代码实现直观来看,Dubbo SPI在使用上和Java SPI比较类似,但也有差异。

相同:

  1. 扩展点即服务提供接口、扩展即服务提供接口实现类、扩展配置文件即services目录下的配置文件 三者相同。

  2. 都是先创建加载器然后访问具体的服务实现类,包括深层次的在初始化加载器时都未实时解析扩展配置文件来获取扩展点实现,而是在使用时才正式解析并获取扩展点实现(即懒加载)。

不同:

  1. 扩展点必须使用@SPI注解修饰(源码中解析会对此做校验)。

  2. Dubbo中扩展配置文件每个扩展(服务提供接口实现类)都指定了一个名称。

  3.  Dubbo SPI在获取扩展类实例时直接通过扩展配置文件中指定的名称获取,而非Java SPI的循环遍历,在使用上更灵活。

3.3  源码分析

以上述的代码实现作为源码分析入口,了解下

2026年华中杯第十八届大学生数学建模挑战赛,B题高质量成品文章、完整解题代码及所有结果表。内容涵盖Python,MATLAB版本代码、Word论文文档无水印(Word版本),并包含详细思路解析,助力参赛队伍高效完成比赛,冲刺高奖项。开赛后持续更新【核心内容】成品文章包含完整解题思路、模型构建、数据分析与结果讨论。格式规范,符合竞赛论文要求,可直接提交或稍作修改使用。完整代码提供Python和MATLAB语言实现,覆盖数据处理、模型训练、结果可视化全流程。代码模块化设计,注释清晰,便于理解与二次开发。结果表格所有实验数据与结果均已整理成表,直观展示模型性能与对比分析。提供一键转换工具,方便用户根据需求调整论文格式。【产品优势】高效实用:成品论文与代码已通过严格测试,确保结果准确且可复现。全面覆盖:从思路解析到最终成果,一站式解决参赛需求。灵活便捷:支持多平台使用,网盘直发,后续更新免费获取。【适用人群】希望快速掌握解题思路与实现方法的学习者。需要高质量参考材料的科研爱好者。【交付清单】成品论文(Word)Python代码包数据集与结果表为了激励学生学习数学的积极性,拓展数学及相关学科知识面,提高学生独立分析、建模、求解、应用、写作等综合能力,湖北省工业与应用数学学会作为主办,举办“华中杯”大学生数学建模挑战赛。“华中杯”大学生数学建模挑战赛系原华中地区大学生数学建模邀请赛,已连续举办十六届,2020年由于疫情原因,暂停一年。2021年更名为“华中杯”大学生数学建模挑战赛。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

douluo998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值