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的使用
图 示例代码的项目结构说明
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 文件夹,并在该文件夹下面创建由接口完全限定名命名的文件,在文件中依次列出该接口实现类的完全限定名。
图 接口实现类说明文件
4)使用定义的接口,利用Java提供的ServiceLoader类发现这个接口的实现,并使用它们。
public class SpiUse {
public static void main(String[] args) {
ServiceLoader<MakeMoney> makeMonies = ServiceLoader.load(MakeMoney.class);
Iterator<MakeMoney> 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接口。
图 mysql-connector-java jar包中Driver的定义
2) 在META-INFA/services文件夹下编写以Driver接口全限定名命名的文档,来引导ServiceLoader发现mysql实现的Driver的接口。
图 mysql jar包下的引导文件
3)注册并管理JDBC服务。
图 jdbc服务的调用过程
我们在使用jdbc 服务时,第一步是获取对数据库的连接,即执行上图的DriverManager.getConnection(url)方法。
图 DriverManager的getConnection()方法的部分代码块
以下代码是模拟数据库厂商实现java.sql.Driver这个接口:
定义SqlDriver接口,全限定名是 com.huangmingfu.SqlDriver:
public abstract class SqlDriver {
private static List<SqlDriver> 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<SqlDriver> serviceLoader = ServiceLoader.load(SqlDriver.class);
Iterator<SqlDriver> 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中。
图 第一步ServiceLoader获取接口实现类的全限定名
图 第二步 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的作用。