剖析 SPI 在 Spring 中的应用

本文详述了Java SPI机制及其在Spring中的应用。Java SPI通过ServiceLoader加载META-INF/services配置,实现服务提供接口的查找。Dubbo SPI在Java SPI基础上进行了改进,支持直接访问扩展类。Spring SPI则通过spring.handlers和spring.factories实现动态扩展,前者处理自定义标签,后者加载实现类。文章通过代码示例和源码分析展示了SPI在不同框架中的实现差异和应用场景。
摘要由CSDN通过智能技术生成

一、概述

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.MysqlDataBaseSPIImpl
jdk.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.AdaptiveCompiler
jdk=org.apache.dubbo.common.compiler.support.JdkCompiler
javassist=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.MysqlDataBaseSPIImpl
oracle = 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 源码分析

以上述的代码实现作为源码分析入口,了解下Dubbo SPI是如何实现的。

ExtensionLoader

① 通过ExtensionLoader.getExtensionLoader(Classtype)创建对应扩展类型的扩展加载器。

ExtensionLoader#getExtensionLoader()
public static <T> Extension
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值