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. CSS Grid

    效果图如上所示 <!DOCTYPE html> <html> <head> <title>练习</title> </head> ...

  2. GitHub上好的Java项目

    1. java-design-patterns(Star:36k)Github地址:https://github.com/iluwatar/java-design-patterns 介绍:设计模式是形 ...

  3. PostgreSQL和MySQL

    PostgreSQL分布式

  4. RxSwift学习笔记10:startWith/merge/zip/combineLatest/withLatestFrom/switchLatest

    //startWith //该方法会在 Observable 序列开始之前插入一些事件元素.即发出事件消息之前,会先发出这些预先插入的事件消息 Observable.of(1,2,3) .startW ...

  5. Alpha阶段项目复审(冲鸭队)

    Alpha阶段项目复审(冲鸭队) 组名 优点 缺点 排名 天冷记得穿秋裤队 支持文件离线开源下载,没有限速 部分功能未实现 1 中午吃啥队 点餐系统用户需求较高,系统功能完善 界面可以再完善一下些 2 ...

  6. 关于调试WCF时引发的异常XmlException: Name cannot begin with the '<' character, hexadecimal value 0x3C” on Client Side

    问题描述:在使用VS2015调试WCF时,偶遇抛出异常名称不能以“<”字符(十六进制0x3c)开头,平时运行时(不调试)没有问题的. 解决方法:检查后发现为了检查异常的位置,勾选了引发通用语言运 ...

  7. 理解Docker

    Docker Image OS分为 内核(kernel) 和 用户 空间,kernel 启动后,会挂载root文件系统提供用户空间. Docker Image 就相当于一个 root文件系统.是一个特 ...

  8. IntelliJ Idea 授权服务器使用

    JetBrains授权服务器 1 http://intellij.mandroid.cn/ 支持的版本 IntelliJ IDEA 7.0 或更高ReSharper 3.1 或更高ReSharper ...

  9. Django项目中使用celery做异步任务

    异步任务介绍 在写项目过程中经常会遇到一些耗时的任务, 比如:发送邮件.发送短信等等~.这些操作如果都同步执行耗时长对用户体验不友好,在这种情况下就可以把任务放在后台异步执行 celery就是用于处理 ...

  10. 使用pyenv来管理python版本

    使用pyenv可以很方便的切换python版本,而不会影响系统的python版本,对需要使用supervisor(仅支持python2)托管程序,项目使用python3开发的情况十分有用 pyenv的 ...