共计 6656 个字符,预计需要花费 17 分钟才能阅读完成。
引语
平时 API 倒是听得很多?SPI 又是啥. 别急我们来先看看面向接口编程的调用关系,来了解一下,API 和 SPI 的相似和不同之处。
SPI 理解
先来一段官话的介绍:SPI 全称为 (Service Provider Interface),是 JDK 内置的一种服务提供发现机制.(听了一脸懵逼)好的,我们结合图片来理解一下。
简单的来说分为调用方,接口,服务方. 接口就是协议,契约,可以调用方定义,也可以由服务方定义,也就是接口是可以位于调用方的包或者服务方的包.
1. 接口的定义和实现都在服务方的时候,仅暴露出接口给调用方使用的时候,我们称为 API;
2. 接口的定义在调用方的时候 (实现在服务方),我们给它也取个名字 –SPI。
应该还比较好理解吧?
SPI 的使用场景
SPI 在框架中其实有很多广泛的应用,这里列举几个例子:
1.Mysql 驱动的选择 driverManager 根据配置来确定要使用的驱动;
2.dubbo 框架中的扩展机制(dubbo 官网链接)
使用实例
看完上面的简介和 SPI 在框架中的应用,想必对 SPI 在读者的大脑中已经产生了一个雏形,talk is cheap!show me the code. 说了这么多, 我们具体写一个简单的例子来看看效果, 验证一下 SPI.
1. 首先定义一个接口, 忍者服务接口
public interface NinjaService {void performTask();
}
2. 接下来写两个实现类,ForbearanceServiceImpl(上忍),ShinobuServiceImpl(下忍)
public class ForbearanceServiceImpl implements NinjaService {
@Override
public void performTask() {System.out.println("上忍在执行 A 级任务");
}
}
public class ShinobuServiceImpl implements NinjaService {
@Override
public void performTask() {System.out.println("下忍在执行 D 级任务");
}
}
3. 接下来我们在 main/resources/ 下创建 META-INF/services 目录, 并且在 services 目录下创建一个 com.scott.java.task.spi.NinjaService(忍者服务类的全限定名)的文件.
4. 创建一个 Client 场景类来调用看看结果
很完美的调用了两个实现类的 performTask()方法.
5. 最后贴一下目录结构
附上一波代码例子的地址, 在 spi 里面,git 链接;
SPI 源码简单分析
1. 先看下核心类 ServiceLoader 的定义和属性
// 继承了 Iterable 类 遍历的时候使用
public final class ServiceLoader<S> implements Iterable<S>
{
// 这就是为啥需要在 META-INF/services/ 目录下创建服务类的文件
private static final String PREFIX = "META-INF/services/";
// 被加载的服务
private final Class<S> service;
// 类加载器
private final ClassLoader loader;
// 访问控制类
private final AccessControlContext acc;
// 实现类的缓存 根据初始化的顺序 也就是在 /services/ 文件中的定义顺序来定义的加载顺序
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒加载 iterator
private LazyIterator lookupIterator;
2. 然后从 client 开始, 然后依次 debug 进去
ServiceLoader<NinjaService> ninjaServices = ServiceLoader.load(NinjaService.class);
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前的类加载器 也就是 AppClassLoader
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);
}
后面的就省略了, 因为这里仅仅就是根据 NinjaService 初始化的事项, 没有什么很难理解的点.
3. 我们在看看具体的调用过程, 这里使用的是 client 对应的 class 文件, 因为增加 for(foreach)在 java 中是个语法糖, 实际上编译后是这样的内容
public static void main(String[] args) {ServiceLoader<NinjaService> ninjaServices = ServiceLoader.load(NinjaService.class);
// 这里一下其实就是增加 for 解糖后的代码 有兴趣可以去了解下 java 的语法糖
Iterator var2 = ninjaServices.iterator();
while(var2.hasNext()) {NinjaService item = (NinjaService)var2.next();
item.performTask();}
}
4. 随着断点继续走, 我们进入到 var2.hasNext()的方法
public boolean hasNext() {
// knownProviders 还没有加载过 provider 走下面的分支
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();}
这里 lookupIterator 上面 ServiceLoader 的属性介绍过, 它其实是 ServiceLoader 中的一个 Iterator 的内部类。然后调用了内部类 Iterator 的 hasNext()方法。
public boolean hasNext() {// acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// ServiceLoader 初始化没有设置过 securityManager, 所以 acc 是 null, 进入 hasNextService()
if (acc == null) {return hasNextService();
} else {PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {public Boolean run() {return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
5.hasNextService()分析
private boolean hasNextService() {if (nextName != null) {return true;}
if (configs == null) {
try {
// 这里加载了 META-INF/services 下的文件 也就是含有两个实现类全限定名的文件
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 因为 loader 是不为 null 的 AppClassLoader
configs = loader.getResources(fullName);
} catch (IOException x) {fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}
// 这里是将上面加载的文件中的两个实现类的文件
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
6. 继续看到 parse 方法, 这里最后返回的是含有两个全限定类名的 Iterator<String>, 其实就是把 services/ 下的文件内容给加载出来
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {fail(service, "Error reading configuration file", x);
} finally {
try {if (r != null) r.close();
if (in != null) in.close();} catch (IOException y) {fail(service, "Error closing configuration file", y);
}
}
return names.iterator();}
附带的说一下 parseLine(service, u, r, lc, names), 检查类名是否符合规范, 符合的话添加到 Iterator 中, 到这里 var2
.hasNext()执行完毕, 结果是加载了 services 下的文件内容
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{String ln = r.readLine();
if (ln == null) {return -1;}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {if ((ln.indexOf('') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name:" + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name:" + ln);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
private boolean hasNextService() {if (nextName != null) {return true;}
if (configs == null) {
try {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;}
pending = parse(service, configs.nextElement());
}
// 这里将下一个实现类的名字赋值给了 LazyIterator 的属性 nextName
nextName = pending.next();
return true;
}
7. 接下来执行的是 NinjaService item = (NinjaService)var2.next()的 next(方法), 然后继续 debug 进去, 这里我省略了一些方法的调用, 只展示出有用的方法这个是 ServiceLoader 的内部类 LazyIterator 的 nextService()方法.
private S nextService() {if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 这里 nextName 在上面已经赋值过了 所以反射创建实例
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());
// 将类添加到 ServiceLoader 的 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}
8. 到这里子类的实现类返回, 分析就结束了.
总结:
1. 了解了什么是 SPI;
2.SPI 和 API 的简单区别和联系;
3. 学习了怎么使用 SPI 来扩展服务;
4. 分析了 ServiceLoader 的源码加载过程,这里扯一句,简单的就是 META-INF/services 定义好要实现的接口 (文件名) 和实现类(文件内容),
ServiceLoader 加载的时候没有实例化实现类, 而是在 Iterator 遍历的时候去用反射创建了实例.
觉得写得还行的可以点个赞, 关注一波, 后面会继续写更好的文章~ XD
参考资料:
1.http://cr.openjdk.java.net/~m…
2.https://www.cnblogs.com/happy…
3.http://dubbo.apache.org/zh-cn…
4.https://www.cnblogs.com/googl…
5.https://zhuanlan.zhihu.com/p/…