一、简介
日常开发中,我们经常接触API
的概念,API(Application Programming Interface)
主要是给应用开发者使用的。而SPI(Service Provider Interface)
主要是给框架开发者使用的。根据面向对象中的开闭原则,当软件需要变化时,尽量通过拓展的形式来实现,而不是通过修改代码的形式来实现。对于框架的开发尤其如此,一旦在代码中涉及到具体的实现类,如果需要替换成另一种实现类,就要去修改源码。这个时候就希望有一种服务发现的机制,可以动态的替换实现类,SPI
就是JDK内置的一种服务提供发现机制。
二、SPI简单示例
1. 定义接口
package top.liaohuaida.spi;
public interface Command {
void execute();
}
2. 实现接口
我们实现两个实现类:
package top.liaohuaida.spi.impl;
import top.liaohuaida.spi.Command;
public class OnCommand implements Command{
public void execute() {
System.out.println("On Command.");
}
}
package top.liaohuaida.spi.impl;
import top.liaohuaida.spi.Command;
public class OffCommand implements Command {
public void execute() {
System.out.println("Off Command.");
}
}
3. 配置文件
需要放置在META-INF/services/接口全限定名
文件内,例如META-INF/services/top.liaohuaida.spi.Command
文件下的配置文件:
top.liaohuaida.spi.impl.OnCommand
top.liaohuaida.spi.impl.OffCommand
目录结构如下所示:
4. 测试
public class SpiTest {
public static void main(String[] args) {
ServiceLoader<Command> commandServiceLoader = ServiceLoader.load(Command.class);
for (Command command : commandServiceLoader) {
command.execute();
}
}
}
运行结果如下所示:
On Command.
Off Command.
当然因为配置文件配置了两个Commmand
实现类,所以commandServiceLoader
就有两个Command
,如果对相应的配置文件进行修改,就能实现接口实现类的动态配置。
三、JAVA类加载机制和SPI
1. 类加载机制的缺陷
我们知道,JAVA类加载使用双亲委派模型:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此 所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
也就是说,这个委托是单向的,即顶层的类加载器无法访问底层的类加载器加载的类,比如启动类加载器不会去访问拓展类加载器加载的类,拓展类加载器也不会去访问应用类加载器加载的类。
而在使用SPI
的场景中,JDK在核心类库中,提供一个SPI
接口,并且该接口还提供了一个工厂方法用于创建该接口的实例,但是该接口的实现交由服务提供者来实现。然而实现类大多在应用层中使用应用类加载器加载,所以这些核心类库中的工厂方法无法创建应用层的接口实现类实例。
2. 类加载机制的补充
为了解决上诉类加载机制双亲委派模型带来的的问题,JAVA引入了线程上下文类加载器(ContextClassLoader)
。java.lang.Thread
类中有两个方法:
public ClassLoader getContextClassLoader()//获取线程中的上下文类加载器
public void setContextClassLoader(ClassLoader cl)//设置线程中的上下文类加载器
通过setContextClassLoader
方法,可以将线程上下文类加载器设置为特定的类加载器,如果没有显式调用set
方法,线程将继承其父线程的上下文类加载器,而Java 应用运行的初始线程的上下文类加载器是应用类加载器。也就是说默认情况,getContextClassLoader
获取到的是应用类加载器。
通过线程上下文加载器,JAVA就能在核心库中获取到应用层中的SPI
实现类我们来看下ServiceLoader
的load
方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器(通常是应用类加载器)
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 使用获取到的类加载器来加载应用层中的类
return ServiceLoader.load(service, cl);
}
load
方法中,通过线程上下文类加载器,位于核心类库的ServiceLoader
也可以加载应用层的SPI
接口实现类。
四、SPI的实际案例
在平时的开发工作中,经常碰到的SPI
的例子就是JDBC
。JDK在核心库中提供了一个SPI
接口java.lang.Driver
,数据库厂商就会提供特定的数据库驱动实现。我们在代码中会使用如下代码获取数据库连接:
Connection conn = DriverManager.getConnection(URL, USER, PASS);
我们并没有显式加载驱动,而是由DriverManager
来帮我们获取具体的数据库驱动,下面是DriverManager
一段简略的代码,从中我们可以看到熟悉的ServiceLoader.load(Driver.class)
方法。
static {
// 加载初始化驱动
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
// ...省略
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 使用ServiceLoader.load来加载驱动
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
//...省略
});
//...省略
}