JDK源码解析之Java SPI机制
1. spi 是什么
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了开闭原则,Java SPI就是为某个接口寻找服务实现的机制,Java Spi的核心思想就是解耦。
整体机制图如下:
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
总结起来就是:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
2. 应用场景
数据库驱动加载接口实现类的加载
JDBC加载不同类型数据库的驱动
日志门面接口实现类加载
SLF4J加载不同提供应商的日志实现类
Spring
Servlet容器启动初始化
org.springframework.web.SpringServletContainerInitializer
Spring Boot
自动装配过程中,加载META-INF/spring.factories文件,解析properties文件
Dubbo
Dubbo大量使用了SPI技术,里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来
例如Protocol 协议接口
3. 使用步骤
以支付服务为例:
创建一个
PayService
添加一个pay
方法package com.imooc.spi; import java.math.BigDecimal; public interface PayService { void pay(BigDecimal price);
}
创建
AlipayService
和WechatPayService
,实现PayService
⚠️SPI的实现类必须携带一个不带参数的构造方法;
package com.imooc.spi; import java.math.BigDecimal; public class AlipayService implements PayService{ public void pay(BigDecimal price) {
System.out.println("使用支付宝支付");
}
}package com.imooc.spi; import java.math.BigDecimal; public class WechatPayService implements PayService{ public void pay(BigDecimal price) {
System.out.println("使用微信支付");
}
}resources目录下创建目录META-INF/services
在META-INF/services创建com.imooc.spi.PayService文件
先以AlipayService为例:在com.imooc.spi.PayService添加com.imooc.spi.AlipayService的文件内容
创建测试类
package com.imooc.spi; import com.util.ServiceLoader; import java.math.BigDecimal; public class PayTests { public static void main(String[] args) {
ServiceLoader<PayService> payServices = ServiceLoader.load(PayService.class);
for (PayService payService : payServices) {
payService.pay(new BigDecimal(1));
}
}
}运行测试类,查看返回结果
4. 原理分析
首先,我们先打开ServiceLoader<S>
这个类
public final class ServiceLoader<S> implements Iterable<S> {
// SPI文件路径的前缀
private static final String PREFIX = "META-INF/services/"; // 需要加载的服务的类或接口
private Class<S> service; // 用于定位、加载和实例化提供程序的类加载器
private ClassLoader loader; // 创建ServiceLoader时获取的访问控制上下文
private final AccessControlContext acc; // 按实例化顺序缓存Provider
private LinkedHashMap<String, S> providers = new LinkedHashMap(); // 懒加载迭代器
private LazyIterator lookupIterator; ......
}
参考具体ServiceLoader具体源码,代码量不多,实现的流程如下:
应用程序调用ServiceLoader.load方法
// 1. 获取ClassLoad
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
} // 2. 调用构造方法
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
} // 3. 校验参数和ClassLoad
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();
} //4. 清理缓存容器,实例懒加载迭代器
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}我们简单看一下这个懒加载迭代器
// 实现完全懒惰的提供程序查找的私有内部类
private class LazyIterator implements Iterator<S>{ // 需要加载的服务的类或接口
Class<S> service;
// 用于定位、加载和实例化提供程序的类加载器
ClassLoader loader;
// 枚举类型的资源路径
Enumeration<URL> configs = null;
// 迭代器
Iterator<String> pending = null;
// 配置文件中下一行className
String nextName = null; private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
} private boolean hasNextService() {
if (nextName != null) {
return true;
}
// 加载配置PREFIX + service.getName()的文件
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());
}
// 获取类名
nextName = pending.next();
return true;
} // 获取下一个Service实现
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 {
// 实例化并进行类转换
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
} // for循环遍历时
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
} public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
} // 禁止删除
public void remove() {
throw new UnsupportedOperationException();
} }将给定URL的内容作为提供程序配置文件进行分析。
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}按行解析配置文件,并保存names列表中
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
// 判断provider容器中是否包含 不包含则讲classname加入 names列表中
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
5. 总结
优点:使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
缺点:线程不安全,虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
JDK源码解析之Java SPI机制的更多相关文章
- 设计模式-简单工厂Coding+jdk源码解析
感谢慕课geely老师的设计模式课程,本套设计模式的所有内容均以课程为参考. 前面的软件设计七大原则,目前只有理论这块,因为最近参与项目重构,暂时没有时间把Coding的代码按照设计思路一点点写出来. ...
- [源码解析] 当 Java Stream 遇见 Flink
[源码解析] 当 Java Stream 遇见 Flink 目录 [源码解析] 当 Java Stream 遇见 Flink 0x00 摘要 0x01 领域 1.1 Flink 1.2 Java St ...
- 【Dubbo】带着问题看源码:什么是SPI机制?Dubbo是如何实现的?
什么是SPI? 在Java中,SPI全称为 Service Provider Interface,是一种典型的面向接口编程机制.定义通用接口,然后具体实现可以动态替换,和 IoC 有异曲同工之妙. ...
- Android -- 从源码解析Handle+Looper+MessageQueue机制
1,今天和大家一起从底层看看Handle的工作机制是什么样的,那么在引入之前我们先来了解Handle是用来干什么的 handler通俗一点讲就是用来在各个线程之间发送数据的处理对象.在任何线程中,只要 ...
- 从JDK源码角度看java并发的公平性
JAVA为简化开发者开发提供了很多并发的工具,包括各种同步器,有了JDK我们只要学会简单使用类API即可.但这并不意味着不需要探索其具体的实现机制,本文从JDK源码角度简单讲讲并发时线程竞争的公平性. ...
- 从JDK源码角度看java并发的原子性如何保证
JDK源码中,在研究AQS框架时,会发现很多地方都使用了CAS操作,在并发实现中CAS操作必须具备原子性,而且是硬件级别的原子性,java被隔离在硬件之上,明显力不从心,这时为了能直接操作操作系统层面 ...
- java.lang.Void类源码解析_java - JAVA
文章来源:嗨学网 敏而好学论坛www.piaodoo.com 欢迎大家相互学习 在一次源码查看ThreadGroup的时候,看到一段代码,为以下: /* * @throws NullPointerEx ...
- Integer.parseInt不同jdk源码解析
执行以下代码: System.out.println(Integer.parseInt("-123")); System.out.println(Integer.parseInt( ...
- dubbo源码分析01:SPI机制
一.什么是SPI SPI全称为Service Provider Interface,是一种服务发现机制,其本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件.这样可以在运行时,动态为 ...
随机推荐
- windows更改命令行cmd的字体为conlosas+微软雅黑
windows更改命令行cmd的字体为conlosas+微软雅黑 动力来源于对美孜孜不倦的追求~ 下载conlosas+微软雅黑字体 谢谢支持. 将解压后的YaHei.Consolas.1.12.tt ...
- python3调用zabbix api
前言 今天写了一个zabbix api,这里整理一下,过程中自己也学习了一些东西. 需求 我们需要查一个文件内容如下: cat /ops/flume_hosts node1 node2 需要把这个文件 ...
- JDK、JRE、JVM之间的关系
JDK.JRE.JVM之间的关系 1.JDK下载地址 http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads ...
- 团队博客-第六周:Alpha阶段项目复审(只会嘤嘤嘤队)
小组名 题目 优点 缺点 排名 小谷围驻广东某工业719电竞大队 广工生活社区 功能多样,设计完整,实用,界面美观 界面风格不够统一,当前时间系统尚未发布 1 大猪蹄子队 四六级背单词游戏 界面十分美 ...
- Apollo配置管理系统使用
- go语言异常处理
go语言异常处理 error接口 go语言引入了一个关于错误错里的标准模式,即error接口,该接口的定义如下: type error interface{ Error() string } 对于要返 ...
- JS应用实例4:表格隔行换色
HTML代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <t ...
- C#6.0语言规范(二) 词法结构
程式 AC#程序由一个或多个源文件组成,正式称为编译单元(编译单元).源文件是Unicode字符的有序序列.源文件通常与文件系统中的文件一一对应,但不需要此对应关系.为了获得最大的可移植性,建议使用U ...
- Java的组合排列问题
从4个人中选2个人参加活动,一共有6种选法. 从n个人中选m个人参加活动,一共有多少种选法?C(m/n)=C((m-1)/(n-1))+C(m/(n-1))数学算法 public class Main ...
- Linux快速目录间切换cd pushd popd
1. cd - 当前目录和之前所在的目录之间的切换 2. cd + Alt . 用上次命令的最后一个目录路径 要用上上次命令的最后一个目录,就Alt+.两次就可以了 3. push ...