简介
SPI全称为Service Provider Interface。是一种为项目留下扩展点的非常好的方式。我们事先在项目中留下Service的接口。通过配置文件就可以控制生成哪些具体的Service实现类。
一个例子
我们创建一个maven项目。创建一个Service接口:
public interface MyService {
void service();
}
还有两个MyService
的实现类:
public class MyServiceA implements MyService {
@Override
public void service() {
System.out.println("MyService A");
}
}
和
public class MyServiceB implements MyService {
@Override
public void service() {
System.out.println("MyService B");
}
}
根据SPI约定,决定如何生成实现类的配置文件位于classpath
的META-INF/services
目录。配置文件的名称为Service接口的全名,包含包名和接口名称。例如包名为com.paul
,那么配置文件名为com.paul.MyService
。文件的内容只需要将需要创建出的实现类全名列出。比如文件内容为:
com.paul.MyServiceA
com.paul.MyServiceB
程序会创建出MyService
两个实现类:MyServiceA
和MyServiceB
。
接下来是加载实现类的方法,JDK提供的写法非常的简洁:
public class MyServiceLoader {
public static void main(String[] args) {
ServiceLoader myServices = ServiceLoader.load(MyService.class);
for (MyService myService : myServices) {
myService.service();
}
}
}
通过ServiceLoader
的load
方法,创建出一个ServiceLoader
对象。迭代这个对象就能够获取所有的实现类。
上面的例子中服务接口和实现类在同一个项目中。实际上不同服务的实现类可以在不同的maven子项目中,配合与之对应的配置文件,打成jar包发布。项目部署的时候,在classpath放入不同的jar包,服务调用端不需要修改任何代码,就能够更改服务的行为,从而实现应用功能的扩展,非常的方便。
源代码分析
想明白SPI的实现方式,阅读源代码是免不了的。
我们从上面例子的ServiceLoader.load(MyService.class)
调用开始分析源代码。load
方法代码如下:
public static ServiceLoader load(Class service) {
// 获取当前线程的context classloader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 调用另一个重载方法,传入classloader
return ServiceLoader.load(service, cl);
}
public static ServiceLoader load(Class service,
ClassLoader loader)
{
// 新建一个ServiceLoader
return new ServiceLoader<>(service, loader);
}
继续查看ServiceLoader
的构造方法:
private ServiceLoader(Class svc, ClassLoader cl) {
// 检查svc不能为null
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 如果没传入classloader,则使用系统的classloader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// 获取访问控制器,Java安全机制
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 调用reload方法
reload();
}
reload
方法内容如下所示。其中providers
变量为一个map,保存了服务具体实现类的名称(包含包名)和实现类这个对象本身。这是ServiceLoader
的一种缓冲机制,同时ServiceLoader
使用了懒加载,实现类只有第一次迭代ServiceLoader
的时候才会创建出,加入到providers
缓存中再返回。以后无论再迭代多少次,都是从providers
缓存中获取。
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
LazyIterator
为懒加载迭代器。这里先不介绍,到调用到它的时候再分析。
ServiceLoader
创建完毕后我们开始迭代它了。创建迭代器的逻辑在ServiceLoader
的iterator
方法:
public Iterator iterator() {
return new Iterator() {
// 创建一个providers缓存的key值迭代器
Iterator> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 先迭代已缓存的实现类,如果没有,则迭代懒加载迭代器
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
// 同样,如果providers保存的有实现类,优先返回
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 否则去懒加载迭代器查找下一个实现类
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
到这里我们需要继续分析LazyIterator
的next
方法了。
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction action = new PrivilegedAction() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
先忽略AccessController
这块儿,next
方法调用的是nextService
方法,继续查看。
private S nextService() {
// 如果没有下一个service,抛出异常
// 这个方法很重要,稍后分析
if (!hasNextService())
throw new NoSuchElementException();
// nextName变量保存的是下一个要迭代的服务实现类全名
String cn = nextName;
nextName = null;
Class> c = null;
try {
// 使用反射创建出这个实现类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
// 如果实现类c不是Service类型,报错退出
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 将c转换为Service类型
S p = service.cast(c.newInstance());
// 将c放入providers缓存
providers.put(cn, p);
// 返回转换为Service类型的实现类
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
hasNextService
方法如下所示。
private boolean hasNextService() {
// 如果有下一个要迭代的实现类名,返回true
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX为META-INF/services/,正是配置文件的位置
// 这里在拼接配置文件路径
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);
}
}
// 一行一行的解析配置文件
// pending是获取解析过的配置文件内容的迭代器
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
// 如果pending和configs都没有下一个元素,返回false
return false;
}
// 解析配置文件,返回迭代器
pending = parse(service, configs.nextElement());
}
// 将配置文件中待解析的一行配置放入nextName变量中。
nextName = pending.next();
return true;
}
到这里ServiceLoader
的实现原理我们已经清楚了。使用的仍然是Java 反射方式,从约定好位置的配置文件中读取各个实现类的名称,通过Class.forname
方式创建出实例并返回。