Java SPI

119 阅读5分钟

Java SPI

是什么

Java SPI的全称是Java Service Provider Interface,是一种动态加载服务的机制。这些专有名字听起来有点难理解,比较抽象,其实从用法和最后的实现结果上来看,Java SPI就是一个让开发者可以使用配置文件来动态指定某个接口或者抽象类的具体实现是哪一个类的机制。我们下面直接使用它,就可以更直观的感受到它有着什么功能。

怎么用

理论基础

在用之前我们需要一些理论基础,对于Java SPI而言,就是它让我们使用所制定的规则是什么,这就像玩具的说明书一样。首先再次重复一下Java SPI的作用 —— 让开发者可以使用配置文件来动态指定某个接口或者抽象类的具体实现是哪一个类的机制。可以看到文字中加粗的部分,要完成这个功能,加粗的部分是必不可少的,所以我们首先得提供这些加粗部分的内容,才可以使用Java SPI。

Java SPI组件

承接上文,介绍一下Java SPI的四个重要组件(注意,这里的组件是逻辑概念,具体对应的实现在解释中):

  1. Service Provider Interface:服务提供方接口,其实它对应的就是上文中的接口抽象类
  2. Service Provider:服务提供方,对应上文中的具体实现(一个或多个);
  3. SPI Configuration File:SPI配置文件,很明显对应上文中的配置文件,这个配置文件的具体位置就在resources目录下的META-INF/services目录下,具体使用方法我们在例子中再介绍;
  4. ServiceLoader:这个在上文中没有提到,其实可以想象得到,配置文件、接口(或抽象类)、实现都有了,还需要的就是从代码中获取该接口的具体实现是什么,而这个ServiceLoader就是用来获取接口的具体实现类结果的角色。

理论基础介绍完毕了,下面我们来实操看看。

代码编写

承接上文,我们得先准备接口(或抽象类)、具体实现和配置文件。

编写接口

我们定义一个MusicInstrument(乐器)接口,就定义一个方法play()(演奏);

public interface MusicalInstrument {
​
    void play();
}

编写具体实现

具体实现类定义两个:Throat(歌喉)、Guitar(吉他):

public class Throat implements MusicalInstrument {
    @Override
    public void play() {
        System.out.println("我一路向北~ 离开有你的季节~");
    }
}
public class Guitar implements MusicalInstrument {
    @Override
    public void play() {
        System.out.println("Guitar Play~");
    }
}

编写配置文件

Java SPI的配置文件是存储在classpath下的META-INF文件夹的services目录下的,文件名为定义好的接口(或抽象类)的名字,而文件中的值则为接口(或抽象类)的 具体实现类的包名+类名:

配置文件路径及名称:

image-20220103133419750.png

配置文件内容:

image-20220103133437104.png

获取具体实现

接下来我们使用ServiceLoader来获取具体指定的实现类,并调用play方法:

public class Main {
​
    public static void main(String[] args) {
​
        // 获取具体实现类对象集合
        ServiceLoader<MusicalInstrument> serviceLoader = ServiceLoader.load(MusicalInstrument.class);
​
        // 遍历调用play方法
        for (MusicalInstrument service : serviceLoader) {
            service.play();
        }
    }
}

最终打印结果:

image-20220103134237648.png

可以看到在代码中动态获取了配置文件中定义好的实现类的实例,并且成功打印了play方法中的内容。

应用场景

Java SPI最典型的应用场景就是数据库驱动,Sun公司对于数据库驱动提供了统一的jdbc接口,但是每个数据库的驱动需要单独开发,使用了Java SPI之后,在使用DriverManager的时候就会自动的加载系统中所有引入的数据库驱动,接下来我们翻翻源码~

首先我们要在项目pom文件中添加相应的驱动依赖,这里我们使用MySQL:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
</dependency>

然后我们在main方法中编写这样的代码:

public class Main {
​
    public static void main(String[] args) throws SQLException {
​
        DriverManager.getDriver("jdbc:mysql://localhost:3306/test");
    }
}

接着运行main方法,然后我们debug看看底层都经过了什么处理~

进入getDriver方法:

image-20220103145635872.png

image-20220103150025333.png

可以看到getDriver方法总共分成了两个部分:1.确认driver是否全部初始化; 2.确认registeredDrivers中是否包含可以处理参数url的driver。而我们所需要关注的是1,这个方法里面就处理了所有的driver的初始化,让我们进入ensureDriversInitialized方法看看~

进入ensureDriversInitialized方法:

image-20220103150447788.png

ensureDriversInitialized方法体比较长,但我们的重点在于Java SPI的部分,我们可以看到这个方法中有我们之前写的Java SPI获取实现类的代码,可以看到接口是com.mysql.Driver,那么问题来了,我们没有定义过配置文件也没有指定过实现,那这些代码到底加载了什么呢?这个时候我们就得去mysql依赖的jar包里面看看了~

image-20220103151036020.png

可以看到我们引入的mysql驱动依赖中包含了我们需要的配置文件,并且在配置文件中定义了它的Driver具体实现类com.mysql.cj.jdbc.Driver,这样一下子就都明了了,在我们使用DriverManager的时候,DriverManager会首先去确认所有的Driver有没有被初始化完成,如果没有被初始化完成则进行初始化操作,初始化操作其实分为两个部分,第一个是通过Java SPI加载不同的驱动jar包中的配置文件所指定的实现类,第二个是使用系统配置jdbc.drivers来加载驱动类,这样就可以将驱动的加载对使用者完全透明,也无需具体指定所需要的驱动实现类,真的是妙呀~

总结

本篇文章介绍了Java SPI,带领大家从Java SPI的理论基础到demo编写,也解析了Java SPI的具体使用场景,文章最后建议大家去看看DriverManager使用Java SPI加载完驱动之后是怎么处理传入的url的,特别是驱动的实例是如何添加到registeredDrivers中的,最后,文章中的代码可以github.com/ChuckChen12…在查看。