Java SPI介绍

87 阅读3分钟

1. SPI介绍

基于接口编程时,两个模块之间需要接口进行交互

如果这个接口在被调用方中,则属于API
如果这个接口在调用方中,则属于SPI

API,Application Programming Interface,应用程序接口
SPI,Service Provider Interface,服务提供接口,它是一种服务发现机制

在JDK的java.util包中有一个ServiceLoader类,它可以通过配置文件加载指定的service provider

被调用方提供了服务接口及其实现后,只需要在META-INF/services/ 目录中创建一个以接口全名命名的文件,然后在文件中列出该接口的具体实现类的全名

调用方就可以使用ServiceLoader来加载到这些实现类,从而实现功能

2. SPI示例

只看介绍的话有点绕,这里用一个简单示例,来演示SPI的使用

首先,声明一个接口:com.example.summerboot.spi.Animal,里面声明方法sound:

package com.example.summerboot.spi;

public interface Animal {
    void sound();
}

然后,实现两个Animal接口的类及其方法:

public class Cat implements Animal {
    @Override
    public void sound() {
        System.out.println("miao ~");
    }
}

public class Dog implements Animal {
    @Override
    public void sound() {
        System.out.println("wang! wang!");
    }
}

再然后,在META-INF/services中创建一个文件,命名为com.example.summerboot.spi.Animal,即接口全名
image.png
并在其中列出两个实现类的全名
image.png
最后,使用ServiceLoader的load方法,来加载这两个实现类,并调用其sound方法:

public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
        for (Animal animal : load) {
            animal.sound();
        }
    }
}

运行结果:
image.png
值得注意的是,ServiceLoader可以加载到所有jar包中META-INF/services下接口的实现类
比如,如果我们把上边的代码修改成这样:

public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
        for (Animal animal : load) {
            animal.sound();
        }
        ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
        for (Driver driver : drivers) {
            System.out.println(driver.getClass().getName());
        }
    }
}

运行结果:
image.png
可以看到,加载Driver接口时,把mysql和druid中的Driver实现类也加载了进来
分别打开mysql和druid的jar包,果然可以看到其配置的META-INF/services下的java.sql.Driver文件
image.png
image.png

3. SPI在DriverManager中的应用

在使用JDBC连接数据库之前,需要使用DriverManager获取连接,那它是怎么确定应该加载哪个驱动呢?
答案就是,它都加载,然后遍历调用所有驱动的connect方法,然后由各个驱动来确定URL是不是自己所支持的

类似于这样的代码:

if (!ConnectionUrl.acceptsUrl(url)) {
    /*
     * According to JDBC spec:
     * The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. This will be common, as when the
     * JDBC driver manager is asked to connect to a given URL it passes the URL to each loaded driver in turn.
     */
    return null;
}

那DriverManager又是怎么进行驱动加载的呢?

拿mysql来说明:
首先,DriverManager中有一段静态代码段用来加载并初始化驱动

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

loadInitialDrivers方法中,会看到这样的代码

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

可见DriverManager也是使用了SPI,使用ServiceLoader把写到配置文件里的Driver都加载了进来。

for (String aDriver : driversList) {
    try {
        println("DriverManager.Initialize: loading " + aDriver);
        Class.forName(aDriver, true,
                ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
        println("DriverManager.Initialize: load failed: " + ex);
    }
}

参考引用

Java SPI思想:zhuanlan.zhihu.com/p/...