SPI机制在JDK/Spring/SpringBoot/Dubbo的区别?

SPI机制在JDK/Spring/SpringBoot/Dubbo的区别?


   概要

   SPI (Service Provider Interface) 是一种服务发现机制,它允许第三方提供者为核心库或主框架提供实现或扩展。这种设计允许核心库/框架在不修改自身代码的情况下,通过第三方实现来增强功能。

   一、JDK原生的SPI

   Java SPI 是 Java 标准的一部分,任何符合 Java SPI 规范的框架或库都可以使用它。Java SPI 是一个纯粹的 Java API没有其他框架的支持,因此需要手动管理和配置。

   1. 应用示例

   在Java的生态系统中,SPI 是一个核心概念,允许开发者提供扩展和替代的实现,而核心库或应用不必更改,下面举出一个例子来说明。

   1)首先定义一个接口

public interface HelloSpi {
    String getName();
    void handle();
}

   2)定义不同的实现类

复制代码
public class OneHelloSpiImpl implements HelloSpi {
    @Override
    public String getName() {
        return "One";
    }
    @Override
    public void handle() {
        System.out.println(getName() + "执行");
    }
}
public class TwoHelloSpiImpl implements HelloSpi {
    @Override
    public String getName() {
        return "Two";
    }
    @Override
    public void handle() {
        System.out.println(getName() + "执行");
    }
}
复制代码

   3)在指定目录(META-INF/services)下创建文件

   文件名是接口的全类名,文件内容是实现类的全类名。

   这里创建的文件名是 org.example.chapter15.HelloSpi , 里面的内容为:

org.example.chapter15.OneHelloSpiImpl
org.example.chapter15.TwoHelloSpiImpl

   JDK原生的SPI主要通过在META-INF/services/目录下放置特定的文件来指定哪些类实现了给定的服务接口(可能会有多个),每个文件对应一个接口或服务类型。这些文件的名称应为接口的全限定名,内容为实现该接口的全限定类名。

   4)测试

复制代码
public class Test {
    public static void main(String[] args) {
        ServiceLoader<HelloSpi> load = ServiceLoader.load(HelloSpi.class);
        Iterator<HelloSpi> iterator = load.iterator();
        while (iterator.hasNext()) {
            HelloSpi next = iterator.next();
            System.out.println(next.getName() + " 准备执行");
            next.handle();
        }
        System.out.println("执行结束");

    }
}
//执行结果
//One 准备执行
//One执行
//Two 准备执行
//Two执行
//执行结束
复制代码

   通过执行结果我们可以看出,HelloSpi接口的所有实现类都得到了调用,我们可以通过这种机制根据不同的业务场景实现拓展的效果。示例是通过ServiceLoader实现的,我们来看一下这个类。

   4. ServiceLoader

   ServiceLoader是一个简单的服务提供者加载工具。是JDK6引进的一个特性。

   部分源码如下:

复制代码
public final class ServiceLoader<S>  implements Iterable<S>
{
    //指定服务提供者配置文件的基本路径
    private static final String PREFIX = "META-INF/services/"; 

    private final ClassLoader loader;

    //构造器
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");

        //默认是系统类加载器(也叫做应用程序类加载器)
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

    public static <S> ServiceLoader<S> load(Class<S> service) {
        //获取当前线程的线程上下文类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();

        //调用下面的方法
        return ServiceLoader.load(service, cl);
    }
    //根据指定的服务接口(service)和类加载器(cl)加载服务提供者
    public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader) 
    {
      return new ServiceLoader<>(service, loader);
    }
    //...
}
复制代码

   ServiceLoader类使用Java的类加载器机制从META-INF/services/目录下加载和实例化服务提供者。例如,ServiceLoader.load(MyServiceInterface.class)会返回一个实现了MyServiceInterface的实例迭代器。

   load方法是通过获取当前线程的线程上下文类加载器实例来加载的。Java应用运行的初始线程的上下文类加载器默认是系统类加载器。这里其实破坏了双亲委派模型,因为Java应用收到类加载的请求时,按照双亲委派模型会向上请求父类加载器完成,这里并没有这么做 。

   5. 缺点

   JDK原生的SPI每次通过ServiceLoader加载时都会初始化一个新的实例,没有实现类的缓存,也没有考虑单例等高级功能。无法做到按需加载或者按需获取某个具体的实现。

   6. 应用场景

   Java中有许多我们常见的框架使用SPI机制的地方,JDBC,Dubbo,Logback等,Spring中也有使用。

   二、Spring的SPI

   Spring SPI 是基于 Java SPI 的机制,但 Spring 为了支持更多的功能和扩展,进行了封装和增强。Spring SPI 用于扩展 Spring 的核心功能,例如扩展 BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor 或自定义事件监听器等。与JDK的SPI不同,Spring的SPI与其IoC (Inversion of Control) 容器集成,通过Spring容器管理第三方库集成。

   Spring的SPI体现在下面几个特点:

  • 依然使用 META-INF/services 文件来声明服务,但 Spring 可以利用容器管理这些服务。
  • 通过 Spring 容器和 Bean 的生命周期管理,自动完成服务加载和管理。
  • 可以结合 Spring 的其他特性(如依赖注入、自动配置)进行更加灵活的扩展。 

   Spring SPI 更侧重于框架内部扩展点的管理(例如 Spring 的 ApplicationListener 或自定义 BeanPostProcessor)。

   三、SpringBoot的SPI

   1.  自动配置

   Spring Boot SPI 是在 Spring Framework SPI 的基础上增强的机制,主要用于支持 Spring Boot 应用中的自动配置和模块化扩展。Spring Boot SPI 通过 META-INF/spring.factories 文件来注册自动配置类,极大简化了 Spring Boot 应用的配置过程。这个文件可以在多个jar中存在,并且Spring Boot会加载所有可见的spring.factories文件。

   Spring Boot SPI 是在 Spring Boot 启动时自动加载并配置相关 Bean,它依赖于 Spring Boot 的自动配置机制,通常会结合 @Configuration 类和 @Conditional 注解来判断是否需要加载某个配置。

   2. 应用示例

   1)创建自动配置类

复制代码
@Configuration
@ConditionalOnClass(HikariDataSource.class)
public class DataSourceAutoConfiguration {
    @Bean
    public DataSource dataSource() {
        // 假设自动配置一个数据库连接池
        return new HikariDataSource();
    }
}
复制代码

   Spring Boot 使用 @Conditional 注解(如 @ConditionalOnClass, @ConditionalOnProperty 等)来控制自动配置是否生效。例如,如果检测到应用类路径中有 HikariCP,则会自动配置数据源。

   2)spring.factories 配置文件

   在 META-INF/spring.factories 文件中列出所有自动配置类。Spring Boot 启动时会加载这些配置类并应用到 Spring 容器中。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

   3. 自动配置加载

   当 Spring Boot 启动时,会扫描 所有jar包中的spring.factories 文件,加载并执行配置类中的自动配置逻辑。这使得开发者无需手动配置数据源,Spring Boot 会根据条件自动配置数据源。

   4.  使用场景

   1)为第三方库或自定义组件提供自动配置。

   2)扩展 Spring Boot 启动过程,减少用户手动配置。

   3)支持 Spring Boot 项目的快速集成和插件化开发。

   四、Dubbo  SPI

   Dubbo 中实现了一套新的 SPI 机制,功能更强大,也更复杂一些。相关逻辑被封装在了ExtensionLoader类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容举例如下:

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

   与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外在使用时还需要在接口上标注 @SPI注解,例如:

复制代码
@SPI
public interface Robot {
    void sayHello();
}

public class OptimusPrime implements Robot {
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}

public class DubboSPITest {
    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}
复制代码

   Dubbo SPI和JDK SPI最大的区别就在于支持“别名”,可以通过某个扩展点的别名来获取固定的扩展点。就像上面的例子中,我可以获取Robot多个SPI实现中别名为“optimusPrime”的实现,也可以获取别名为“bumblebee”的实现,这个功能非常有用!

   通过@SPI注解的value属性,还可以默认一个“别名”的实现。比如在Dubbo中,默认的是Dubbo私有协议:dubbo protocol - dubbo://

   来看看Dubbo中协议的接口:

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

  在Protocol接口上,增加了一个@SPI注解,而注解的value值为Dubbo ,通过SPI获取实现时就会获取 Protocol SPI配置中别名为dubbo的那个实现,com.alibaba.dubbo.rpc.Protocol文件如下:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol

   然后只需要通过getDefaultExtension,就可以获取到@SPI注解上value对应的那个扩展实现了。

   五、总结

   1. Java SPI 是最基本的服务提供者接口机制,适用于插件化架构和服务扩展。需要手动管理和配置。

   2. Spring SPI 在 Java SPI 基础上,利用 Spring 框架的容器和管理能力,提供了更加灵活的扩展机制,适用于 Spring 框架本身的扩展。

   3. Spring Boot SPI 是 Spring SPI 的进一步增强,特别强调自动配置,可以大大简化 Spring Boot 项目中第三方库的集成和配置过程。

   4.  Dubbo的SPI 支持别名,可以通过名称获取扩展点的某个固定实现,配合Dubbo SPI的注解很方便。另外,还支持Dubbo内部的依赖注入,通过目录来区分Dubbo 内置SPI和外部SPI,优先加载内部,保证内部的优先级最高。

 

   参考链接:

   https://juejin.cn/post/7197070078361387069

   https://blog.csdn.net/qq_43592352/article/details/131002868

posted @   欢乐豆123  阅读(87)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示