Spring SPI

 SPI 服务供给接口(Service Provider Interface)。是Java 1.5新添加的一个内置标准,允许不同的开发者去实现某个特定的服务。

1 SPI 介绍

一个接口,可能会有许多个实现,我们在编写代码时希望能动态切换具体实现,例如:

Interface interface = new Implement1(); // 创建一个具体的interface

上面是硬编码方式,我们希望在不修改代码的情况下,更换interface的具体实现。当然我们可以使用配置文件方式来实现这个需求,伪代码如下:

ResourceBundle rb = ResourceBundle.getBundle(“interface.properties”);

String impName = rb.getString(“impName”);

Interface interface = (Interface) Class.forName(impName).newInstance();

SPI 的实现则类似于上面的方法。让系统找到具体的实现。

 1.1 SPI的使用

Spring SPI_第1张图片

图 示例代码的项目结构说明

1)定义一个接口,在spi_example_interface项目中定义MakeMoney接口。

public interface MakeMoney {
    void hardWord();
}

2) 在自定义项目中实现接口,在spi_example_implement项目编写TeacherMakeMoney和ProgrammerMakeMoney两个类并实现MakeMoney接口。

public class ProgrammerMakeMoney implements MakeMoney {

    public ProgrammerMakeMoney() {
        System.out.println("程序员实例被创建了");
    }

    @Override
    public void hardWord() {
        System.out.println("敲代码");
    }
}

public class TeacherMakeMoney implements MakeMoney {

    public TeacherMakeMoney() {
        System.out.println("教师实例被创建了");
    }

    @Override
    public void hardWord() {
        System.out.println("教书");
    }
}

 3)在spi_example_implement项目中,resources文件下新建META-INF/services 文件夹,并在该文件夹下面创建由接口完全限定名命名的文件,在文件中依次列出该接口实现类的完全限定名。

Spring SPI_第2张图片

图 接口实现类说明文件

4)使用定义的接口,利用Java提供的ServiceLoader类发现这个接口的实现,并使用它们。

public class SpiUse {
    public static void main(String[] args) {
        ServiceLoader makeMonies = ServiceLoader.load(MakeMoney.class);
        Iterator iterator = makeMonies.iterator();
        while (iterator.hasNext()) {
            MakeMoney imp = iterator.next(); // 实现被加载的系统
            imp.hardWord();
        }
    }
}
/*
运行结果:
程序员实例
敲代码
教师实例被创建了
教书
*/

1.2 java.sql.Driver与SPI

在Java中定义了接口java.sql.Driver,其并没有具体的实现,具体的实现都是由不同的厂商提供。下面将以mysql的驱动为例,来大致介绍Java如何管理JDBC服务。

1)实现java.sql.Driver接口。

Spring SPI_第3张图片

图 mysql-connector-java jar包中Driver的定义

2) 在META-INFA/services文件夹下编写以Driver接口全限定名命名的文档,来引导ServiceLoader发现mysql实现的Driver的接口。

Spring SPI_第4张图片

图 mysql jar包下的引导文件

3)注册并管理JDBC服务。

Spring SPI_第5张图片

图 jdbc服务的调用过程

我们在使用jdbc 服务时,第一步是获取对数据库的连接,即执行上图的DriverManager.getConnection(url)方法。

Spring SPI_第6张图片

图 DriverManager的getConnection()方法的部分代码块

以下代码是模拟数据库厂商实现java.sql.Driver这个接口:

定义SqlDriver接口,全限定名是 com.huangmingfu.SqlDriver:

public abstract class SqlDriver {

    private static List driverList = new ArrayList<>();

    /**
     * 执行sql
     */
    public abstract void execute(String sql);

    public abstract Boolean connect(String url);

    public static void register(SqlDriver sqlDriver) {
        driverList.add(sqlDriver);
    }

    public static SqlDriver getConnect(String url) {
        for (SqlDriver driver : driverList)
            if (driver.connect(url)) return driver;
        return null;
    }

}

第三方项目中对SqlDriver接口的实现(mysql和oracle)

public class MySqlDriver extends SqlDriver {

    public MySqlDriver() {
        System.out.println("MySqlDriver实例被创建");
    }

    static {
        System.out.println("MySqlDriver实例被创建被加载到虚拟机了,进行注册:SqlDriver.register");
        SqlDriver.register(new MySqlDriver());
    }

    @Override
    public void execute(String sql) {
        System.out.println("mysql数据驱动,执行sql:" + sql);
    }

    @Override
    public Boolean connect(String url) {
        return url.startsWith("mysql");
    }
}

public class OracleDriver extends SqlDriver {

    public OracleDriver() {
        System.out.println("OracleDriver 实例被创建");
    }

    static {
        System.out.println("OracleDriver实例被创建被加载到虚拟机了,进行注册:SqlDriver.register");
        SqlDriver.register(new OracleDriver());
    }

    @Override
    public void execute(String sql) {
        System.out.println("oracle数据驱动,执行sql:" + sql);
    }

    @Override
    public Boolean connect(String url) {
        return url.startsWith("oracle");
    }

}

 在第三方项目的META-INF/com.huangmingfu.SqlDriver 引导文件中写入实现类的全限定名:

com.custom.MySqlDriver
com.custom.OracleDriver

使用Driver的实现类,来获取数据库连接:

public class UserDriver {

    private static SqlDriver sqlDriver;

    public static void main(String[] args) throws Exception{
        System.out.println("项目启动....");
//        classForName();
        spi();
    }

    /**
     * 反射形式
     */
    private static void classForName() throws Exception {
        System.out.println("尝试先通过class.forName的形式");
        sqlDriver = (SqlDriver)Class.forName("com.custom.MySqlDriver").newInstance();
        sqlDriver.execute("SELECT VERSION();");
    }

    /**
     * spi形式
     */
    private static void spi() {
        ServiceLoader serviceLoader = ServiceLoader.load(SqlDriver.class);
        Iterator iterator = serviceLoader.iterator();
        while (iterator.hasNext()) iterator.next(); //只是做加载动作
        SqlDriver driver = SqlDriver.getConnect("mysql://");
        if (driver != null) driver.execute("SELECT VERSION()");
    }
}
/*
运行结果
项目启动....
MySqlDriver实例被创建被加载到虚拟机了,进行注册:SqlDriver.register
MySqlDriver实例被创建
MySqlDriver实例被创建
OracleDriver实例被创建被加载到虚拟机了,进行注册:SqlDriver.register
OracleDriver 实例被创建
OracleDriver 实例被创建
mysql数据驱动,执行sql:SELECT VERSION()
 */

2 SPI 原理

java实现SPI的是ServiceLoader类,其实现步骤一共有两步:1)根据接口的全限定名查找META-INF/services下的接口实现引导文件记录的实现类全限定名集合;2)通过Class.forName(全限定名).newInstance()方法来将这些实现类加载进jvm中。

Spring SPI_第7张图片

图 第一步ServiceLoader获取接口实现类的全限定名

Spring SPI_第8张图片

图 第二步 ServiceLoader创建实现类的实例

3 SPI的优缺点及应用场景

spi 能扩展服务,将接口与实现解耦。通过服务接口和服务提供者,实现了服务规范的制定和服务具体实现的分离。

API

在大多数情况下,都是实现方制定接口并完成对接口的实现。调用方仅仅依赖接口调用,且无权选择不同实现。API是直接被应用开发人员使用。

SPI

是调用方来制定接口规范,提供给外部来实现。调用方在调用时则选择自己需要的外部实现。SPI是被框架扩展人员使用。

表 API与SPI的对比

缺点:

1)不能按需加载,需要遍历所有的实现并实例化,然后在循环中才能找到我们需要的实现。

2)多个并发多线程使用ServiceLoader类的实例是不安全的。

应用场景:

有关组织和公司定义接口标准,第三方提供具体实现。例如JDBC。

4 Spring Boot 中的spring.factories

在Spring Boot项目中,怎么将pom.xml文件里添加的依赖中的bean注册到Spring Boot项目的容器中呢?

在项目中,@ComponentScan注解只会扫描项目包内的bean并注册到Spring容器中,项目依赖包中的bean不会被扫描和注册。此时可以利用SPI来对这些依赖包中的bean进行加载注册。

META-INF/spring.factories 文件类似于SPI中的接口实现类引导文件。有spring-core包中的SpringFactoriesLoader类充当着类似ServiceLoader的作用。

你可能感兴趣的:(Spring,spring,java,后端)