【Java杂记】SPI机制:使用示例及原理浅析

2,047 阅读4分钟

1.SPI是什么

SPI全称Service Provider Interface,是 JDK 内置的一种服务提供发现机制,目前市面上有很多框架都是用它来做服务的扩展发现。简单来说,它是一种动态替换发现的机制。它可以用来启用框架扩展和替换组件。整体机制图如下: 在这里插入图片描述

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制

  • 接口:调用方需要的服务
  • 策略模式:多个提供方,会提供自己的接口实现
  • 配置文件:提供方告知调用方,具体实现类在哪里,然后调用方去加载(本质上是一种通信方式)

==> 所以SPI的核心思想就是解耦。另外,三个重要概念:扩展点:一个Java接口 ; 扩展:扩展点的实现类 ;扩展实例:扩展点实现类的实例

2.应用场景

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略。比如,数据库驱动加载接口实现类的加载JDBC加载不同类型数据库的驱动:

  • java只提供了Driver接口,无具体实现(因为数据库有很多种)

  • MysqlDriver和OracleDriver由具体厂商编写提供

  • Driver工程引入提供方jar包后,如何知道具体实现类在哪?

    通过读取提供方名为META/INF.Driver全类名的配置文件,里面写了具体实现类是什么,然后加载就好 在这里插入图片描述

除此之外,应用到 SPI 的地方还有:

  • 日志门面接口实现类加载SLF4J加载不同提供商的日志实现类

  • Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

  • Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

3.使用示例

要使用Java SPI,应遵循以下流程:

在这里插入图片描述

  1. 为什么配置文件是要放在META-INF/services? 原因是ServiceLoader默认是加载该目录下文件 在这里插入图片描述

  2. ServiceLoader有什么用?当接口实现类所在的jar包放在主程序的classpath中,ServiceLoder会根据配置文件中的全类名将实现类加载到JVM中

再注意一点,SPI的实现类必须携带一个不带参数的构造方法(默认空参构造)

示例代码

  1. 定义一组接口 (假设是com.example.spitestDemo.SpiTestDemo),表示服务调用方
// 服务调用方需要的服务
public interface SpiTestDemo {
    void way1();
    void way2();
}
  1. 令起两个项目,分别实现该接口(引入接口jar包),表示两个服务提供方
// 提供方1的实现
public class SpiTestDemoImpl1 implements SpiTestDemo {
    @Override
    public void way1() {
        System.out.println("SpiTestDemoImpl1------way1");
    }
 
    @Override
    public void way2() {
        System.out.println("SpiTestDemoImpl1------way2");
    }
}
 
// 提供方2的实现 
public class SpiTestDemoImpl2 implements SpiTestDemo {
    @Override
    public void way1() {
        System.out.println("SpiTestDemoImpl2------way1");
    }
 
    @Override
    public void way2() {
        System.out.println("SpiTestDemoImpl2------way2");
    }
}

在两个提供方的src/main/resources/ 下建立 /META-INF/services 目录

  • 新增一个以接口命名的文件 (com.example.spitestDemo.SpiTestDemo文件)
    - src
        -main
            -resources
                - META-INF
                    - services
                        - com.example.spitestDemo.SpiTestDemo
    
  • 内容是要应用的实现类全类名(这里是SpiTestDemoImpl1和SpiTestDemoImpl2,每行一个类)
    com.example.spitestdemo.SpiTestDemoImpl1
    com.example.spitestdemo.SpiTestDemoImpl2
    
  1. 调用方引入提供方的两个jar包,使用 ServiceLoader 来加载配置文件中指定的实现。
class SpitestdemoApplicationTests {
 
    public void testWay(){
    	 // 传入接口作为参数,返回解析到的实现类
        ServiceLoader<SpiTestDemo> serviceLoader = ServiceLoader.load(SpiTestDemo.class);
        // 不同的实现类,有不同的逻辑
        for(SpiTestDemo spiTestDemo : serviceLoader){
            spiTestDemo.way1();
            spiTestDemo.way2();
        }
    }
 
}

在这里插入图片描述

4.原理浅析

这里就拿最熟悉的mysql jar包中jdbc加载驱动这一步骤源码简单分析其结构

  1. 查看源码,可以知道mysql的jar包中就能显示整个spi机制的所有的配置文件及源码。

  2. 在连接数据库的时候,第一步加载驱动,就是使用spi机制,spi机制会自动找到相关的驱动实现,如下图完整结构 在这里插入图片描述

  3. 而这个接口服务是在jdk中

  4. 初始化加载主要是通过ServiceLoader来实现服务查找

5.总结

优点:使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

缺点

  • JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现,就是如果你在 META-INF/service 下的文件里面加了 N 个实现类,那么 JDK 启动的时候都会一次性全部加载。那么如果有的扩展点实现初始化很耗时或者如果有些实现类并没有用到, 那么会很浪费资源
  • 如果扩展点加载失败,会导致调用方报错,而且这个错误很难定位到是这个原因
  • 多个并发多线程使用ServiceLoader类的实例是不安全的。