目录
1 是什么
Java Service Provider Interface(Java spi,Java服务提供程序接口),是在Java 6引入的用于发现和加载指定接口匹配实现类的功能,简称服务程序提供接口。
主要好处:面向接口编程,将服务定义和具体实现分离,实现解耦,实现动态加载模块化。
主要定义了四个组件,具体信息如下,这四者关系如下图:
1.1 服务
一组编程接口和实现类,提供应用的功能,或对外部资源的访问能力。
1.2 服务提供者接口
充当服务的接口SPI
1.3 服务提供商
SPI的具体实现,通过放到资源目录META-INF /services中的提供商配置文件进行配置和识别的。文件名是 SPI 的完全限定名,其内容是 SPI 实现的完全限定名。
1.4 服务加载器
加载我们特定目录的实现类,SPI 的核心是 ServiceLoader 类。这具有延迟发现和加载实现的作用。它使用上下文类路径来查找提供程序实现,并将它们放在内部缓存中。
2 源码学习
主要是利用ServiceLoader加载实现类,并放在内部缓存中,服务加载器就是ServiceLoader。
以Driver加载数据库驱动为例,我们从如下测试入手:
//load作用是实例化ServiceLoader,设置内部相应属性
ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
//获取ServiceLoader的惰性迭代器
Iterator<Driver> driverIterator = load.iterator();
//hasNext执行真正的加载步骤
while (driverIterator.hasNext()) {
System.out.println(driverIterator.next().getClass().getName());
}
2.1 ServiceLoader.load(Driver.class)处理逻辑
进入load方法
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
获取当前线程的上下文 ClassLoader,并调用load方法,该方法就是执行`return new ServiceLoader<>(service, loader),构造函数如下:
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();
}
主要是判空处理,和获取安全管理器,并执行reload方法:
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
获取内部类LazyIterator实例,通过上述步骤实例化了ServiceLoader和如下属性:
2.2 加载实现类
获取ServiceLoader内部的迭代器:
public Iterator<S> iterator() {
//返回迭代器实例
return new Iterator<S>() {
//缓存已经知晓的实现类和名字
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
//实现Iterator里hashNext和next方法主要是利用内部类LazyIterator的hashNext和next方法,返回迭代器不支持移除
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
由于在前边实例化ServiceLoader时执行了providers.clear(),所有返回的迭代器实例knownProviders为null,惰性加载就是当我们执行前边测试程序driverIterator.hasNext()时执行加载实例化过程。
进入到LazyIterator的hasNext:
可以看出主要是调用内部的hashNextService方法:
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//获取资源位置并加载资源:这里为META-INF/services/java.sql.Driver
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
//解析资源,按照行进行解析,去除空格..放入name列表,并返回name的迭代器
pending = parse(service, configs.nextElement());
}
//实现类全限名赋予nextName
nextName = pending.next();
return true;
}
主要流程:
- 获取资源位置并加载资源:这里为META-INF/services/java.sql.Driver
- 解析资源,按照行进行解析,去除空格…放入name列表,并返回name的迭代器
- 实现类全限名赋予nextName
进入到LazyIterator的next:
主要是进入到nextService():
private S nextService() {
//判断是否存在服务
if (!hasNextService())
throw new NoSuchElementException();
//获取要加载的全限定名
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//将类加载进jvm
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
//断定是否实现自服务接口
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//创建实例
S p = service.cast(c.newInstance());
//放入providers缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
主要流程:
- 判断是否存在服务实现类
- 获取要加载的全限定名
- 将类加载进jvm
- 断定是否实现自服务接口
- 创建实例,放入providers缓存,返回实例
3 扩展实例
首先定义接口模块、实现类模块、接口和实现类的start模块(方便导入)和测试模块
3.1 接口模块
定义子模块hello-son2其中定义接口MyInterface:
3.2 实现类模块
定义子模块hello-son1,实现类MyImp:
上图中我们需要创建services目录,并创建对应接口的文件,内容如下:
文件名定义为全限定接口,内容为实现类全限定名。
3.3 打包和安装到本地仓库
打包时注意将resources内容打包到META-INF下:
之后创建一个hello-parent-start打包上述两个maven模块:
由于hello-son1依赖了hello-son2所有这里只添加了hello-son1
3.4 测试模块
首先导入依赖:
编写测试类:
运行结果:
参考文献
[1] Java 服务提供程序接口.