你说说对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-one
和spi-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 制
深入理解Java 中SPI 制 概述 SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比 ...
- 深入理解 Java 中 SPI 机制
本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/vpy5DJ-hhn0iOyp747oL5A作者:姜柱 SPI(Service Provider ...
- 谈谈我对Java中CallBack的理解
谈谈我对Java中CallBack的理解 http://www.cnblogs.com/codingmyworld/archive/2011/07/22/2113514.html CallBack是回 ...
- 沉淀再出发:关于java中的AQS理解
沉淀再出发:关于java中的AQS理解 一.前言 在java中有很多锁结构都继承自AQS(AbstractQueuedSynchronizer)这个抽象类如果我们仔细了解可以发现AQS的作用是非常大的 ...
- Java中hashcode的理解
Java中hashcode的理解 原文链接http://blog.csdn.net/chinayuan/article/details/3345559 怎样理解hashCode的作用: 以 java. ...
- JAVA中SPI机制
之前研究dubbo的时候就很好奇,里面各种扩展机制,期间也看过很多关于SPI的机制,今日有缘再度看到有文章总结,故记录一下, 首先了解一下 JAVA中SPI简单的用法 可参考这篇文章,https:// ...
- java中ThrealLocal的理解
目录 java中threadlocal的理解 一.threadlocal的生命周期和ThreadLocalMap的生命周期 二.ThreadLocal的作用 三.threadlocal示例 四.Inh ...
- java中threadlocal的理解
[TOC] #java中threadlocal的理解##一.threadlocal的生命周期和ThreadLocalMap的生命周期可以吧TreadLocal看做是一个map来使用,只不过这个map是 ...
- 关于java中多态的理解
java三大特性:封装,继承,多态. 多态是java的非常重要的一个特性: 那么问题来了:什么是多态呢? 定义:指允许不同类的对象对同一消息做出响应.即同一消息可以根据发送对象的不同而采用多种不同的行 ...
随机推荐
- C++实现管理系统
概述 系统中需要实现的功能如下: 添加联系人:向通讯录中添加新人,信息包括(姓名.性别.年龄.联系电话.家庭住址)最多记录1000人 显示联系人:显示通讯录中所有的联系人信息 删除联系人:按照姓名进行 ...
- Java--关于cpu占用解决方案
关于cpu占用高的解决方案--java篇 通俗一点:找到进程,找到下面的线程,找到线程正在做的事,分析线程正在做的事. 一.查看cpu占用高的进程 top命令可以查看(假设%cpu已经属于很高了,我们 ...
- Future Callable 线程池 例1
package com.niewj.concurrent; import java.util.concurrent.Callable; import java.util.concurrent.Exec ...
- 华为+京东数科(原京东金融)面经--Java后台开发
华为: 1.笔试中遇到的问题,如何解决的?(Scanner 如何结束循环读取数据,笔者在面试中因没有理解到Scanner类的hasNext()与hasNextLine()是阻塞方法,导致没有正确退出循 ...
- TypeScript注意
中文文档中,元组目前已经不能越界访问
- 【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈
写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记.其观看地址如下:尚硅谷2020最新版宋红康JVM教程 一.运行时数据区 我们在编写Java程序时,使用JVM的流程主要如下所示: 虚拟机在 ...
- C++之父:精通C++很难,但你一天之内就能学习使用C++
精通C++听起来好像就是一个笑话.为什么C++比别的语言难学那么多?其实这基本上是因为C++之父Bjarne Stroustrup 说过的一句话"我特别的讨厌语言的设计者把自己的喜好强加给用 ...
- 基于FFmpeg的Dxva2硬解码及Direct3D显示(二)
解析视频源 目录 解析视频源 获取视频流 解析视频流 说明:这篇博文分为"获取视频流"和"解析视频流"两个部分,使用的是FFmpeg4.1的版本,与网上流传的低 ...
- 储存与RAID--独立磁盘阵列
存储: 专门用来插硬盘的机器,作用是增加插口,可以多插硬盘. 这种有策略保证硬盘坏了,数据不丢.而本地磁盘坏了,会导致数据丢失,故一般操作系统放在本地磁盘.而数据放在存储盘. 存储里依然有:cpu( ...
- AQS详解,并发编程的半壁江山
千呼万唤始出来,终于写到AQS这个一章了,其实为了写这一章,前面也是做了很多的铺垫,比如之前的 深度理解volatile关键字 线程之间的协作(等待通知模式) JUC 常用4大并发工具类 CAS 原子 ...