什么是SPI机制

最近我建了另一个文章分类,用于扩展JDK中一些重要但不常用的功能。

SPI,全名Service Provider Interface,是一种服务发现机制。它可以看成是一种针对接口实现类的解耦方案。我们只需要采用配置文件方式配置好接口的实现类,就可以利用SPI机制去加载到它们了,当我们需要修改实现类时,改改配置文件就可以了,而不需要去改代码。

当然,有的同学可能会问,spring也可以做接口实现类的解耦,是不是SPI就没用了呢?虽然两者都可以达到相同的目的,但是不一定所有应用都可以引入spring框架,例如JDBC自动发现驱动并注册,它就是采用SPI机制,它就不大可能引入spring来解耦接口实现类。另外,druiddubbo等都采用了SPI机制。

怎么使用SPI

需求

利用SPI机制加载用户服务接口的实现类并测试。

工程环境

JDK:1.8.0_201

maven:3.6.1

IDE:eclipse 4.12

主要步骤

  1. 编写用户服务类接口和实现类;
  2. classpath路径下的META-INF/services文件夹下配置好接口的实现类;
  3. 利用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!");
}
}

JDK6后不再需要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()时才会去加载配置文件。那么,我们直接看LazyIteratorhasNext()方法

	// 接口类型
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()时才会去实例化接口实现类。那么,我们直接看LazyIteratornext()方法。

    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的源码基本分析完。

参考资料

-深入理解SPI机制

相关源码请移步:https://github.com/ZhangZiSheng001/01-spi-demo

本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/12114744.html

JDK源码系列(一) ------ 深入理解SPI机制的更多相关文章

  1. 深入学习JDK源码系列之、ArrayList

    前言 JDK源码解析系列文章,都是基于JDK8分析的,虽然JDK15马上要出来了,但是JDK8我还不会,我... 类图 实现了RandomAccess接口,可以随机访问 实现了Cloneable接口, ...

  2. JDK源码系列总索引

    一 目标 记录学习jdk源码的一些笔记和心得,jdk版本使用11.0.1,工具idea Class后面序号为优先级1-4,优先级递减 目录转载自博客: https://blog.csdn.net/qq ...

  3. HashSet源码分析:JDK源码系列

    1.简介 继续分析源码,上一篇文章把HashMap的分析完毕.本文开始分析HashSet简单的介绍一下. HashSet是一个无重复元素集合,内部使用HashMap实现,所以HashMap的特征耶继承 ...

  4. 【JDK源码系列】ConcurrentHashMap

    并发永远是高性能的话题,而并发容器又是java中重要的并发工具,所以今天我们来分析一下Concurrent包中ConcurrentHashMap(以下简称Chashmap).普通容器在某些并发情况下的 ...

  5. motan源码分析二:使用spi机制进行类加载

    在motan的源码中使用了很多的spi机制进行对象的创建,下面我们来具体分析一下它的实现方法. 1.在实际的jar包的\META-INF\services目录中引入相关的文件,例如下图中,我解压了co ...

  6. hbase源码系列(十三)缓存机制MemStore与Block Cache

    这一章讲hbase的缓存机制,这里面涉及的内容也是比较多,呵呵,我理解中的缓存是保存在内存中的特定的便于检索的数据结构就是缓存. 之前在讲put的时候,put是被添加到Store里面,这个Store是 ...

  7. 13 hbase源码系列(十三)缓存机制MemStore与Block Cache

    这一章讲hbase的缓存机制,这里面涉及的内容也是比较多,呵呵,我理解中的缓存是保存在内存中的特定的便于检索的数据结构就是缓存. 之前在讲put的时候,put是被添加到Store里面,这个Store是 ...

  8. Java是如何实现自己的SPI机制的? JDK源码(一)

    注:该源码分析对应JDK版本为1.8 1 引言 这是[源码笔记]的JDK源码解读的第一篇文章,本篇我们来探究Java的SPI机制的相关源码. 2 什么是SPI机制 那么,什么是SPI机制呢? SPI是 ...

  9. JDK源码学习系列05----LinkedList

                                             JDK源码学习系列05----LinkedList 1.LinkedList简介 LinkedList是基于双向链表实 ...

随机推荐

  1. 《C程序设计语言》笔记(一)

    一:导言 1:printf中的格式化字符串: %ld                    按照long整型打印 %6d                   按照十进制整数打印,至少6个字符宽,不够的 ...

  2. 【NS2】trace 文件格式(转载)

    本文档是对 http://nsnam.isi.edu/nsnam/index.php/NS-2_Trace_Formats > 的翻译. 译注:本文描述的无线格Trace格式已经有些陈旧,现在一 ...

  3. SQL语法之DDL和DML

    SQL语法之DDL和DML        DDL数据库定义语言 create 创建 alter 修改 drop 删除 drop和delete的区别 truncate DML 数据操作语言 insert ...

  4. QT自定义窗口

    qt 中允许自定义窗口控件,使之满足特殊要求, (1)可以修改其显示,自行绘制 (2)可以动态显示 (3)可以添加事件,支持鼠标和键盘操作 自定义控件可以直接在QtDesigner里使用,可以直接加到 ...

  5. 使用css制作三角

    1. 字符实现三角效果关于字符实现三角我早在09年的时候就介绍了:使用字符实现兼容性的圆角尖角效果.一转眼两年过去了,这个技术开始被越来越多的人所熟知.使用的字符是正棱形“◆”字符,编码表示为◆ . ...

  6. 06多次查询某区间内topk问题

            题目描述:给定一个数组,需要多次查找不同区间内的,第k大或者第k小的元素.         考虑题目是多次查找,如果采用只对查询区间内的元素进行排序的思路,然后输出第k大的数的策略,那 ...

  7. IP应用加速技术详解:如何提升动静混合站点的访问速率?

    全站加速(DCDN)-IPA是阿里云自主研发四层加速产品,它基于TCP/UDP的私有协议提供加速服务,包括解决跨运营商网络不稳定.单线源站.突发流量.网络拥塞等诸多因素导致的延迟高.服务不稳定的问题, ...

  8. behavior planning——10 behaior planning pseudocode

    One way to implement a transition function is by generating rough trajectories for each accessible & ...

  9. H3C TCP/UDP端口号

  10. canvas+js实现验证码功能

    转载自:https://blog.csdn.net/qq_42463851/article/details/90755734<!DOCTYPE html> <html> < ...