什么是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

引入依赖

  1. <dependency>
  2. <groupId>junit</groupId>
  3. <artifactId>junit</artifactId>
  4. <version>4.12</version>
  5. <scope>test</scope>
  6. </dependency>

编写用户服务类接口

路径:cn.zzs.spi

  1. public interface UserService {
  2. void save();
  3. }

编写接口实现类

路径:cn.zzs.spi。这里就简单实现就好了。

  1. public class UserServiceImpl1 implements UserService {
  2. @Override
  3. public void save() {
  4. System.err.println("执行服务1的save方法");
  5. }
  6. }
  7. // ------------------------
  8. public class UserServiceImpl2 implements UserService {
  9. @Override
  10. public void save() {
  11. System.err.println("执行服务2的save方法");
  12. }
  13. }

配置接口文件

resources路径下创建META-INF/services文件夹,并以UserService的全限定类名为文件名,创建一个文件。如图所示。

文件中写入接口实现类的全限定类名,多个用换行符隔开。

  1. cn.zzs.spi.UserServiceImpl1
  2. cn.zzs.spi.UserServiceImpl2

编写测试方法

路径:test下的cn.zzs.spi。如果实际项目中配置了比较多的接口文件,可以考虑抽取工具类。

  1. public class UserServiceTest {
  2. @Test
  3. public void test() {
  4. // 1. 创建一个ServiceLoader对象
  5. ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
  6. // 2. 创建一个迭代器
  7. Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
  8. // 3. 加载配置文件并实例化接口实现类
  9. while(userServiceIterator.hasNext()) {
  10. UserService userService = userServiceIterator.next();
  11. userService.save();
  12. System.out.println("==================");
  13. }
  14. }
  15. }

测试结果

  1. 执行服务1save方法
  2. ==================
  3. 执行服务2save方法
  4. ==================

SPI在JDBC中的应用

本文以mysql 8.0.15版本的驱动来说明。首先,当我们调用Class.forName("com.mysql.cj.jdbc.Driver")时,会去执行这个类的静态代码块,在静态代码块中就会完成驱动注册。

  1. static {
  2. try {
  3. //静态代码块中注册当前驱动
  4. java.sql.DriverManager.registerDriver(new Driver());
  5. } catch (SQLException E) {
  6. throw new RuntimeException("Can't register driver!");
  7. }
  8. }

JDK6后不再需要Class.forName(driver)也能注册驱动。因为从JDK6开始,DriverManager增加了以下静态代码块,当类被加载时会执行static代码块的loadInitialDrivers方法。

而这个方法会通过查询系统参数(jdbc.drivers)和SPI机制两种方式去加载数据库驱动。

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

  1. static {
  2. loadInitialDrivers();
  3. }
  4. //这个方法通过两个渠道加载所有数据库驱动:
  5. //1. 查询系统参数jdbc.drivers获得数据驱动类名
  6. //2. SPI机制
  7. private static void loadInitialDrivers() {
  8. //通过系统参数jdbc.drivers读取数据库驱动的全路径名。该参数可以通过启动参数来设置,其实引入SPI机制后这一步好像没什么意义了。
  9. String drivers;
  10. try {
  11. drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
  12. public String run() {
  13. return System.getProperty("jdbc.drivers");
  14. }
  15. });
  16. } catch (Exception ex) {
  17. drivers = null;
  18. }
  19. //使用SPI机制加载驱动
  20. AccessController.doPrivileged(new PrivilegedAction<Void>() {
  21. public Void run() {
  22. //读取META-INF/services/java.sql.Driver文件的类全路径名。
  23. ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  24. Iterator<Driver> driversIterator = loadedDrivers.iterator();
  25. //加载并初始化类
  26. try{
  27. while(driversIterator.hasNext()) {
  28. // 这里才会去实例化驱动
  29. driversIterator.next();
  30. }
  31. } catch(Throwable t) {
  32. // Do nothing
  33. }
  34. return null;
  35. }
  36. });
  37. if (drivers == null || drivers.equals("")) {
  38. return;
  39. }
  40. //加载jdbc.drivers参数配置的实现类
  41. String[] driversList = drivers.split(":");
  42. for (String aDriver : driversList) {
  43. try {
  44. Class.forName(aDriver, true,
  45. ClassLoader.getSystemClassLoader());
  46. } catch (Exception ex) {
  47. println("DriverManager.Initialize: load failed: " + ex);
  48. }
  49. }
  50. }

mysql的驱动包中,我们可以看到SPI的配置文件。

源码分析

本文将根据测试例子中方法的调用顺序来分析。

  1. @Test
  2. public void test() {
  3. // 1. 创建一个ServiceLoader对象
  4. ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
  5. // 2. 创建一个迭代器
  6. Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
  7. // 3. 加载配置文件并实例化接口实现类
  8. while(userServiceIterator.hasNext()) {
  9. UserService userService = userServiceIterator.next();
  10. userService.save();
  11. System.out.println("==================");
  12. }
  13. }

注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

创建一个ServiceLoader

我们从load(Class service)方法开始分析,可以看到,调用这个方法时还不会去加载配置文件和初始化接口实现类。因为SPI采用延迟加载的方式,只有去调用hasNext()才会去加载配置文件,调用next()才会去实例化对象。

  1. public static <S> ServiceLoader<S> load(Class<S> service) {
  2. // 获得当前线程上下文的类加载器
  3. ClassLoader cl = Thread.currentThread().getContextClassLoader();
  4. return ServiceLoader.load(service, cl);
  5. }
  6. public static <S> ServiceLoader<S> load(Class<S> service,
  7. ClassLoader loader)
  8. {
  9. // 创建一个ServiceLoader对象
  10. return new ServiceLoader<>(service, loader);
  11. }
  12. private ServiceLoader(Class<S> svc, ClassLoader cl) {
  13. // 校验接口类型和类加载器是否为空
  14. service = Objects.requireNonNull(svc, "Service interface cannot be null");
  15. loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  16. // 初始化访问控制器
  17. acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
  18. reload();
  19. }
  20. // 存放接口实现类对象。形式为全限定类名=实例对象
  21. private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
  22. // 迭代器,有加载和实例化接口实现类的方法
  23. private LazyIterator lookupIterator;
  24. public void reload() {
  25. // 清空存放的接口实现类对象
  26. providers.clear();
  27. // 创建一个LazyIterator
  28. lookupIterator = new LazyIterator(service, loader);
  29. }
  30. // LazyIterator是ServiceLoader的内部类
  31. private class LazyIterator implements Iterator<S> {
  32. private LazyIterator(Class<S> service, ClassLoader loader) {
  33. this.service = service;
  34. this.loader = loader;
  35. }
  36. }

创建一个迭代器

因为SPI机制采用了延迟加载的方式,所以在没有调用next()之前,providers会是一个空的Map,也就是说以下的knownProviders也会是一个空的迭代器,所以,这个时候都必须去调用lookupIterator的方法,本文讨论的正是这种情况。

  1. public Iterator<S> iterator() {
  2. return new Iterator<S>() {
  3. // providers的迭代器,一般为空
  4. Iterator<Map.Entry<String,S>> knownProviders
  5. = providers.entrySet().iterator();
  6. public boolean hasNext() {
  7. if (knownProviders.hasNext())
  8. return true;
  9. return lookupIterator.hasNext();
  10. }
  11. public S next() {
  12. if (knownProviders.hasNext())
  13. return knownProviders.next().getValue();
  14. return lookupIterator.next();
  15. }
  16. public void remove() {
  17. throw new UnsupportedOperationException();
  18. }
  19. };
  20. }

加载配置文件

前面已经提到,当调用hasNext()时才会去加载配置文件。那么,我们直接看LazyIteratorhasNext()方法

  1. // 接口类型
  2. Class<S> service;
  3. // 类加载器
  4. ClassLoader loader;
  5. // 配置文件列表,一般只有一个
  6. Enumeration<URL> configs = null;
  7. // 所有实现类全限定类名的迭代器
  8. Iterator<String> pending = null;
  9. // 下一个实现类全限定类名
  10. String nextName = null;
  11. public boolean hasNext() {
  12. return hasNextService();
  13. }
  14. private boolean hasNextService() {
  15. // 判断是否有下一个实现类全限定类名,有的话直接返回true
  16. // 第一次调用这个方法nextName肯定是null的
  17. if(nextName != null) {
  18. return true;
  19. }
  20. // 下面就是加载配置文件了
  21. if(configs == null) {
  22. // 本文例子中:fullName = META-INF/services/cn.zzs.spi.UserService
  23. String fullName = PREFIX + service.getName();
  24. if(loader == null)
  25. configs = ClassLoader.getSystemResources(fullName);
  26. else
  27. configs = loader.getResources(fullName);
  28. }
  29. // pending是所有实现类全限定类名的迭代器,此时是空
  30. while((pending == null) || !pending.hasNext()) {
  31. // 如果文件中没有配置实现类,直接返回false
  32. if(!configs.hasMoreElements()) {
  33. return false;
  34. }
  35. // 解析配置文件,并初始化pending迭代器
  36. pending = parse(service, configs.nextElement());
  37. }
  38. // 将第一个实现类的全限定类名赋值给nextName
  39. nextName = pending.next();
  40. return true;
  41. }

解析的过程就是简单的IO操作,这里就不再扩展了。

实例化接口实现类

前面已经提到,当调用next()时才会去实例化接口实现类。那么,我们直接看LazyIteratornext()方法。

  1. public S next() {
  2. return nextService();
  3. }
  4. private S nextService() {
  5. // 判断是否有下一个接口实现类。因为前面已经有nextName,所以直接返回true
  6. if (!hasNextService())
  7. throw new NoSuchElementException();
  8. // 获得下一个接口实现类的全限定类名
  9. String cn = nextName;
  10. // 将nextName置空,这样下次调用hasNext()就会重新赋值nextName
  11. nextName = null;
  12. Class<?> c = null;
  13. // 加载接口实现类
  14. c = Class.forName(cn, false, loader);
  15. // 判断是否是指定接口的实现类
  16. if (!service.isAssignableFrom(c)) {
  17. fail(service,"Provider " + cn + " not a subtype");
  18. }
  19. // 转化为指定类型
  20. S p = service.cast(c.newInstance());
  21. // 放入providers的Map中
  22. // 前面提到过,只有调用了next()方法,这个Map才会放入元素
  23. providers.put(cn, p);
  24. return p;
  25. }

以上,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. Oracle使用——PLSQL查询表结构并导出EXCEL

    背景 有一次需要查询Oracle数据库中的所有表接口并且导出excel,方法记录如下 使用 使用PLSQL工具查询表结构,SQL语句如下 SELECT B.TABLE_NAME AS '表名', C. ...

  2. Java练习 SDUT-1959_简单枚举类型——植物与颜色

    简单枚举类型--植物与颜色 Time Limit: 1000 ms Memory Limit: 65536 KiB Problem Description 请定义具有red, orange, yell ...

  3. TCP/IP,UDP,HTTP,SOCKET之间的区别和关系

    TCP/IP TCP/IP代表传输控制协议/网际协议,指的是一系列协组.可分为四个层次:数据链路层.网络层.传输层和应用层. 在网络层:有IP协议.ICMP协议.ARP协议.RARP协议和BOOTP协 ...

  4. 设计模式 - 工厂模式(factory pattern) 具体解释

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/u012515223/article/details/27081511 工厂模式(factory pa ...

  5. Spring AOP 的实现 原理

    反射实现 AOP 动态代理模式实例说明(Spring AOP 的实现 原理)   比如说,我们现在要开发的一个应用里面有很多的业务方法,但是,我们现在要对这个方法的执行做全面监控,或部分监控.也许我们 ...

  6. OracleSpatial函数

    Oracle_spatial的函数 一sdo_Geom包的函数: 用于表示两个几何对象的关系(结果为True/False)的函数:RELATE,WITHIN_DISTANCE 验证的函数:VALIDA ...

  7. Android教程 -04 启动其它Activity,静态工厂设计模式传递数据

    视频建议采用超清模式观看, 欢迎点击订阅我的优酷 意图 Intent 一个应用程序肯定不只有一个界面,如何切换到其它界面,只时候就需要启动其它的Activity.启动Activity有多种方式.我在这 ...

  8. OpenCV 安装与调试

    Visual Studio 是微软提供的面向任何开发者的同类最佳工具. OpenCV(开源计算机视觉库)是一个开源的计算机视觉和机器学习软件库. 目前最新版本:Visual Studio 2019.O ...

  9. stl_tree.h/insert_unique()

    // 安插新值:節點鍵值不允許重複,若重複則安插無效. // 注意,傳回值是個pair,第一元素是個 RB-tree 迭代器,指向新增節點, // 第二元素表示安插成功與否. template < ...

  10. java项目和npm项目命令窗口部署方便调试

    1.maven窗口选择root项目 在点击lifstyle--> package 点击播放箭头 build完毕以后会出现在TARGET目录下todo.jar 在执行 java -jar todo ...