Java 的 SPI 机制
什么是SPI机制?
SPI机制( Service Provider Interface)是Java的一种服务发现机制,为了方便应用扩展。那什么是服务发现机制?简单来说,就是你定义了一个接口,但是不提供实现,接口实现由其他系统应用实现。你只需要提供一种可以找到其他系统提供的接口实现类的能力或者说机制.
SPI机制在Java中有很广泛的运用,比如:eclipse和idea里的插件使用就是通过SPI机制实现的。开发工具提供一个扩展接口,具体的实现由插件开发者实现,开发工具提供一种服务发现机制来找到具体插件的实现,这就达到了插件的安装效果。从而可以使用插件服务。如果不需要某一插件,只需要删除某一插件的实现类,开发工具找不到具体的插件实现,这就达到了插件的卸载效果。不管是安装还是卸载都不会影响其他代码,其他服务。非常方便的实现了可插拔的效果。
JDBC中数据库连接驱动也使用了SPI机制,来达到适配不同DB数据库的效果。
SPI机制除了在jdk里有运用,在springboot中也用到了。springboot自动装配中"查找spring.factories 文件步骤"就是基于SPI的部分设计思想实现的。
SPI 有如下的好处:
不需要改动源码就可以实现扩展,解耦。
实现扩展对原来的代码几乎没有侵入性。
只需要添加配置就可以实现扩展,符合开闭原则。
API 和 SPI 区别
API:大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用。
SPI :是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。

SPI实现服务接口与服务实现的解耦:
- 服务提供者(如 springboot starter)提供出 SPI 接口,让客户端去自定义实现。
- 客户端(普通的 springboot 项目)即可通过本地注册的形式,将实现类注册到服务端,轻松实现可插拔。
  
简单实现
定义接口
package com.test.service;
public interface ISpi {
    void say();
}
第一个实现类:
package com.test.service.impl;
import com.test.service.ISpi;
public class FirstSpiImpl implements ISpi {
    @Override
    public void say() {
        System.out.println("我是第一个SPI实现类");
    }
}
第二个实现类:
package com.test.service.impl;
import com.test.service.ISpi;
public class SecondSpiImpl implements ISpi {
    @Override
    public void say() {
        System.out.println("我是第二个SPI实现类");
    }
}
在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名,并写上需要动态加载的实现类的全路径名。
#com.test.service.impl.FirstSpiImpl
com.test.service.impl.SecondSpiImpl
ServiceLoader
ServiceLoader是JDK提供的专门用于实现SPI机制的类。位于java.util.ServiceLoader
ServiceLoader类的构造函数被私有化了。所以构建ServiceLoader对象只能通过ServiceLoader.load()方法。该方法有两个重载
使用ServiceLoader时可选择是否用自定义类加载器来加载目标类。也可默认使用应用程序类加载器加载。
jdk通过ServiceLoader类去ClassPath下的 “META-INF/services/”(此路径约定成俗) 路径里查找相应的接口实现类。ServiceLoader类核心功能就两个点,都在ServiceLoader的内部类LazyIterator中:
- 查找相应接口对应实现类:hasNextService()
- 加载相应接口实现类到虚拟机内:nextService()
public final class ServiceLoader<S> implements Iterable<S> {
    //扫描目录前缀
    private static final String PREFIX = "META-INF/services/";
    // 被加载的类或接口
    private final Class<S> service;
    // 用于定位、加载和实例化实现方实现的类的类加载器
    private final ClassLoader loader;
    // 上下文对象
    private final AccessControlContext acc;
    // 按照实例化的顺序缓存已经实例化的类
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
    // 懒查找迭代器
    private java.util.ServiceLoader.LazyIterator lookupIterator;
    // 私有内部类,提供对所有的service的类的加载与实例化
    private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        String nextName = null;
        //...
        private boolean hasNextService() {
            if (configs == null) {
                try {
                    //获取目录下所有的类 扫描目录前缀(META-INF/services/)+ 相应接口全限定名
                    String fullName = PREFIX + service.getName();
                    //该loader是构造ServiceLoader类时设置。可传入自定义类加载器,如未传入,则默认应用程序类加载器
                    if (loader == null)
                        //在系统中查找资源,注意查找资源的加载器是从当前线程上下文中获取。也就是默认的应用程序类加载器。所以能加载到第三方jar包下的classpath路径。
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    //...
                }
                //....
            }
        }
        private S nextService() {
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //反射加载类
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
            }
            try {
                //实例化
                S p = service.cast(c.newInstance());
                //放进缓存
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                //..
            }
            //..
        }
    }
}
应用程序通过迭代器接口获取对象实例,这里首先会判断 providers 对象中是否有实例对象:
- 有实例,那么就返回
- 没有,执行类的装载步骤,具体类装载实现如下:
LazyIterator#hasNextService 读取 META-INF/services 下的配置文件,获得所有能被实例化的类的名称,并完成 SPI 配置文件的解析
LazyIterator#nextService 负责实例化 hasNextService() 读到的实现类,并将实例化后的对象存放到 providers 集合中缓存


应用案例
Java定义了一套JDBC的接口,但并未提供具体实现类,而是在不同厂商提供的数据库实现包。
一般要根据自己使用的数据库驱动jar包,比如我们最常用的MySQL,其mysql-jdbc-connector.jar 里面就有:

小结
JDK中的SPI实现,是由ServiceLoader类根据自定义传入类加载器或者应用程序类加载器在约定好的固定路径下(ClassPath:META-INF/services/)去查找和加载第三方接口实现类。
注意:要使用JDK中的SPI机制有几个前提条件
- 服务提供方必须实现目标接口
- 服务提供方必须在自身ClassPath:META-INF/services/路径下建立文件,文件名为目标接口全限定名。文件内容为实现目标接口的具体实现类全限定名
Java 的 SPI 机制的更多相关文章
- java 的SPI机制
		今天看到spring mvc 使用Java Validation Api(JSR-303)进行校验,需要加载一个 其具体实现(比如Hibernate Validator), 本来没有什么问题,但是突然 ... 
- Java的SPI机制与简单的示例
		一.SPI机制 这里先说下SPI的一个概念,SPI英文为Service Provider Interface单从字面可以理解为Service提供者接口,正如从SPI的名字去理解SPI就是Service ... 
- Java之SPI机制
		之前开阿里的HSF框架,里面用到了Java的SPI机制,今天闲暇的时候去了解了一下,通过写博客来记录一下 SPI的全名为Service Provider Interface,我对于该机制的理解是为接口 ... 
- Java的Spi机制心得
		Java spi : 是Java EE 给服务供应商提供的接口,供应商遵循接口契约提供自己的实现.. 简单来讲就是为某个接口寻找服务实现的机制. 在看JDBC源码当看到DriverManage.get ... 
- 深入理解 Java 中 SPI 机制
		本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/vpy5DJ-hhn0iOyp747oL5A作者:姜柱 SPI(Service Provider ... 
- JAVA中SPI机制
		之前研究dubbo的时候就很好奇,里面各种扩展机制,期间也看过很多关于SPI的机制,今日有缘再度看到有文章总结,故记录一下, 首先了解一下 JAVA中SPI简单的用法 可参考这篇文章,https:// ... 
- Java的SPI机制
		目录 1. 什么是SPI 2. 为什么要使用SPI 3. 关于策略模式和SPI的几点区别 4. 使用介绍或者说约定 4.1 首先介绍几个名词 4.2 约定 5. 具体的demo实现 5.1 创建服务提 ... 
- Java spi机制浅谈
		最近看到公司的一些框架和之前看到的开源的一些框架的一些服务发现和接入都采用了java的spi机制. 所以简单的总结下java spi机制的思想. 我们系统里抽象的各个模块,往往有很多不同的实现方案,比 ... 
- 聊聊Java SPI机制
		一.Java SPI机制 SPI(Service Provider Interface)是JDK内置的服务发现机制,用在不同模块间通过接口调用服务,避免对具体服务服务接口具体实现类的耦合.比如JDBC ... 
- 你应该了解的 Java SPI 机制
		前言 不知大家现在有没有去公司复工,我已经在家办公将近 3 周了,同时也在家呆了一个多月:还好工作并没有受到任何影响,我个人一直觉得远程工作和 IT 行业是非常契合的,这段时间的工作效率甚至比在办公室 ... 
随机推荐
- typora基础语法
			Markdown学习 标题 三级标题 #加空格加你要的文字 字体 加粗 hello world!(前后两个**) hello world!(前后一个**) hello world!(前后三个***) ... 
- Think Python 学习笔记
			#!/usr/bin/env python# coding: utf-8# # Think Python 学习笔记# 1.关于异或计算符# In[2]:6^2# 2.关于函数# 注意:变量名称不能用数 ... 
- 自行封装JDBCUtils
			自己封装JDBCUtils package com.javasm.util; import com.javasm.bean.Emp; import com.javasm.constants.JDBCC ... 
- Windows下fmt库的链接与使用
			下载源码. 使用mingw编译源码.注意设置cmake文件的产生路径.pkgconfig文件的产生路径(windows下用不到产生的pc文件).库的安装路径. make -j8 install. 新建 ... 
- NVT模拟时出现水分子空洞,以及NPT时盒子收缩的原理
			NVT出现水分子空洞,以及NPT时盒子收缩的原理: For a periodic system, constant pressure is the only way to equilibrate de ... 
- database.property文件
			注意修改用户名密码 mysql8的版本要注意配置时区 此文件放置连接数据库的相关参数 jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://l ... 
- pip第三方库安装失败原因及解决办法
			pip安装三方库失败原因及解决方法 提示:WARNING: You are using pip version 20.2.3, however version 20.2.4 is available. ... 
- 发布jar包到远程仓库 (maven deploy)
			背景: 项目有开放服务模块,现有个需求,需要把开放服务提供成一个jar包,用户可以直接对接. 流程: 1.在pom.xml文件添加distributionManagement节点,将项目打包上传到私服 ... 
- luac编译命令
			luac -o out.lua 1.lua 可以不要后缀 luac -o out 1.lua 
- 集成电路仿真器(SPICE)的实现原理
			本文系统地介绍类SPICE集成电路仿真器的实现原理,包括改进节点分析(MNA).非线性器件建模.DC/AC分析.时域/(复)频域仿真以及涉及的数值方法. 基于介绍的原理,实现了SPICE-like仿真 ... 
