JDK的SPI原理及源码分析
Posted 听闻有感1
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK的SPI原理及源码分析相关的知识,希望对你有一定的参考价值。
SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。java语言的特性,一处编译处处运行,很大程度上是因为使用了使用spi机制。
JDK在rt.jar包中定义了很多的接口,这些接口由于各种原因没有给出实现类,
操作系统不同,实现方式不同,例如nio底层的selector实现类,不同os有自己的实现
服务商不同,实现方式不同,例如jdbc,不同的数据库服务商对jdbc都有自己的实现
统一调用接口,例如slf4j
SPI是门面模式的一种应用场景,在平时的开发过程中,如果发现一个模块需要集成多个平台同一个功能,不妨考虑使用这种机制,比如支付功能、对象存储功能等等。此外dubbo为了集成多协议多平台,对spi的使用非常多
SPI简单使用
定义接口类
public interface SPIService {
void execute();
}
然后,定义两个实现类
public class SpiImpl1 implements SPIService{
public void execute() {
System.out.println("SpiImpl1");
}
}
public class SpiImpl2 implements SPIService{
public void execute() {
System.out.println("SpiImpl2");
}
}
最后呢,要在ClassPath路径下配置添加一个文件。文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。文件路径为:
resources/META_INF/services/com.pd.spi.SPIInterface
文件内容为:
com.pd.spi.SpiImpl1
com.pd.spi.SpiImpl2
在测试代码中,我们使用ServiceLoader.load
或者Service.providers
方法拿到实现类的实例
public class Test {
public static void main(String[] args) {
ServiceLoader<SPIInterface> spiImpls = ServiceLoader.load(SPIInterface.class);
for(SPIInterface impl : spiImpls){
impl.execute();
}
}
}
输出:
SpiImpl1
SpiImpl2
SPI源码分析
两种服务获取方式:
Service.providers
包位于sun.misc.Service
,
ServiceLoader.load
包位于java.util.ServiceLoader
首先看一下ServiceLoader
类的成员:
public final class ServiceLoader<S> implements Iterable<S>{
//配置文件的路径前缀
private static final String PREFIX = "META-INF/services/";
// 需要加载的服务类接口类型对象
private final Class<S> service;
// 类加载器
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// 已加载的服务类实现集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 真正加载逻辑所在的对象,内部类
private LazyIterator lookupIterator;
}
静态方法load()
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
可以看到,这里获取了线程上下文类加载器来加载实现类,双亲委派模式的破坏者。
调用了重载的load()
方法
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
调用了私有的构造函数,由于实现类对象最终还是保存到了ServiceLoader
的成员变量providers
中,所以这里猜测,在构造方法中完成了实现类的获取和实例化:
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
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
到此初始化流程就结束了,构造方法中并没有加载的过程啊,猜测错误。
原来这个类名称是LazyIterator
,原来这里使用了懒加载,当ServiceLoader
初始化的时候并不会主动去加载实现类,而是在用户代码中使用到实现类的时候再进行加载。
当用户代码执行到此:
for(SPIInterface impl : spiImpls){
impl.execute();
}
将会执行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()
方法
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;
}
// 解析配置文件,返回一个ArrayList的迭代器对象
pending = parse(service, configs.nextElement());
}
//nextName指向迭代器指向的那个对象,是实现类的全类名
nextName = pending.next();
return true;
}
拿到实现类的全类名,现在开始实例化:
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()
方法:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
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 {
Sp = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated",x);
}
throw new Error(); // This cannot happen
}
使用反射的方式实例化实现类
总结:
1、 Jdk的spi 会一次性加载并实例化扩展点的所有实现,就是如果在MATA-INF/services下的文件里面加了N个实现类,那么JDK启动的时候都会一次性全部实例化,那么如果有的扩展点初始化很耗时,且运行时并没有用到,那么就会很浪费资源(堆)
2、 扩展点加载失败,会导致调用方报错,而且这个错误很难定位到时这个原因。
因此Dubbo在使用SPI时,对其做了很多的优化。
以上是关于JDK的SPI原理及源码分析的主要内容,如果未能解决你的问题,请参考以下文章