JDK源码系列(一) ------ 深入理解SPI机制
什么是SPI机制
最近我建了另一个文章分类,用于扩展JDK
中一些重要但不常用的功能。
SPI
,全名Service Provider Interface
,是一种服务发现机制。它可以看成是一种针对接口实现类的解耦方案。我们只需要采用配置文件方式配置好接口的实现类,就可以利用SPI
机制去加载到它们了,当我们需要修改实现类时,改改配置文件就可以了,而不需要去改代码。
当然,有的同学可能会问,spring
也可以做接口实现类的解耦,是不是SPI
就没用了呢?虽然两者都可以达到相同的目的,但是不一定所有应用都可以引入spring
框架,例如JDBC
自动发现驱动并注册,它就是采用SPI
机制,它就不大可能引入spring
来解耦接口实现类。另外,druid
、dubbo
等都采用了SPI
机制。
怎么使用SPI
需求
利用SPI
机制加载用户服务接口的实现类并测试。
工程环境
JDK
:1.8.0_201
maven
:3.6.1
IDE
:eclipse 4.12
主要步骤
- 编写用户服务类接口和实现类;
- 在
classpath
路径下的META-INF/services
文件夹下配置好接口的实现类; - 利用
SPI
机制加载接口实现类并测试。
创建项目
项目类型Maven Project,打包方式jar
引入依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
编写用户服务类接口
路径:cn.zzs.spi
public interface UserService {
void save();
}
编写接口实现类
路径:cn.zzs.spi
。这里就简单实现就好了。
public class UserServiceImpl1 implements UserService {
@Override
public void save() {
System.err.println("执行服务1的save方法");
}
}
// ------------------------
public class UserServiceImpl2 implements UserService {
@Override
public void save() {
System.err.println("执行服务2的save方法");
}
}
配置接口文件
在resources
路径下创建META-INF/services
文件夹,并以UserService
的全限定类名为文件名,创建一个文件。如图所示。
文件中写入接口实现类的全限定类名,多个用换行符隔开。
cn.zzs.spi.UserServiceImpl1
cn.zzs.spi.UserServiceImpl2
编写测试方法
路径:test下的cn.zzs.spi
。如果实际项目中配置了比较多的接口文件,可以考虑抽取工具类。
public class UserServiceTest {
@Test
public void test() {
// 1. 创建一个ServiceLoader对象
ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
// 2. 创建一个迭代器
Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
// 3. 加载配置文件并实例化接口实现类
while(userServiceIterator.hasNext()) {
UserService userService = userServiceIterator.next();
userService.save();
System.out.println("==================");
}
}
}
测试结果
执行服务1的save方法
==================
执行服务2的save方法
==================
SPI在JDBC中的应用
本文以mysql
8.0.15版本的驱动来说明。首先,当我们调用Class.forName("com.mysql.cj.jdbc.Driver")
时,会去执行这个类的静态代码块,在静态代码块中就会完成驱动注册。
static {
try {
//静态代码块中注册当前驱动
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
JDK
6后不再需要Class.forName(driver)
也能注册驱动。因为从JDK6
开始,DriverManager
增加了以下静态代码块,当类被加载时会执行static代码块的loadInitialDrivers
方法。
而这个方法会通过查询系统参数(jdbc.drivers
)和SPI
机制两种方式去加载数据库驱动。
注意:考虑篇幅,以下代码经过修改,仅保留所需部分。
static {
loadInitialDrivers();
}
//这个方法通过两个渠道加载所有数据库驱动:
//1. 查询系统参数jdbc.drivers获得数据驱动类名
//2. SPI机制
private static void loadInitialDrivers() {
//通过系统参数jdbc.drivers读取数据库驱动的全路径名。该参数可以通过启动参数来设置,其实引入SPI机制后这一步好像没什么意义了。
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
//使用SPI机制加载驱动
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//读取META-INF/services/java.sql.Driver文件的类全路径名。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//加载并初始化类
try{
while(driversIterator.hasNext()) {
// 这里才会去实例化驱动
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
if (drivers == null || drivers.equals("")) {
return;
}
//加载jdbc.drivers参数配置的实现类
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
try {
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
在mysql
的驱动包中,我们可以看到SPI
的配置文件。
源码分析
本文将根据测试例子中方法的调用顺序来分析。
@Test
public void test() {
// 1. 创建一个ServiceLoader对象
ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
// 2. 创建一个迭代器
Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
// 3. 加载配置文件并实例化接口实现类
while(userServiceIterator.hasNext()) {
UserService userService = userServiceIterator.next();
userService.save();
System.out.println("==================");
}
}
注意:考虑篇幅,以下代码经过修改,仅保留所需部分。
创建一个ServiceLoader
我们从load(Class service)
方法开始分析,可以看到,调用这个方法时还不会去加载配置文件和初始化接口实现类。因为SPI
采用延迟加载的方式,只有去调用hasNext()
才会去加载配置文件,调用next()
才会去实例化对象。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获得当前线程上下文的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return 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();
}
// 存放接口实现类对象。形式为全限定类名=实例对象
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 迭代器,有加载和实例化接口实现类的方法
private LazyIterator lookupIterator;
public void reload() {
// 清空存放的接口实现类对象
providers.clear();
// 创建一个LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
// LazyIterator是ServiceLoader的内部类
private class LazyIterator implements Iterator<S> {
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
}
创建一个迭代器
因为SPI
机制采用了延迟加载的方式,所以在没有调用next()
之前,providers
会是一个空的Map
,也就是说以下的knownProviders
也会是一个空的迭代器,所以,这个时候都必须去调用lookupIterator
的方法,本文讨论的正是这种情况。
public Iterator<S> iterator() {
return new Iterator<S>() {
// providers的迭代器,一般为空
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
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();
}
};
}
加载配置文件
前面已经提到,当调用hasNext()
时才会去加载配置文件。那么,我们直接看LazyIterator
的hasNext()
方法
// 接口类型
Class<S> service;
// 类加载器
ClassLoader loader;
// 配置文件列表,一般只有一个
Enumeration<URL> configs = null;
// 所有实现类全限定类名的迭代器
Iterator<String> pending = null;
// 下一个实现类全限定类名
String nextName = null;
public boolean hasNext() {
return hasNextService();
}
private boolean hasNextService() {
// 判断是否有下一个实现类全限定类名,有的话直接返回true
// 第一次调用这个方法nextName肯定是null的
if(nextName != null) {
return true;
}
// 下面就是加载配置文件了
if(configs == null) {
// 本文例子中:fullName = META-INF/services/cn.zzs.spi.UserService
String fullName = PREFIX + service.getName();
if(loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
}
// pending是所有实现类全限定类名的迭代器,此时是空
while((pending == null) || !pending.hasNext()) {
// 如果文件中没有配置实现类,直接返回false
if(!configs.hasMoreElements()) {
return false;
}
// 解析配置文件,并初始化pending迭代器
pending = parse(service, configs.nextElement());
}
// 将第一个实现类的全限定类名赋值给nextName
nextName = pending.next();
return true;
}
解析的过程就是简单的IO操作,这里就不再扩展了。
实例化接口实现类
前面已经提到,当调用next()
时才会去实例化接口实现类。那么,我们直接看LazyIterator
的next()
方法。
public S next() {
return nextService();
}
private S nextService() {
// 判断是否有下一个接口实现类。因为前面已经有nextName,所以直接返回true
if (!hasNextService())
throw new NoSuchElementException();
// 获得下一个接口实现类的全限定类名
String cn = nextName;
// 将nextName置空,这样下次调用hasNext()就会重新赋值nextName
nextName = null;
Class<?> c = null;
// 加载接口实现类
c = Class.forName(cn, false, loader);
// 判断是否是指定接口的实现类
if (!service.isAssignableFrom(c)) {
fail(service,"Provider " + cn + " not a subtype");
}
// 转化为指定类型
S p = service.cast(c.newInstance());
// 放入providers的Map中
// 前面提到过,只有调用了next()方法,这个Map才会放入元素
providers.put(cn, p);
return p;
}
以上,SPI
的源码基本分析完。
参考资料
本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/12114744.html
JDK源码系列(一) ------ 深入理解SPI机制的更多相关文章
- 深入学习JDK源码系列之、ArrayList
前言 JDK源码解析系列文章,都是基于JDK8分析的,虽然JDK15马上要出来了,但是JDK8我还不会,我... 类图 实现了RandomAccess接口,可以随机访问 实现了Cloneable接口, ...
- JDK源码系列总索引
一 目标 记录学习jdk源码的一些笔记和心得,jdk版本使用11.0.1,工具idea Class后面序号为优先级1-4,优先级递减 目录转载自博客: https://blog.csdn.net/qq ...
- HashSet源码分析:JDK源码系列
1.简介 继续分析源码,上一篇文章把HashMap的分析完毕.本文开始分析HashSet简单的介绍一下. HashSet是一个无重复元素集合,内部使用HashMap实现,所以HashMap的特征耶继承 ...
- 【JDK源码系列】ConcurrentHashMap
并发永远是高性能的话题,而并发容器又是java中重要的并发工具,所以今天我们来分析一下Concurrent包中ConcurrentHashMap(以下简称Chashmap).普通容器在某些并发情况下的 ...
- motan源码分析二:使用spi机制进行类加载
在motan的源码中使用了很多的spi机制进行对象的创建,下面我们来具体分析一下它的实现方法. 1.在实际的jar包的\META-INF\services目录中引入相关的文件,例如下图中,我解压了co ...
- hbase源码系列(十三)缓存机制MemStore与Block Cache
这一章讲hbase的缓存机制,这里面涉及的内容也是比较多,呵呵,我理解中的缓存是保存在内存中的特定的便于检索的数据结构就是缓存. 之前在讲put的时候,put是被添加到Store里面,这个Store是 ...
- 13 hbase源码系列(十三)缓存机制MemStore与Block Cache
这一章讲hbase的缓存机制,这里面涉及的内容也是比较多,呵呵,我理解中的缓存是保存在内存中的特定的便于检索的数据结构就是缓存. 之前在讲put的时候,put是被添加到Store里面,这个Store是 ...
- Java是如何实现自己的SPI机制的? JDK源码(一)
注:该源码分析对应JDK版本为1.8 1 引言 这是[源码笔记]的JDK源码解读的第一篇文章,本篇我们来探究Java的SPI机制的相关源码. 2 什么是SPI机制 那么,什么是SPI机制呢? SPI是 ...
- JDK源码学习系列05----LinkedList
JDK源码学习系列05----LinkedList 1.LinkedList简介 LinkedList是基于双向链表实 ...
随机推荐
- 【NS2】NS2中802.11代码深入理解—packet传输的流程(转载)
如何传送一个封包(How to transmit a packet?)首先,我们要看的第一个function是在mac-802_11.cc内的recv( ),程式会先判断目前呼叫recv( )这个pa ...
- Spring中配置DataSource数据源的几种选择
从JNDI获得DataSource. 从第三方的连接池获得DataSource. 使用DriverManagerDataSource获得DataSource. 一.从JNDI获得DataSource ...
- 【Leetcode链表】合并两个有序链表(21)
题目 将两个有序链表合并为一个新的有序链表并返回.新链表是通过拼接给定的两个链表的所有节点组成的. 示例: 输入:1->2->4, 1->3->4 输出:1->1-> ...
- c++ 对象池的创建
template <class T> class ObjectPool { public: using DeleterType = std::function<void(T*)> ...
- 二分查找 Day08
package com.sxt.arraytest2; /* * 二分查找 前提:有序 */ public class TestBinarySearch { public static void ma ...
- QQ 聊天机器人API
QQ机器人是腾讯陆续推出的的人工智能聊天机器人的总称. 都说小Q妹妹聪明好学,我们能够教她说话.也能够请他帮忙查询邮编.手机号,或者解释成语.翻译成语,据说她还会查询手机号码归属地.应用科学计算器. ...
- MySQL数据库优化(五)——MySQL查询优化
http://blog.csdn.net/daybreak1209/article/details/51638187 一.mysql查询类型(默认查询出所有数据列)1.内连接 默认多表关联 ...
- laravel5 怎么获取数组形式的数据
当构建 JSON API 时,您可能常常需要把模型和关联对象转换成数组或JSON.所以Eloquent里已经包含了这些方法.要把模型和已载入的关联对象转成数组,可以使用 toArray方法: $use ...
- laravel 授权使用gate门类
第一:先注册 第二:使用方式三种 路由中:Route::group(['middleware'=>'can:system'],function() {}) 模板中:@can("syst ...
- get和post的区别?
GET:一般用于信息获取,使用URL传递参数,对所发送信息的数量也有限制,一般在2000个字符 POST:一般用于 修改服务器上的资源,对所发送的信息没有限制. GET方式需要使用Request.Qu ...