详解java实践SPI机制及浅析源码

1.概念

正式步入今天的核心内容之前,溪源先给大家介绍一下关于SPI机制的相关概念,最后会提供实践源代码。

SPI即Service Provider Interface,属于JDK内置的一种动态的服务提供发现机制,可以理解为运行时动态加载接口的实现类。更甚至,大家可以将SPI机制与设计模式中的策略模式建立联系。

SPI机制:

从上图中理解SPI机制:标准化接口+策略模式+配置文件;

SPI机制核心思想:系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制

使用场景:

  • 1.数据库驱动加载:面对不同厂商的数据库,JDBC需要加载不同类型的数据库驱动;
  • 2.日志接口实现:SLF4J加载不同日志实现类;
  • 3.溪源在实际开发中也使用了SPI机制:面对不同仪器平台的结果文件上传需要解析具体的结果,文件不同,解析逻辑不同,因此采用SPI机制能够解耦和降低维护成本;

SPI机制使用约定:

从上面的图中,我们可以清晰的知道SPI的三部分:接口+实现类+配置文件;因此,项目中若要利用SPI机制,则需要遵循以下约定:

  • 当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名。
  • 主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;

注意:除SPI,我还发布了最新Java架构项目实战教程+大厂面试题库, 点击此处免费获取,小白勿进!

2.实践

整体包结构如图:

新建标准化接口:

public interface SayService {
  void say(String word);
}

建立两个实现类

@Service
public class ASayServiceImpl implements SayService {
  @Override
  public void say(String word) {
    System.out.println(word + " A say: I am a boy");
  }
}

@Service
public class BSayServiceImpl implements SayService {
  @Override
  public void say(String word) {
    System.out.println(word + " B say: I am a girl");
  }
}

新建META-INF/services目录和配置文件(以接口全限定名)

配置文件内容为实现类全限定名

com.qxy.spi.impl.ASayServiceImpl
com.qxy.spi.impl.BSayServiceImpl

单测

@SpringBootTest
@RunWith(SpringRunner.class)
public class SpiTest {

  static ServiceLoader<SayService> services = ServiceLoader.load(SayService.class);

  @Test
  public void test1() {
    for (SayService sayService : services) {
      sayService.say("Hello");
    }
  }

}

结果

Hello A say: I am a boy
Hello B say: I am a girl

3.源码

源码主要加载流程如下:

应用程序调用ServiceLoader.load方法 ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量;

  • loader(ClassLoader类型,类加载器)
  • acc(AccessControlContext类型,访问控制器)
  • providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
  • lookupIterator(实现迭代器功能)

应用程序通过迭代器接口获取对象实例 ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,执行类的装载。

  • 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件;
  • 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
  • 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。
public final class ServiceLoader<S>
  implements Iterable<S>
{
  // 加载具体实现类信息的前缀
  private static final String PREFIX = "META-INF/services/";

  // 需要加载的接口
  // The class or interface representing the service being loaded
  private final Class<S> service;

  // 用于加载的类加载器
  // The class loader used to locate, load, and instantiate providers
  private final ClassLoader loader;

  // 创建ServiceLoader时采用的访问控制上下文
  // The access control context taken when the ServiceLoader is created
  private final AccessControlContext acc;

  // 用于缓存已经加载的接口实现类,其中key为实现类的完整类名
  // Cached providers, in instantiation order
  private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

  // 用于延迟加载接口的实现类
  // The current lazy-lookup iterator
  private LazyIterator lookupIterator;

  public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
  }

  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();
  }

  private static void fail(Class<?> service, String msg, Throwable cause)
    throws ServiceConfigurationError
  {
    throw new ServiceConfigurationError(service.getName() + ": " + msg,
                      cause);
  }

  private static void fail(Class<?> service, String msg)
    throws ServiceConfigurationError
  {
    throw new ServiceConfigurationError(service.getName() + ": " + msg);
  }

  private static void fail(Class<?> service, URL u, int line, String msg)
    throws ServiceConfigurationError
  {
    fail(service, u + ":" + line + ": " + msg);
  }

  // Parse a single line from the given configuration file, adding the name
  // on the line to the names list.
  //具体解析资源文件中的每一行内容
  private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
             List<String> names)
    throws IOException, ServiceConfigurationError
  {
    String ln = r.readLine();
    if (ln == null) {
    	//-1表示解析完成
      return -1;
    }
    // 如果存在'#'字符,截取第一个'#'字符串之前的内容,'#'字符之后的属于注释内容
    int ci = ln.indexOf('#');
    if (ci >= 0) ln = ln.substring(0, ci);
    ln = ln.trim();
    int n = ln.length();
    if (n != 0) {
    	//不合法的标识:' '、'\t'
      if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
        fail(service, u, lc, "Illegal configuration-file syntax");
      int cp = ln.codePointAt(0);
      //判断第一个 char 是否一个合法的 Java 起始标识符
      if (!Character.isJavaIdentifierStart(cp))
        fail(service, u, lc, "Illegal provider-class name: " + ln);
      	//判断所有其他字符串是否属于合法的Java标识符
      for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
        cp = ln.codePointAt(i);
        if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
          fail(service, u, lc, "Illegal provider-class name: " + ln);
      }
      //不存在则缓存
      if (!providers.containsKey(ln) && !names.contains(ln))
        names.add(ln);
    }
    return lc + 1;
  }

  private Iterator<String> parse(Class<?> service, URL u)
    throws ServiceConfigurationError
  {
    InputStream in = null;
    BufferedReader r = null;
    ArrayList<String> names = new ArrayList<>();
    try {
      in = u.openStream();
      r = new BufferedReader(new InputStreamReader(in, "utf-8"));
      int lc = 1;
      while ((lc = parseLine(service, u, r, lc, names)) >= 0);
    } catch (IOException x) {
      fail(service, "Error reading configuration file", x);
    } finally {
      try {
        if (r != null) r.close();
        if (in != null) in.close();
      } catch (IOException y) {
        fail(service, "Error closing configuration file", y);
      }
    }
    return names.iterator();
  }

  // Private inner class implementing fully-lazy provider lookup
  //
  private class LazyIterator
    implements Iterator<S>
  {

    Class<S> service;
    ClassLoader loader;
    // 加载资源的URL集合
	  Enumeration<URL> configs = null;
	  // 需加载的实现类的全限定类名的集合
	  Iterator<String> pending = null;
	  // 下一个需要加载的实现类的全限定类名
	  String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
      this.service = service;
      this.loader = loader;
    }

    private boolean hasNextService() {
      if (nextName != null) {
        return true;
      }
      if (configs == null) {
        try {
        // 资源名称,META-INF/services + 全限定名
          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;
    }

    private S nextService() {
      if (!hasNextService())
        throw new NoSuchElementException();
      String cn = nextName;
      nextName = null;
      Class<?> c = null;
      try {
      //反射构造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());
         // 实例完成,添加缓存,Key:实现类全限定类名,Value:实现类实例
        providers.put(cn, p);
        return p;
      } catch (Throwable x) {
        fail(service,
           "Provider " + cn + " could not be instantiated",
           x);
      }
      throw new Error();     // This cannot happen
    }

    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);
      }
    }

    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);
      }
    }

    public void remove() {
      throw new UnsupportedOperationException();
    }

  }

  public Iterator<S> iterator() {
    return new Iterator<S>() {

      Iterator<Map.Entry<String,S>> knownProviders
        = providers.entrySet().iterator();

      public boolean 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();
      }

    };
  }

  public static <S> ServiceLoader<S> load(Class<S> service,
                      ClassLoader loader)
  {
  // 返回ServiceLoader的实例
    return new ServiceLoader<>(service, loader);
  }

  public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {

    ClassLoader cl = ClassLoader.getSystemClassLoader();
    ClassLoader prev = null;
    while (cl != null) {
      prev = cl;
      cl = cl.getParent();
    }
    return ServiceLoader.load(service, prev);
  }

  public String toString() {
    return "java.util.ServiceLoader[" + service.getName() + "]";
  }

}

4.总结

SPI机制在实际开发中使用得场景也有很多。特别是统一标准的不同厂商实现,溪源也正是利用SPI机制(但略做改进,避免过多加载资源浪费)实现不同技术平台的结果文件解析需求。

优点

使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

缺点

虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。

源码传送门:SPI Service

到此这篇关于详解java实践SPI机制及浅析源码的文章就介绍到这了,更多相关java SPI机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

时间: 2020-07-23

深入学习Java中的SPI机制

概述 SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现. Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦. SPI整体机制图如下 当服务

Java利用Sping框架编写RPC远程过程调用服务的教程

RPC,即 Remote Procedure Call(远程过程调用),说得通俗一点就是:调用远程计算机上的服务,就像调用本地服务一样. RPC 可基于 HTTP 或 TCP 协议,Web Service 就是基于 HTTP 协议的 RPC,它具有良好的跨平台性,但其性能却不如基于 TCP 协议的 RPC.会两方面会直接影响 RPC 的性能,一是传输方式,二是序列化. 众所周知,TCP 是传输层协议,HTTP 是应用层协议,而传输层较应用层更加底层,在数据传输方面,越底层越快,因此,在一般情况下

Java SPI 机制知识点总结

前言 不知大家现在有没有去公司复工,我已经在家办公将近 3 周了,同时也在家呆了一个多月:还好工作并没有受到任何影响,我个人一直觉得远程工作和 IT 行业是非常契合的,这段时间的工作效率甚至比在办公室还高,同时由于我们公司的业务在海外,所以疫情几乎没有造成太多影响. 扯远了,这次主要是想和大家分享一下 Java 的 SPI 机制. 还没看过的朋友的我先做个前景提要,当时的需求: 我实现了一个类似于的 SpringMVC 但却很轻量的 http 框架 cicada,其中当然也需要一个 IOC 容器

Java SPI机制原理及代码实例

SPI的全名为:Service Provider Interface,大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的.在java.util.ServiceLoader的文档里有比较详细的介绍. 简单的总结下 Java SPI 机制的思想.我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块.jdbc模块的方案等.面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码. 一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要

Java SPI的简单小实例

JDK有个ServiceLoader类,在java.util包里,支持按约定目录/META-INF/services去找到接口全路径命名的文件,读取文件内容得到接口实现类的全路径,加载并实例化.如果我们在自己的代码中定义一个接口,别人按接口实现并打包好了,那么我们只需要引入jar包,通过ServiceLoader就能够把别人的实现用起来.举个例子,JDK中的JDBC提供一个数据库连接驱动接口,不同的厂商可以有不同的实现,如果它们给的jar包里按规定提供了配置和实现类,那么我们就可以执行不同的数据

Java的SPI机制实例详解

Java的SPI机制实例详解 SPI的全名为Service Provider Interface.普通开发人员可能不熟悉,因为这个是针对厂商或者插件的.在java.util.ServiceLoader的文档里有比较详细的介绍.究其思想,其实是和"Callback"差不多."Callback"的思想是在我们调用API的时候,我们可以自己写一段逻辑代码,传入到API里面,API内部在合适的时候会调用它,从而实现某种程度的"定制". 典型的是Colle

JAVA SPI特性及简单应用代码实例

最近在研究dubbo时,发现了JAVA的SPI特性.SPI的全名为Service Provider Interface,是JDK内置的一种服务发现机制. 具体实现: 1.定义一个接口 public interface IShape { /** * 渲染 */ void render(); } 2.添加几种实现 public class CircularShape implements IShape { @Override public void render() { System.out.pri

详解JAVA SPI机制和使用方法

JAVA SPI 简介 SPI 是 Java 提供的一种服务加载方式,全名为 Service Provider Interface.根据 Java 的 SPI 规范,我们可以定义一个服务接口,具体的实现由对应的实现者去提供,即服务提供者.然后在使用的时候再根据 SPI 的规范去获取对应的服务提供者的服务实现.通过 SPI 服务加载机制进行服务的注册和发现,可以有效的避免在代码中将具体的服务提供者写死.从而可以基于接口编程,实现模块间的解耦. SPI 机制的约定 1 在 META-INF/serv

详解java.lang.reflect.Modifier.isInterface()方法

详解java.lang.reflect.Modifier.isInterface()方法 java.lang.reflect.Modifier.isInterface(int mod)方法判断如果给定mod参数包含final修饰符,则返回true,否则返回false. 声明 以下是java.lang.reflect.Modifier.isInterface()方法的声明. public static boolean isInterface(int mod) 参数 mod - 一组修饰符. 返回值

详解Java生成PDF文档方法

最近项目需要实现PDF下载的功能,由于没有这方面的经验,从网上花了很长时间才找到相关的资料.整理之后,发现有如下几个框架可以实现这个功能. 1. 开源框架支持 iText,生成PDF文档,还支持将XML.Html文件转化为PDF文件: Apache PDFBox,生成.合并PDF文档: docx4j,生成docx.pptx.xlsx文档,支持转换为PDF格式. 比较: iText开源协议为AGPL,而其他两个框架协议均为Apache License v2.0. 使用PDFBox生成PDF就像画图

详解JAVA类加载机制(推荐)

JAVA源码编译由三个过程组成: 1.源码编译机制. 2.类加载机制 3.类执行机制 我们这里主要介绍编译和类加载这两种机制. 一.源码编译 代码编译由JAVA源码编译器来完成.主要是将源码编译成字节码文件(class文件).字节码文件格式主要分为两部分:常量池和方法字节码. 二.类加载 类的生命周期是从被加载到虚拟机内存中开始,到卸载出内存结束.过程共有七个阶段,其中到初始化之前的都是属于类加载的部分 加载----验证----准备----解析-----初始化----使用-----卸载 系统可能

详解Java中Method的Invoke方法

在写代码的时候,发现从父类class通过getDeclaredMethod获取的Method可以调用子类的对象,而子类改写了这个方法,从子类class通过getDeclaredMethod也能获取到Method,这时去调用父类的对象也会报错.虽然这是很符合多态的现象,也符合java的动态绑定规范,但还是想弄懂java是如何实现的,就学习了下Method的源代码.  Method的invoke方法 1.先检查 AccessibleObject的override属性是否为true. Accessib

详解Java继承中属性、方法和对象的关系

大家都知道子类继承父类是类型的继承,包括属性和方法!如果子类和父类中的方法签名相同就叫覆盖!如果子类和父类的属性相同,父类就会隐藏自己的属性! 但是如果我用父类和子类所创建的引用指向子类所创建的对象,父类引用所调用子类对象中的属性值或方法的结果是什么呢? 看代码: public class FieldDemo { public static void main(String[] args){ Student t = new Student("Jack"); Person p = t;/

详解java生成json字符串的方法

例1:将map对象添加一次元素(包括字符串对.数组),转换成json对象一次. 代码: package com.json; //这是使用org.json的程序: import java.util.HashMap; import java.util.Map; import org.json.JSONException; import org.json.JSONObject; public class jsontest { public static void main(String[] args)

详解java中反射机制(含数组参数)

详解java中反射机制(含数组参数) java的反射是我一直非常喜欢的地方,因为有了这个,可以让程序的灵活性大大的增加,同时通用性也提高了很多.反射原理什么的,我就不想做过大介绍了,网上一搜,就一大把.(下面我是只附录介绍下) Reflection 是Java被视为动态(或准动态)语言的一个关键性质.这个机制允许程序在运行时透过Reflection APIs取得任何一个已知名称的class的内部信息,包括其modifiers(诸如public, static 等等).superclass(例如O

详解Java的堆内存与栈内存的存储机制

堆与内存优化     今天测了一个项目的数据自动整理功能,对数据库中几万条记录及图片进行整理操作,运行接近到最后,爆出了java.lang.outOfMemoryError,java heap space方面的错误,以前写程序很少遇到这种内存上的错误,因为java有垃圾回收器机制,就一直没太关注.今天上网找了点资料,在此基础上做了个整理.  一.堆和栈 堆-用new建立,垃圾回收器负责回收 1.程序开始运行时,JVM从OS获取一些内存,部分是堆内存.堆内存通常在存储地址的底层,向上排列. 2.堆

详解Java编写并运行spark应用程序的方法

我们首先提出这样一个简单的需求: 现在要分析某网站的访问日志信息,统计来自不同IP的用户访问的次数,从而通过Geo信息来获得来访用户所在国家地区分布状况.这里我拿我网站的日志记录行示例,如下所示: 121.205.198.92 - - [21/Feb/2014:00:00:07 +0800] "GET /archives/417.html HTTP/1.1" 200 11465 "http://shiyanjun.cn/archives/417.html/" &qu

详解Java 本地接口 JNI 使用方法

详解Java 本地接口 JNI 使用方法 对于Java程序员来说,Java语言的好处和优点,我想不用我说了,大家自然会说出很多一套套的.但虽然我们作为java程序员,但我们不得不承认java语言也有一些它本身的缺点.比如在性能.和底层打交道方面都有它的缺点.所以java就提供了一些本地接口,他主要的作用就是提供一个标准的方式让java程序通过虚拟机与原生代码进行交互,这也就是我们平常常说的java本地接口(JNI--java native Interface).它使得在 Java 虚拟机 (VM