热门标签 | 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机制来实现功能。


推荐阅读
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 展开全部下面的代码是创建一个立方体Thisexamplescreatesanddisplaysasimplebox.#Thefirstlineloadstheinit_disp ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • FeatureRequestIsyourfeaturerequestrelatedtoaproblem?Please ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
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社区 版权所有