一、简述
本文介绍 SPI 机制。
二、什么是 SPI 机制
SPI(Service Provider Interface)机制是 Java 编程语言中的一种机制,用于实现组件之间的解耦和扩展。SPI 允许开发者编写服务接口(Service Interface),并在运行时动态地加载实现了该接口的服务提供者(Service Provider)。
SPI 机制的基本原理如下:
- 定义服务接口:开发者定义一个服务接口,描述了一组操作或功能,这些功能可以由不同的提供者来实现。
- 服务提供者编写服务:不同的服务提供者可以编写不同的服务(即实现该服务接口,并提供自己的实现逻辑)。
- 配置服务注册文件:每个服务提供者都需要在特定的位置提供一个描述文件,通常是
META-INF/services/接口全限定名
,文件内容为该接口实现类的全限定类名列表。 - 服务加载器加载服务:在程序运行时,Java 虚拟机会通过 SPI 机制加载并实例化这些服务提供者,然后调用其方法来完成具体的功能。
三、为什么使用 SPI 机制
- 假设我们有一个服务接口 Animal,定义了动物的基本行为:
public interface Animal {
void makeSound();
}
- Animal 接口有一个实现类
public class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Dog barks: Woof woof!");
}
}
- 编写接口和类的使用逻辑
public class Main {
public void sound() {
Animal animal = new Dog();
animal.makeSound();
}
}
上面的例子是传统的开发模式(在一个项目中定义接口,实现接口)。现在将以上内容拆分到两个项目:
- 在 A 项目中定义接口
public interface Animal {
void makeSound();
}
- 在 A 项目中定义接口的使用逻辑(但是没有实现类)
import java.util.ServiceLoader;
public class Main {
public void sound() {
// 使用 ServiceLoader 加载 Animal 接口的实现类
ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);
// 遍历并调用每个实现类的方法
for (Animal animal : animals) {
animal.makeSound();
}
}
}
- B 项目中引入 A 项目,并且实现 A 中的接口
public class Dog implements Animal {
@Override
public void makeSound() {
System.out.println("Dog barks: Woof woof!");
}
}
- B 项目在
META-INF/services/Animal
文件中提供实现类的全限定类名:
com.example.Dog
上面两种方式可以实现同样的功能,第二种方式采用的是 SPI 机制。在 SPI 机制中,A 定义方法的执行逻辑,并开放了一个扩展点交由 B 实现,扩展点的具体功能是由实现者自己处理,这样就实现了解耦。可以看出 SPI 机制的优点在于它的松耦合性和扩展性,因为服务接口与具体的实现是分离的,开发者可以根据需要灵活地替换服务提供者接口的实现。
四、如何使用 SPI 机制
我们这里以 JDBC 为例来说明该如何使用 SPI。
4.1 定义服务接口
Java 团队在 java.sql
包中定义了 Driver 接口
package java.sql;
import java.util.logging.Logger;
public interface Driver {
Connection connect(String url, java.util.Properties info)
throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
4.2 服务加载器加载服务
Java 团队在 java.sql.DriverManager
类中使用 java.util.ServiceLoader
来加载 java.sql.Driver
接口的实现类(可能不止一个),并实现了一些逻辑。
package java.sql;
import java.util.ServiceLoader;
public class DriverManager {
// 此处省略了一些逻辑内容
private static void ensureDriversInitialized() {
// 服务加载器加载服务
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 迭代遍历能够找到的服务实现
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
// 此处省略了一些逻辑内容
}
4.3 服务提供者编写服务
MySQL 团队在自己项目的 com.mysql.cj.jdbc
包中编写了针对 Java 团队 java.sql.Driver
接口的实现。
package com.mysql.cj.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
4.4 服务提供者配置服务注册文件
MySQL 团队在自己项目中 META-INF/services/
目录下配置了服务注册文件。该文件名是接口的全限定名称,文件里面的内容是实现类的全限定名称。
4.5 SPI 机制处理流程
- Java 团队预先定义好接口,定义好接口的使用逻辑
- MySQL 团队实现接口,并以规定好的方式配置服务注册文件(即配置文件必须在 META-INF/services 目录下,文件名称以接口全限定名称命名,文件内容为接口实现类的全限定名称)
- 当在项目中引入了 MySQL 的驱动之后,JDBC 就能根据之前的服务注册文件找到对应的实现类
- 接下来,可以使用 JDBC 连接、操作数据库了