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. windows更改命令行cmd的字体为conlosas+微软雅黑

    windows更改命令行cmd的字体为conlosas+微软雅黑 动力来源于对美孜孜不倦的追求~ 下载conlosas+微软雅黑字体 谢谢支持. 将解压后的YaHei.Consolas.1.12.tt ...

  2. python3调用zabbix api

    前言 今天写了一个zabbix api,这里整理一下,过程中自己也学习了一些东西. 需求 我们需要查一个文件内容如下: cat /ops/flume_hosts node1 node2 需要把这个文件 ...

  3. JDK、JRE、JVM之间的关系

       JDK.JRE.JVM之间的关系 1.JDK下载地址 http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads ...

  4. 团队博客-第六周:Alpha阶段项目复审(只会嘤嘤嘤队)

    小组名 题目 优点 缺点 排名 小谷围驻广东某工业719电竞大队 广工生活社区 功能多样,设计完整,实用,界面美观 界面风格不够统一,当前时间系统尚未发布 1 大猪蹄子队 四六级背单词游戏 界面十分美 ...

  5. Apollo配置管理系统使用

  6. go语言异常处理

    go语言异常处理 error接口 go语言引入了一个关于错误错里的标准模式,即error接口,该接口的定义如下: type error interface{ Error() string } 对于要返 ...

  7. JS应用实例4:表格隔行换色

    HTML代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <t ...

  8. C#6.0语言规范(二) 词法结构

    程式 AC#程序由一个或多个源文件组成,正式称为编译单元(编译单元).源文件是Unicode字符的有序序列.源文件通常与文件系统中的文件一一对应,但不需要此对应关系.为了获得最大的可移植性,建议使用U ...

  9. Java的组合排列问题

    从4个人中选2个人参加活动,一共有6种选法. 从n个人中选m个人参加活动,一共有多少种选法?C(m/n)=C((m-1)/(n-1))+C(m/(n-1))数学算法 public class Main ...

  10. Linux快速目录间切换cd pushd popd

    1.   cd -     当前目录和之前所在的目录之间的切换 2.   cd + Alt . 用上次命令的最后一个目录路径 要用上上次命令的最后一个目录,就Alt+.两次就可以了 3.   push ...