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);
    }

      

  1. 创建AlipayServiceWechatPayService,实现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("使用微信支付");
    }
    }
  2. resources目录下创建目录META-INF/services

  3. 在META-INF/services创建com.imooc.spi.PayService文件

  4. 先以AlipayService为例:在com.imooc.spi.PayService添加com.imooc.spi.AlipayService的文件内容

  5. 创建测试类

    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));
    }
    }
    }
  6. 运行测试类,查看返回结果

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具体源码,代码量不多,实现的流程如下:

  1. 应用程序调用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);
    }

      

     
  2. 我们简单看一下这个懒加载迭代器

    // 实现完全懒惰的提供程序查找的私有内部类
    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();
    } }

      

     
  3. 将给定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();
    }

      

     
  4. 按行解析配置文件,并保存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机制的更多相关文章

  1. 设计模式-简单工厂Coding+jdk源码解析

    感谢慕课geely老师的设计模式课程,本套设计模式的所有内容均以课程为参考. 前面的软件设计七大原则,目前只有理论这块,因为最近参与项目重构,暂时没有时间把Coding的代码按照设计思路一点点写出来. ...

  2. [源码解析] 当 Java Stream 遇见 Flink

    [源码解析] 当 Java Stream 遇见 Flink 目录 [源码解析] 当 Java Stream 遇见 Flink 0x00 摘要 0x01 领域 1.1 Flink 1.2 Java St ...

  3. 【Dubbo】带着问题看源码:什么是SPI机制?Dubbo是如何实现的?

    什么是SPI? ​ 在Java中,SPI全称为 Service Provider Interface,是一种典型的面向接口编程机制.定义通用接口,然后具体实现可以动态替换,和 IoC 有异曲同工之妙. ...

  4. Android -- 从源码解析Handle+Looper+MessageQueue机制

    1,今天和大家一起从底层看看Handle的工作机制是什么样的,那么在引入之前我们先来了解Handle是用来干什么的 handler通俗一点讲就是用来在各个线程之间发送数据的处理对象.在任何线程中,只要 ...

  5. 从JDK源码角度看java并发的公平性

    JAVA为简化开发者开发提供了很多并发的工具,包括各种同步器,有了JDK我们只要学会简单使用类API即可.但这并不意味着不需要探索其具体的实现机制,本文从JDK源码角度简单讲讲并发时线程竞争的公平性. ...

  6. 从JDK源码角度看java并发的原子性如何保证

    JDK源码中,在研究AQS框架时,会发现很多地方都使用了CAS操作,在并发实现中CAS操作必须具备原子性,而且是硬件级别的原子性,java被隔离在硬件之上,明显力不从心,这时为了能直接操作操作系统层面 ...

  7. java.lang.Void类源码解析_java - JAVA

    文章来源:嗨学网 敏而好学论坛www.piaodoo.com 欢迎大家相互学习 在一次源码查看ThreadGroup的时候,看到一段代码,为以下: /* * @throws NullPointerEx ...

  8. Integer.parseInt不同jdk源码解析

    执行以下代码: System.out.println(Integer.parseInt("-123")); System.out.println(Integer.parseInt( ...

  9. dubbo源码分析01:SPI机制

    一.什么是SPI SPI全称为Service Provider Interface,是一种服务发现机制,其本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件.这样可以在运行时,动态为 ...

随机推荐

  1. Chrome浏览器用AdBlockPlus拦截百度广告

    一:安装AdBlockPlus插件,这个貌似要FQ安装,不知道可不可以本地安装: 二:在右侧的扩展那里找到ABP扩展,然后设置-高级-我的过滤列表栏-开始创建我的过滤列表: 三:在列表栏里添加 bai ...

  2. IOPLL动态重配

    连接 Avalon -MM接口 mgmt_waitrequest:当 PLL 重配置进程开始后,此端口变高并在 PLL 重配置期间保持高电平. PLL 重配置进程完成后,此端口变低. I/O PLL重 ...

  3. Codeforces Round #540 (Div. 3)--1118C - Palindromic Matrix

    https://codeforces.com/contest/1118/problem/C 在查找元素的时候,必须按4,2,1的顺序进行.因为,如果先找1,可能就把原来的4拆散了,然后再找4,就找不到 ...

  4. ubuntu 14.04下搭建esp32开发环境

    esp32是乐鑫出品的一款集成了wifi和蓝牙的集成模块,板上自带两个哈佛结构的Xtensa LX6 CPU双核处理器,本文主要讲解如何在linux下搭建其编译开发环境. 首先ctrl+alt+t打开 ...

  5. Docker 启动不了容器的问题

    今天在运行 docker 的时候,就是执行 docker exec 命令的时候,发现一直报错.具体的报错信息如下: Error response from daemon: Container XXX ...

  6. Dynamic Programming | Set 4 (Longest Common Subsequence)

    首先来看什么是最长公共子序列:给定两个序列,找到两个序列中均存在的最长公共子序列的长度.子序列需要以相关的顺序呈现,但不必连续.例如,"abc", "abg", ...

  7. 利用PHP扩展Taint找出网站的潜在安全漏洞实践

    一.背景 笔者从接触计算机后就对网络安全一直比较感兴趣,在做PHP开发后对web安全一直比较关注,2016时无意中发现Taint这个扩展,体验之后发现确实好用:不过当时在查询相关资料时候发现关注此扩展 ...

  8. webpack打包工具

    目的:平时小项目中例如一些网站需要进行打包压缩,用这个工具可以进行打包压缩,就可以上传到服务器. 使用方法: 1,引进需要打包的项目,把入口html替换掉项目中的index.html,把引进的js,c ...

  9. 了解甚少的GNU C的__attribute__ 机制

    平时忙着赶项目,很多东西都是不求甚解,当工作中遇到的一些比较刁钻的问题时,才发现自己和那些大牛的 差距---内功.熟练码农和码神的最大区别估计就是内功是否深厚了.在自我反思的过程中,也要逐渐的积累一些 ...

  10. PHP字母数字验证码和中文验证码

    1:字母数字组合的验证码 HTML代码: 验证码:<input type="text" name="code"> <img onclick=& ...