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.SpiImpl1com.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();}}}
输出:
SpiImpl1SpiImpl2
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 createdprivate 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时,对其做了很多的优化。
