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. noip第18课资料

  2. MFC开发中添加自定义消息和消息响应函数

    (1)在.h或.cpp文件定义一个消息 #define CLICK_MESSAGE_BOX WM_USER+1001 //add by 20180612 给主窗口ctrl.cpp发送消息 //自定义消 ...

  3. c++ 异常处理(1)

    异常 (exception) 是 c++ 中新增的一个特性,它提供了一种新的方式来结构化地处理错误,使得程序可以很方便地把异常处理与出错的程序分离,而且在使用上,它语法相当地简洁,以至于会让人错觉觉得 ...

  4. kernel解析dtb为节点

    title: 解析dtb为节点 date: 2019/4/26 14:02:18 toc: true --- kernel解析dtb为节点 head.s入口传递 回顾 看以前的笔记 kernel(二) ...

  5. select 的问题

    #include <errno.h> #include <string.h> #include <fcntl.h> #include <sys/socket. ...

  6. Android开发工程师文集-1 小时学会SQLite

    前言 大家好,给大家带来Android开发工程师文集-1 小时学会SQLite的概述,希望你们喜欢 内容 什么是Sqlite: 效率高,开源,小型,程序驱动,支持事务操作,无数据类型,可嵌入的关系型数 ...

  7. 我来谈谈PHP和JAVA在web开发上的的区别

    这里的标题写的是谈谈PHP和JAVA的区别,其实是委婉的说法,其实别人是想听PHP好还是JAVA好!!! 从而从中找到存在感!!! 因为由于我是从多年的php开发转到java开发的.所以最,不时的有好 ...

  8. 【Spark调优】数据本地化与参数调优

    数据本地化对于Spark Job性能有着巨大的影响,如果数据以及要计算它的代码是在一起的,那么性能当然会非常高.但是,如果数据和计算它的代码是分开的,那么其中之一必须到另外一方的机器上.移动代码到其匹 ...

  9. vue 自学笔记(6) axios的使用

    前情提要:axios 的使用 axios是一个ajax 的包,主要在node.js 使用 axios 的官网 https://www.kancloud.cn/yunye/axios/234845 一: ...

  10. Spark基础脚本入门实践3:Pair RDD开发

    Pair RDD转化操作 val rdd = sc.parallelize(List((1,2),(3,4),(3,6))) //reduceByKey,通过key来做合并val r1 = rdd.r ...