Java中SPI的一些理解

前言

最近在面试的时候被问到SPI了,没回答上来,主要也是自己的原因,把自己给带沟里去了,因为讲到了类加载器的双亲委派模型,后面就被问到了有哪些是破坏了双亲委派模型的场景,然后我就说到了SPI,JNDI,以及JDK9的模块化都破坏了双亲委派。
然后就被问,那你说说对Java中的SPI的理解吧。然后我就一脸懵逼了,之前只是知道它会破坏双亲委派,也知道是个怎么回事,但是并没有深入了解,那么这次我就好好的来总结一下这个知识吧。

什么是SPI

SPI全称Service Provider Interface,字面意思是提供服务的接口,再解释详细一下就是Java提供的一套用来被第三方实现或扩展的接口,实现了接口的动态扩展,让第三方的实现类能像插件一样嵌入到系统中。

咦。。。
这个解释感觉还是有点绕口。
那就说一下它的本质。

将接口的实现类的全限定名配置在文件中(文件名是接口的全限定名),由服务加载器读取配置文件,加载实现类。实现了运行时动态为接口替换实现类。

SPI示例

还是举例说明吧。
我们创建一个项目,然后创建一个module叫spi-interface。

在这个module中我们定义一个接口:

/**
 * @author jimoer
 **/
public interface SpiInterfaceService {

  /**
   * 打印参数
   * @param parameter 参数
   */
  void printParameter(String parameter);
}

再定义一个module,名字叫spi-service-one,pom.xml中依赖spi-interface。
在spi-service-one中定义一个实现类,实现SpiInterfaceService 接口。

package com.jimoer.spi.service.one;
import com.jimoer.spi.app.SpiInterfaceService;

/**
 * @author jimoer
 **/
public class SpiOneService implements SpiInterfaceService {
  /**
   * 打印参数
   *
   * @param parameter 参数
   */
  @Override
  public void printParameter(String parameter) {
    System.out.println("我是SpiOneService:"+parameter);
  }
}

然后再spi-service-one的resources目录下创建目录META-INF/services,在此目录下创建一个文件名称为SpiInterfaceService接口的全限定名称,文件内容写入SpiOneService这个实现类的全限定名称。
效果如下:

再创建一个module,名称为:spi-service-one,也是依赖spi-interface,并且定义一个实现类SpiTwoService 来实现SpiInterfaceService 接口。

package com.jimoer.spi.service.two;
import com.jimoer.spi.app.SpiInterfaceService;
/**
 * @author jimoer
 **/
public class SpiTwoService implements SpiInterfaceService {
  /**
   * 打印参数
   *
   * @param parameter 参数
   */
  @Override
  public void printParameter(String parameter) {
    System.out.println("我是SpiTwoService:"+parameter);
  }
}

目录结构如下:

下面再创建一个用来测试的module,名为:spi-app。

pom.xml中依赖spi-service-onespi-service-two

<dependencies>
  <dependency>
    <groupId>com.jimoer.spi</groupId>
    <artifactId>spi-service-one</artifactId>
    <version>1.0-SNAPSHOT</version>
  </dependency>
  <dependency>
    <groupId>com.jimoer.spi</groupId>
    <artifactId>spi-service-two</artifactId>
    <version>1.0-SNAPSHOT</version>
  </dependency>
</dependencies>

创建测试类

/**
 * @author jimoer
 **/
public class SpiService {

  public static void main(String[] args) {

    ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);
    Iterator<SpiInterfaceService> iterator = spiInterfaceServices.iterator();
    while (iterator.hasNext()){
      SpiInterfaceService sip = iterator.next();
      sip.printParameter("参数");
    }
  }
}

执行结果:

我是SpiTwoService:参数
我是SpiOneService:参数

通过运行结果我们可以看到,已经将SpiInterfaceService接口的所有实现都加载到了当前项目中,并且执行了调用。

这整个代码结构我们可以看出SPI机制将模块的装配放到了程序外面,就是说,接口的实现可以在程序外面,只需要在使用的时候指定具体的实现。并且动态的加载到自己的项目中。
SPI机制的主要目的:
一是为了解耦,将接口和具体实现分离开来;
二是提高框架的扩展性。以前写程序的时候,接口和实现都写在一起,调用方在使用的时候依赖接口来进行调用,无权选择使用具体的实现类。

SPI的实现

那么我们来看一下SPI具体是如何实现的呢?
通过上面的例子,我们可以看到,SPI机制的核心代码是下面这段:

ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);

那么我们来看一下ServiceLoader.load()方法的源码:

public static <S> ServiceLoader<S> load(Class<S> service) {
  ClassLoader cl = Thread.currentThread().getContextClassLoader();
  return ServiceLoader.load(service, cl);
}

看到Thread.currentThread().getContextClassLoader();我就明白是怎么回事了,这个就是线程上下文类加载器,因为线程上下文类加载器就是为了做类加载双亲委派模型的逆序而创建的。

使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了,双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。
《深入理解Java虚拟机(第三版)》

虽然知道了它是破坏双亲委派的了,但是具体实现,还是需要具体往下看的。

在ServiceLoader里找到具体实现hasNext()的方法了,那么继续来看这个方法的实现。

hasNext()方法又主要调用了hasNextService()方法。

// 固定路径
private static final String PREFIX = "META-INF/services/";

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());
   }
   // 后面next()方法中判断当前类是否已经出现化的时候要用
   nextName = pending.next();
   return true;
 }

主要就是去加载META-INF/services/路径下的接口全限定名称的文件然后去里面找到实现类的类路径将实现类进行类加载。

继续看迭代器是如何取出每一个实现对象的。那就要看ServiceLoader中实现了迭代器的next()方法了。

next()方法主要是nextService()实现的,那么继续看nextService()方法。

private S nextService() {
   if (!hasNextService())
     throw new NoSuchElementException();
   String cn = nextName;
   nextName = null;
   Class<?> c = null;
   try {
   // 直接加载类,无需初始化(因为上面hasNext()已经初始化了)。
     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
 }

看到这里就可以明白了,是如何创建出对象的了。先在hasNext()将接口的实现类进行加载并判断是否存在接口的实现类,然后在next()方法中将实现类进实例化。

Java中使用SPI机制的功能其实有很多,像JDBC、JNDI、以及Spring中也有使用,甚至RPC框架(Dubbo)中也有使用SPI机制来实现功能。

以上就是Java中SPI的一些理解的详细内容,更多关于Java SPI的资料请关注我们其它相关文章!

(0)

相关推荐

  • java中spi使用详解

    一.简介 java中spi(service provider interface)是jdk内置的一种服务发现机制,可以基于配置,在运行时加载指定服务.java中提供了很多服务提供接口,如jdbc.jndi等. 1.什么是SPI SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件. SPI的作用就是为这些被扩展的API寻找服务实现. 2.SPI和API的使用场景 API (Application Pr

  • Java 基于Spire.Cloud.SDK for Java在PDF中绘制形状

    Spire.Cloud.SDK for Java提供了pdfPathApi接口可用于在PDF文档中绘制形状(或图形),如绘制线条形状drawLine().绘制矩形形状drawRectanglef(),下面将介绍如何通过Java示例和步骤来实现: 一.导入jar文件.(有2种方式) 创建Maven项目程序,通过maven仓库下载导入.以IDEA为例,新建Maven项目,在pom.xml文件中配置maven仓库路径,并指定spire.cloud.sdk的依赖,如下: <repositories>

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

    1.概念 正式步入今天的核心内容之前,溪源先给大家介绍一下关于SPI机制的相关概念,最后会提供实践源代码. SPI即Service Provider Interface,属于JDK内置的一种动态的服务提供发现机制,可以理解为运行时动态加载接口的实现类.更甚至,大家可以将SPI机制与设计模式中的策略模式建立联系. SPI机制: 从上图中理解SPI机制:标准化接口+策略模式+配置文件: SPI机制核心思想:系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编

  • Java SPI机制原理及代码实例

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

  • 浅谈Java的SPI技术

    人狠话不多,先上截图和代码吧. 这是工程目录.看到resources目录下面没有,添加了META-INF和services目录. 添加了一个com.gys.test.IAnimal文件.这个文件名和上面的接口名完全对应.这个目录的文件名是有讲究的,按照SPI规范来的. 上代码: package com.gys.test; public interface IAnimal { void eat(); } package com.gys.test.impl; import com.gys.test.

  • Java 添加、删除、替换、格式化Word中的文本的步骤详解(基于Spire.Cloud.SDK for Java)

    Spire.Cloud.SDK for Java提供了TextRangesApi接口可通过addTextRange()添加文本.deleteTextRange()删除文本.updateTextRangeText()替换文本.updateTextRangeFormat()格式化文本等.本文将从以上方法介绍如何来实现对文本的操作.可参考以下步骤进行准备: 一.导入jar文件 创建Maven项目程序,通过maven仓库下载导入.以IDEA为例,新建Maven项目,在pom.xml文件中配置maven仓

  • 详解JAVA SPI机制和使用方法

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

  • 在java中使用SPI创建可扩展的应用程序操作

    简介 什么是可扩展的应用程序呢?可扩展的意思是不需要修改原始代码,就可以扩展应用程序的功能.我们将应用程序做成插件或者模块. 这样可以在不修改原应用的基础上,对系统功能进行升级或者定制化. 本文将会向大家介绍如何通过java中的SPI机制实现这种可扩展的应用程序. SPI简介 SPI的全称是Java Service Provider Interface.是java提供的一种服务发现的机制. 通过遵循相应的规则编写应用程序之后,就可以使用ServiceLoader来加载相应的服务了. SPI的实现

  • 如何通过Maven仓库安装Spire系列的Java产品

    前言 Spire系列库中已发布的Java产品目前有三个,即Spire.PDF for Java.Spire.Presentation for Java.Spire.Barcode for Java.使用该Java产品,除了可以从官网:https://www.e-iceblue.cn/Downloads.html下载,也可以通过Maven来安装使用.本教程中,以通过Maven安装Spire.PDF for Java为例介绍具体安装方法和步骤. 首先,在pom.xml文件中配置Maven仓库路径.

  • Java SPI的简单小实例

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

  • Java基于Spire Cloud Excel把Excel转换成PDF

    Spire.Cloud.Excel Sdk 提供GeneralApi接口和WorkbookApi接口,支持将本地Excel和云端Excel文档转换为ODS, PDF, XPS, PCL, PS等格式.本文以将Excel表格转为PDF为例,介绍实现格式转换的步骤及方法. 所需工具:Spire.Cloud.Excel.Sdk 必要步骤: 步骤1:Jar文件下载及导入.可通过"下载中心"下载获取jar:或者通过maven仓库安装导入,具体参考安装方法. 步骤2:ID及Key获取.需要在云端创

  • Java的SPI机制实例详解

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

  • Java 添加、删除、格式化Word中的图片步骤详解( 基于Spire.Cloud.SDK for Java )

    本文介绍使用Spire.Cloud.SDK for Java提供的ImagesApi接口来操作Word中的图片.具体可通过addImage()方法添加图片.deleteImage()方法删除图片.updateImageFormat()格式化Word中的图片以及getImageFormat()获取Word中的图片格式等.操作方法和代码示例可参考下文中的步骤. 步骤1:导入jar文件 创建Maven项目程序,通过maven仓库下载导入.以IDEA为例,新建Maven项目,在pom.xml文件中配置m

随机推荐