SPI 全称为 Service Provider Interface
,是一种服务发现机制,其核心在于将接口实现类的全限定名配置在文件中,由服务加载器读取并加载实现类,从而可在运行时为接口动态替换实现类,为程序提供拓展功能。
1. 重新阐述 Java SPI 示例
- 定义接口和实现类:
首先定义一个名为
Robot
的接口,包含sayHello
方法。public interface Robot { void sayHello(); }
然后创建两个实现类 OptimusPrime
和 Bumblebee
,它们都实现了 Robot
接口并实现了 sayHello
方法。
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
配置文件:
- 在
META-INF/services
文件夹下创建名为org.apache.spi.Robot
的文件(这里假设接口全限定名为org.apache.spi.Robot
),文件内容包含实现类的全限定名:
- 在
org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee
- 测试代码:
public class JavaSPITest {
@Test
public void sayHello() throws Exception {
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
System.out.println("Java SPI");
// 两种遍历模式
serviceLoader.forEach(Robot::sayHello);
Iterator<Robot> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
Robot robot = iterator.next();
// 可以调用 robot.sayHello() 进行测试
}
}
}
2. 经典 Java SPI 应用:JDBC DriverManager
- 传统 JDBC 驱动加载:
在JDBC4.0
之前,需要先使用Class.forName("com.mysql.jdbc.Driver")
加载数据库驱动,然后进行连接操作。
// STEP 1: Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
// STEP 2: Open a connection
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);
使用 SPI 后的 JDBC 驱动加载:
JDBC4.0
之后利用 Java 的 SPI 扩展机制,无需手动调用Class.forName
加载驱动,可直接获取连接。DriverManager
类是驱动管理器,在其静态初始化块中调用loadInitialDrivers
方法,其中涉及 SPI 的使用:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
加载驱动的四个步骤:
- 从系统变量获取驱动定义。
- 用 SPI 获取驱动实现类(字符串形式)。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
- 遍历使用 SPI 获取的实现,实例化各实现类。
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
- 根据第一步的驱动列表实例化具体实现类。
3. Java SPI 机制源码解析
ServiceLoader 类的 load
方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
该方法根据服务类型和类加载器创建 ServiceLoader
对象。
- `ServiceLoader` 构造器:
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager()!= null)? AccessController.getContext() : null;
reload();
}
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
私有构造器创建懒迭代器 LazyIterator
对象,只有调用迭代方法时才执行加载逻辑。
- 迭代器的
hasNext()
方法:
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
该方法最终调用 hasNextService
方法,会通过加载器获取配置对象并解析 META-INF/services/
目录下的文件。
- 迭代器的
next()
方法:
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
本质是调用 nextService
方法,通过反射加载类对象,实例化类并缓存到 providers
对象中。
4. Java SPI 机制的缺陷
- 无法按需加载,会遍历并实例化所有实现,即使部分实现类不需要或实例化耗时。
- 获取实现类的方式仅能通过 Iterator,不够灵活,不能根据参数获取特定实现类。
- 多线程使用
ServiceLoader
实例时不安全。
5. Spring SPI 机制
创建接口和实现类:
定义 MyTestService
接口:
public interface MyTestService {
void printMylife();
}
创建实现类 WorkTestService
和 FamilyTestService
:
public class WorkTestService implements MyTestService {
public WorkTestService(){
System.out.println("WorkTestService");
}
public void printMylife() {
System.out.println("我的工作");
}
}
public class FamilyTestService implements MyTestService {
public FamilyTestService(){
System.out.println("FamilyTestService");
}
public void printMylife() {
System.out.println("我的家庭");
}
}
- 配置文件:
在 META-INF/spring.factories
中配置接口和实现类:
com.courage.platform.sms.demo.service.MyTestService = com.courage.platform.sms.demo.service.impl.FamilyTestService,com.courage.platform.sms.demo.service.impl.WorkTestService
- 测试代码:
List<MyTestService> myTestServices = SpringFactoriesLoader.loadFactories(
MyTestService.class,
Thread.currentThread().getContextClassLoader()
);
for (MyTestService testService : myTestServices) {
testService.printMylife();
}
与 Java SPI 的区别:
- Java SPI 一个服务接口对应一个配置文件,Spring SPI 一个
spring.factories
配置文件存放多个接口及实现类,以接口全限定名作为 key,实现类作为 value,多个实现类用逗号分隔。 - 两者都无法获取特定的实现,只能按顺序获取所有实现。
- Java SPI 一个服务接口对应一个配置文件,Spring SPI 一个
6. Dubbo SPI 机制
配置文件:
- 配置文件放在
META-INF/dubbo
路径下,以键值对方式配置实现类:
- 配置文件放在
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
- 测试代码:
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
特点:
- 支持按需加载接口实现类,可通过键值对方式指定要加载的实现。
- 相比 Java SPI 增加了 IOC 和 AOP 等特性。
近期才哥整理出了一个可用于快速刷面试题的小程序,其中收录了常见面试题及其答案,涵盖了基础、并发、JVM、MySQL、Redis、Spring、SpringMVC、SpringBoot、SpringCloud、消息队列等多个类型,感兴趣的可以点击下方试试。
评论 (0)