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.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 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原理及源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Dubbo的SPI机制与JDK机制的不同及原理分析

Dubbo底层源码分析之SPI扩展点

Java SPI机制实战详解及源码分析

2JDK8中的HashMap实现原理及源码分析

Dubbo 2.7.3源码分析——JDK SPI篇

dubbo源码分析01:SPI机制