【Java】深入理解Java中的spi机制
深入理解Java中的spi机制
SPI
全名为Service Provider Interface
是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
JAVA SPI
= 基于接口的编程+策略模式+配置文件 的动态加载机制
Java SPI的具体约定如下:
当服务的提供者,提供了服务接口的一种实现之后,在jar
包的META-INF/services/
目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。
而当外部程序装配这个模块的时候,就能通过该jar
包META-INF/services/
里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
根据SPI的规范我们的服务实现类必须有一个无参构造方法
。
为什么一定要在classes
中的META-INF/services
下呢?
JDK提供服务实现查找的一个工具类:java.util.ServiceLoader
在这个类里面已经写死
// 默认会去这里寻找相关信息
private static final String PREFIX = "META-INF/services/";
常见的使用场景:
JDBC
加载不同类型的数据库驱动- 日志门面接口实现类加载,
SLF4J
加载不同提供商的日志实现类 Spring
中大量使用了SPI
,- 对
servlet3.0
规范 - 对
ServletContainerInitializer
的实现 - 自动类型转换
Type Conversion SPI(Converter SPI、Formatter SPI)
等
- 对
Dubbo
里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现
简单的spi实例
整体包结构如下
└─main
├─java
│ └─com
│ └─xinchen
│ └─spi
│ └─App.java
│ └─IService.java
│ └─ServiceImplA.java
│ └─ServiceImplB.java
└─resources
└─META-INF
└─services
└─com.xinchen.spi.IService
SPI接口
public interface IService {
void say(String word);
}
具体实现类
public class ServiceImplA implements IService {
@Override
public void say(String word) {
System.out.println(this.getClass().toString() + " say: " + word);
}
}
public class ServiceImplB implements IService {
@Override
public void say(String word) {
System.out.println(this.getClass().toString() + " say: " + word);
}
}
/resource/META-INF/services/com.xinchen.spi.IService
com.xinchen.spi.ServiceImplA
com.xinchen.spi.ServiceImplB
Client类
public class App {
static ServiceLoader<IService> services = ServiceLoader.load(IService.class);
public static void main(String[] args) {
for (IService service:services){
service.say("Hello World!");
}
}
}
// 结果:
// class com.xinchen.spi.ServiceImplA say: Hello World!
// class com.xinchen.spi.ServiceImplB say: Hello World!
源码解析
java.util.ServiceLoader
中的Fied区域
// 加载具体实现类信息的前缀
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;
从ServiceLoader.load(IService.class)
进入源码中
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程上下文的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
在ServiceLoader.load(service, cl)
中
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
// 返回ServiceLoader的实例
return new ServiceLoader<>(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();
}
public void reload() {
// 清空已经缓存的加载的接口实现类
providers.clear();
// 创建新的延迟加载迭代器
lookupIterator = new LazyIterator(service, loader);
}
private LazyIterator(Class<S> service, ClassLoader loader) {
// 指定this类中的 需要加载的接口service和类加载器loader
this.service = service;
this.loader = loader;
}
当我们通过迭代器获取对象实例的时候,首先在成员变量providers
中查找是否有缓存的实例对象
如果存在则直接返回,否则则调用lookupIterator
延迟加载迭代器进行加载
迭代器判断的代码如下
public Iterator<S> iterator() {
// 返回迭代器
return new Iterator<S>() {
// 查询缓存中是否存在实例对象
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
// 如果缓存中已经存在返回true
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();
}
};
}
LazyIterator的类加载
// 判断是否拥有下一个实例
private boolean hasNextService() {
// 如果拥有直接返回true
if (nextName != null) {
return true;
}
// 具体实现类的全名 ,Enumeration<URL> config
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;
}
// 转换config中的元素,或者具体实现类的真实包结构
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 {
// 加载类对象
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 {
// 通过c.newInstance()实例化
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
}
总结
优点
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
缺点
多个并发多线程使用ServiceLoader类的实例是不安全的
虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。
参考
【Java】深入理解Java中的spi机制的更多相关文章
- 一文搞懂Java/Spring/Dubbo框架中的SPI机制
几天前和一位前辈聊起了Spring技术,大佬突然说了SPI,作为一个熟练使用Spring的民工,心中一紧,咱也不敢说不懂,而是在聊完之后赶紧打开了浏览器,开始的学习之路,所以也就有了这篇文章.废话不多 ...
- 结合实战和源码来聊聊Java中的SPI机制?
写在前面 SPI机制能够非常方便的为某个接口动态指定其实现类,在某种程度上,这也是某些框架具有高度可扩展性的基础.今天,我们就从源码级别深入探讨下Java中的SPI机制. 注:文章已收录到:https ...
- java中的SPI机制
1 SPI机制简介 SPI的全名为Service Provider Interface.大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的.在java.util.ServiceLoader的文档里 ...
- Java 中的 SPI 机制是什么鬼?高级 Java 必须掌握!
作者:sigangjun blog.csdn.net/sigangjun/article/details/79071850 SPI的全名为:Service Provider Interface,大多数 ...
- Java是如何实现自己的SPI机制的? JDK源码(一)
注:该源码分析对应JDK版本为1.8 1 引言 这是[源码笔记]的JDK源码解读的第一篇文章,本篇我们来探究Java的SPI机制的相关源码. 2 什么是SPI机制 那么,什么是SPI机制呢? SPI是 ...
- JDK中的SPI机制
前言 最近学习类加载的过程中,了解到JDK提供给我们的一个可扩展的接口:java.util.ServiceLoader, 之前自己不了解这个机制,甚是惭愧... 什么是SPI SPI全称为(Servi ...
- java笔记--理解java类加载器以及ClassLoader类
类加载器概述: java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制 ...
- 【Java面试】Zookeeper中的Watch机制的原理?
一个工作了7年的粉丝,遇到了一个Zookeeper的问题. 因为接触过Zookeeper这个技术,不知道该怎么回答. 我说一个工作了7年的程序员,没有接触过主流技术,这不正常. 于是我问了他工资以后, ...
- Java深度理解——Java字节代码的操纵
导读:Java作为业界应用最为广泛的语言之一,深得众多软件厂商和开发者的推崇,更是被包括Oracle在内的众多JCP成员积极地推动发展.但是对于 Java语言的深度理解和运用,毕竟是很少会有人涉及的话 ...
随机推荐
- 六种获取配置properties文件的方法
总结一下六种获取配置properties文件的方法,代码如下: package com.xujingyang.test ; import java.io.BufferedInputStream ; i ...
- 4、linux-grep awk sed and cuf sort uniq join
1.grep 1.1 grep [-invc] [--color=auto] '搜寻字符串' filename选项与参数:-i :忽略大小写的不同-n :顺便输出行号-v :显示没有 '搜寻字符串' ...
- HTML5+JavaScript动画基础 完整版 中文pdf扫描版
<HTML5+JavaScript动画基础>包括了基础知识.基础动画.高级动画.3D动画和其他技术5大部分,分别介绍了动画的基本概念.动画的JavaScript基础.动画中的三角学.渲染技 ...
- VS(Visual Studio)中快速找出含中文的字符串
环境:visual studio 2017 1.ctrl + shift + f 打卡全局查找 2.输入(".*[\u4E00-\u9FA5]+)|([\u4E00-\u9FA5]+.*&q ...
- 剑指Spring源码(三)俯瞰Spring的Bean的生命周期(大众版)
距离上一次写Spring源码解析,已经过去了快要好几个月了,主要原因还是Spring的源码解析类文章太难写了,不像我先前写的什么CAS源码,AQS源码,LinkedBlockingQueue等等,这些 ...
- 目标检测的图像特征提取之_LBP特征
LBP(Local Binary Pattern,局部二值模式)是一种用来描述图像局部纹理特征的算子:它具有旋转不变性和灰度不变性等显著的优点.它是首先由T. Ojala, M.Pietikäinen ...
- 洛谷P3396 哈希冲突(分块)
传送门 题解在此,讲的蛮清楚的->这里 我就贴个代码 //minamoto #include<iostream> #include<cstdio> #include< ...
- 让你的Kivy支持中文字符
前言 默认Kivy字体不支持中文,本文简单描述如何让你的应用支持中文字符 代码 from kivy.app import App from kivy.core.text import LabelBas ...
- docker默认存储空间用完情况下,做迁移数据
由于docker默认存放数据路径为/var/lib/docker,但运行了一段时间后,发现/var/lib/docker下的目录文件过大,导致此分区空间不够用.通过以下方法,解决该问题. 如何避免: ...
- day23作业详解
1.题目 2.题目详解 点击查看详细内容 1. 1-1 封装 把功能封装到类中 class Message(object): def email(self):pass def msg(self):pa ...