热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java中SPI的一些理解_java

这篇文章主要介绍了Java中SPI的一些理解,帮助大家更好的理解和学习Java的相关知识,感

前言

最近在面试的时候被问到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


  
    com.jimoer.spi
    spi-service-one
    1.0-SNAPSHOT
  
  
    com.jimoer.spi
    spi-service-two
    1.0-SNAPSHOT
  

创建测试类

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

  public static void main(String[] args) {

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

执行结果:

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

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

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

SPI的实现

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

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

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

public static  ServiceLoader load(Class 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机制来实现功能。


推荐阅读
  • 本文探讨了如何选择一个合适的序列化版本ID(serialVersionUID),包括使用生成器还是简单的整数,以及在不同情况下应如何处理序列化版本ID。 ... [详细]
  • 本文探讨了Android系统中联系人数据库的设计,特别是AbstractContactsProvider类的作用与实现。文章提供了对源代码的详细分析,并解释了该类如何支持跨数据库操作及事务处理。源代码可从官方Android网站下载。 ... [详细]
  • 深入理解Java字节码:方法调用详解
    本文详细介绍了Java字节码中的方法调用机制,通过具体示例解析了字节码如何处理方法调用及其参数传递。文章由Mahmoud Anouti撰写,原文链接:https://dzone.com/articles/introduction-to-java-bytecode ... [详细]
  • java datarow_DataSet  DataTable DataRow 深入浅出
    本篇文章适合有一定的基础的人去查看,最好学习过一定net编程基础在来查看此文章。1.概念DataSet是ADO.NET的中心概念。可以把DataSet当成内存中的数据 ... [详细]
  • 本文介绍了如何通过安装和配置php_uploadprogress扩展来实现文件上传时的进度条显示功能。通过一个简单的示例,详细解释了从安装扩展到编写具体代码的全过程。 ... [详细]
  • 本文详细探讨了 Android Service 组件中 onStartCommand 方法的四种不同返回值及其应用场景。Service 可以在后台执行长时间的操作,无需提供用户界面,支持通过启动和绑定两种方式创建。 ... [详细]
  • 雨林木风 GHOST XP SP3 经典珍藏版 YN2014.04
    雨林木风 GHOST XP SP3 经典珍藏版 YN2014.04 ... [详细]
  • 探索OpenWrt中的LuCI框架
    本文深入探讨了OpenWrt系统中轻量级HTTP服务器uhttpd的工作原理及其配置,重点介绍了LuCI界面的实现机制。 ... [详细]
  • 本文将详细介绍如何配置并整合MVP架构、Retrofit网络请求库、Dagger2依赖注入框架以及RxAndroid响应式编程库,构建高效、模块化的Android应用。 ... [详细]
  • MyBatis入门指南:环境搭建与基础配置详解
    本文详细介绍了MyBatis的基础配置流程,包括在Maven项目中添加MyBatis依赖、IDEA中配置数据库连接、导入SQL脚本以及编写mybatis-config.xml配置文件等关键步骤。 ... [详细]
  • 本文探讨了一个Web工程项目的需求,即允许用户随时添加定时任务,并通过Quartz框架实现这些任务的自动化调度。文章将介绍如何设计任务表以存储任务信息和执行周期,以及如何通过一个定期扫描机制自动识别并加载新任务到调度系统中。 ... [详细]
  • 本文介绍了如何使用Java编程语言实现凯撒密码的加密与解密功能。凯撒密码是一种替换式密码,通过将字母表中的每个字母向前或向后移动固定数量的位置来实现加密。 ... [详细]
  • 宝塔面板下启用HTTPS的详细指南
    本文提供了在宝塔面板环境中配置HTTPS的具体步骤,确保您的网站通信更加安全可靠。 ... [详细]
  • 本文由公众号【数智物语】(ID: decision_engine)发布,关注获取更多干货。文章探讨了从数据收集到清洗、建模及可视化的全过程,介绍了41款实用工具,旨在帮助数据科学家和分析师提升工作效率。 ... [详细]
  • 通过命令行执行Robot Framework测试用例的方法
    本文介绍如何利用命令行工具来运行Robot Framework中的测试文件及用例,解决常见的模块未找到错误。 ... [详细]
author-avatar
HAOCWH
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有