1、什么是SPI机制
SPI是Service Provider Interface的简写,是JDK内置的一种服务提供发现机制,是一种扩展机制。常见的框架使用SPI机制方式:
- JDBC驱动加载:可以根据不同的数据库厂商引入不同的JDBC驱动实现
- springboot的SPI机制:可以在spring.factories中加入自定义的配置类、事件监听和初始化器等
- Dubbo的SPI机制:提供了如集群扩展,路由扩展和负载扩展等扩展点,可以自有替换原有的功能配置。
2、如何使用SPI
- 首先需要定义一个接口
- 针对该接口,实现出类
- 在jar包的resources目录下,新建META-INF/services目录,并创建“接口全限定名”为命名的文件,内容为实现类的全限定名
- 主程序通过java.util.ServiceLoder动态装载实现类,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
- SPI的实现类必须携带一个不带参数的构造方法;
3、Java SPI的源码分析
首先看ServiceLoader类源码
public final class ServiceLoader<S> implements Iteriable<S>{
private static final String PREFIX = "META-INF/services/";
private final class<S> service;
private final ClassLoader loader;
private final AccessControlContext acc;
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
private LazyIterator lookupIterator;
private ServiceLoader(Class<S> svc, ClassLoader cl){
service = Objects.requireNonNull(svc, "");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public static <S> ServiceLoader<S> load(Class<S> svc){
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(srv, cl);
}
public static <S> ServiceLoader<S> load(Class<S> svc, ClassLoader cl){
return new ServiceLoader(svc, cl);
}
public void reload(){
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private static class LazyIterator implements Iterator<S>{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> services, ClassLoader loader){
this.service = service;
this.loader = loader;
}
public boolean hasNext(){....}
public S next(){...}
public void remove(){...}
}
}
接看reload 方法,其中使用到了LazyIterator对象,该对象的作用是在使用到定义的SPI类时才会去加载接口实现。
public boolean hasNext(){
if(acc == null){
return hasNextService();
}else{
PrivilegedAction<Boolean> action = new PrivilegedAction<>(){
public Boolean run(){return hasNextService();}
}
return AccessController.doPrivileged(action, acc);
}
}
public boolea 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);
}
}...
}
while((pending == null)|| !pending.hasNext()){
if(!configs.hasMoreElements()){
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
从hasNext()方法可以看到,只会去META-INF/services/目录下取出一个服务提供者实现类的全限定名赋值给nextName;这里使用的懒加载,是在用到真正的服务的时候才会去加载服务提供者。
再往下看LazyIterator的next()方法,
public S next(){
if(acc == null){
return nextService();
}else{
...
}
}
public S nextService(){
if(!hasNextService()){
throw new NoSuchElementException();
}
String cn = nextName;
nextName = null;
Class<?> c = null;
try{
c= Class.forName(c, flase, loader);
}...
try{
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}...
}
SPI的接口是Java核心库的一部分,是由引导类加载器(Bootstrap Classloader)来加载的,SPI的实现类是由系统类加载器(System ClassLoader)来加载的。具体的Demon有:
public interface IHello{
String getHello(String name);
}
创建目录:
- src
-main
-resources
- META-INF
- services
- com.my.test.IHello
目录内容:com.my.test.IHelloService
使用 ServiceLoader 来加载配置文件中指定的实现。
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<IHello> ih = ServiceLoader.load(IHello.class);
for (IHello s : ih) {
s.hello();
}
}
}
3、Dubbo的SPI机制
dubbo的扩展点加载从JDK标准的SPI(Service Provider Interface)扩展点发现机制加强而来,主要实现是在ExtensionLoader类中。当接口上打了@SPI注解时,dubbo会在META-INF/dubbo/internal/接口全名文件下读取扩展点.配置文件内容为name->implementation class,可以声明多个提供者。
Java SPI的缺点:
- 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,即接口的实现类全部加载并实例化一遍
- 获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用ServiceLoader类的实例是不安全的。
- 如果扩展点加载失败,出现的错误信息不明确,连扩展点的名称都拿不到。
dubbo SPI 的区别:
- dubbo spi增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
- JDK的spi是通过System ClassLoader,Dubbo spi是通过Extend ClassLoader