一、SPI概念
SPI ,全称Service Provider Interface,从字面理解SPI 就是 Service 提供者接口,是一种服务发现机制。它通过查找 ClassPath 路径下的 META-INF/services 文件夹中的文件,自动加载文件里所定义的类。
SPI 机制在 Dubbo、common-logging、JDBC等框架中都有应用,是厂商为开发者预留的扩展框架能力的的接口,为框架扩展提供了可能。
在日常开发中都是将问题抽象成各种 API 接口,然后提供各种实现,这些API接口的实现都是封装在我们的 Jar 或者框架中,当我们想提供一种新的实现时可以不修改原来的代码只需实现API 接口就可以了 ,但我们还是生成新 Jar 或框架(虽然可以通过在代码里扫描某个目录已加载Api的新实现,但这不是 Java 的机制,只是hack方法),通过Java SPI 机制我们可以在不修改 Jar 或者框架的时候为 API 接口提供新实现。
1. 小例子
1.1 先定义一个需要实现的接口,如下
package com.myzuji.study.spi;
public interface SPIService {
void execute(); }
1.2 在定义两个实现,如下
package com.myzuji.study.spi.impl;
import com.myzuji.study.spi.SPIService;
public class SPIServiceImpl1 implements SPIService {@Override
public void execute() {
System.out.println("SPIImpl1.excute()");
}
}
package com.myzuji.study.spi.impl;
import com.myzuji.study.spi.SPIService;
public class SPIServiceImpl2 implements SPIService {
@Override
public void execute() {
System.out.println("SPIImpl2.excute()");
}
}
1.3 SPI 配置,如下
在ClassPath路径下的META-INF/services文件夹下创建一个文件,文件名为接口的全限定名com.myzuji.study.spi.SPIService,文件内容为实现类的全限定名,多个实现类用换行符分隔,如下
com.myzuji.study.spi.impl.SPIServiceImpl1 com.myzuji.study.spi.impl.SPIServiceImpl2
2. 测试
然后我们就可以通过ServiceLoader.load或者Service.providers方法拿到实现类的实例。其中,Service.providers包位于sun.misc.Service,而ServiceLoader.load包位于java.util.ServiceLoader。代码如下:
package com.myzuji.study.spi; import sun.misc.Service; import java.util.Iterator; import java.util.ServiceLoader; public class SPITest { public static void main(String[] args) { Iteratorproviders = Service.providers(SPIService.class); while (providers.hasNext()) { SPIService spiService = providers.next(); spiService.execute(); } System.out.println("============"); ServiceLoader loader = ServiceLoader.load(SPIService.class); Iterator spiServiceIterator = loader.iterator(); while (spiServiceIterator.hasNext()) { SPIService spiService = spiServiceIterator.next(); spiService.execute(); } } } 输出结果是: SPIImpl1.excute() SPIImpl2.excute() ============ SPIImpl1.excute() SPIImpl2.excute()
小例子的目录结构
com/myzuji/study/spi/impl/SPIServiceImpl1.java com/myzuji/study/spi/impl/SPIServiceImpl2.java com/myzuji/study/spi/SPIService.java com/myzuji/study/spi/SPITest.java META-INF/services/com.myzuji.study.spi.SPIService
二、源码分析
在运行测试方法时会发现有两种加载方式,分别位于 JDK 的 sun.misc 和 java.util 包中, sun 包下的 源码看不到,就以 ServiceLoader.load() 为例,通过源码看看它里面到底怎么做的。
通过上面的测试用例可以发现在调用 ServiceLoader.load(SPIService.class) 时,进入
public static ServiceLoader load(Class service) { //获取当前线程上下文加载器 ClassLoader cl=Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service,cl); }
继续进入 ServiceLoader.load(service,cl)
public static ServiceLoader load(Class service,ClassLoader loader) { return new ServiceLoader<>(service, loader); }
返回一个 ServiceLoader 实例,继续看下它的构造函数
public final class ServiceLoader implements Iterable { //配文件路径 private static final String PREFIX = "META-INF/services/"; //代表被加载的服务类或接口 private final Class service; //用于定位,加载和实例化的类加载器 private final ClassLoader loader; //创建 ServiceLoader 时采用的访问控制上下文 private final AccessControlContext acc; //已加载的服务类集合 private LinkedHashMapproviders = new LinkedHashMap<>(); //内部类,真正加载服务的类 private LazyIterator lookupIterator; public void reload(){ //先清空 providers.clear(); //实例化内部类 lookupIterator=newLazyIterator(service,loader); } private ServiceLoader(Class 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(); }
在调用 Iterator<SPIService> spiServiceIterator=loader.iterator(); 时返回了一个 Iterator 容器
public Iterator iterator() { return new Iterator() { Iterator> knownProviders = providers.entrySet().iterator(); public boolean hasNext() { //第一次执行 hasNext() knownProviders 的 size 为 0 会继续执行lookupIterator.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(); } }; }
进入lookupIterator.hasNext() 最终会进入到内部类 LazyIterator 的 hasNextService()中
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction action = new PrivilegedAction() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
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());
}
nextName = pending.next();
return true;
}
当调用 spiServiceIterator.next(); 方法时,进入到内部类 LazyIterator的nextService() 中
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction action = new PrivilegedAction() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class c = null;
try {
//生成名称为 cn 的 Class 对象,不进行初始化
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.put(cn, p);
//返回实例
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",x);
}
throw new Error(); // This cannot happen
}
通过上面的分析可以看到 ServiceLoader 不是立即实例化服务实现者的,只有等到需要时才通过Iterator 实现获取对应的服务提供者才会加载对应的配置文件进行解析,具体来说是在调用Iterator的hasNext方法时会去加载配置文件进行解析,在调用next方法时会将对应的服务提供者进行实例化并进行缓存。所有的配置文件只加载一次,服务提供者也只实例化一次,如需要重新加载配置文件可调用ServiceLoader的reload方法。
三、 总结
优点:实现解耦,应用程序可以根据实现业务情况启用或替换组件
缺点:
> 不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
> 获取某个实现类的方式不够灵活,只能通过Iterator 形式获取,不能根据某个参数类获取对应的实现类。
> 多个并发多线程使用ServiceLoadder 类的实例不安全。
> 加载不到实现类时抛出并不是真正原因的异常,错误难定位。
鉴于SPI 的诸多缺点,很多系统都是自己实现一套类加载机制,如DUBBO。