Su的技术博客

  • 首页
  • Java
  • MySQL
  • DDD
  • 事故复盘
  • 架构方案
  • Other
  • 工具
  • 打赏
  • 关于
  1. 首页
  2. Java
  3. 正文
                           

【京东】可插拔组件设计机制—SPI

2023-05-03 171点热度 0人点赞 0条评论

一、SPI 是什么

SPI 的全称是Service Provider Interface,即提供服务接口;是一种服务发现机制,SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
如下图:

系统设计的各个抽象,往往有很多不同的实现方案,在面对象设计里,一般推荐模块之间基于接口编程,模块之间不对实现硬编码,一旦代码涉及具体的实现类,就违反了可插拔的原则。Java SPI 就是提供这样的一个机制,为某一个接口寻找服务的实现,有点类似IOC 的思想,把装配的控制权移到程序之外,在模块化涉及里面这个各尤为重要。与其说SPI 是java 提供的一种服务发现机制,倒不如说是一种解耦思想。

二、适用场景

  1. 数据库驱动加载接口实现类的加载;如:JDBC 加载Mysql,Oracle...
  2. 日志门面接口实现类加载,如:SLF4J 对log4j、logback 的支持
  3. Spring中大量使用了SPI,特别是spring-boot 中自动化配置的实现
  4. Dubbo 也是大量使用SPI 的方式实现框架的扩展,它是对原生的SPI 做了封装,允许用户扩展实现Filter 接口。

 

三、使用介绍

要使用 Java SPI,需要遵循以下约定:
  1. 当服务提供者提供了接口的一种具体实现后,需要在JAR 包的META-INF/services 目录下创建一个以“接口全限制定名”为命名的文件,内容为实现类的全限定名;
  2. 接口实现类所在的JAR放在主程序的classpath 下,也就是引入依赖。
  3. 主程序通过java.util.ServiceLoder 动态加载实现模块,它会通过扫描META-INF/services 目录下的文件找到实现类的全限定名,把类加载值JVM,并实例化它;
  4. SPI 的实现类必须携带一个不带参数的构造方法。
示例:

spi-interface 模块定义

  • 定义一组接口:public interface MyDriver 
spi-jd-driver
spi-ali-driver
  • 实现为:
  • public class JdDriver implements MyDriver
  • public class AliDriver implements MyDriver
在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (org.MyDriver 文件)
内容是要应用的实现类分别 com.jd.JdDriver和com.ali.AliDriver

spi-core
一般都是平台提供的核心包,包含加载使用实现类的策略等等,我们这边就简单实现一下逻辑:a.没有找到具体实现抛出异常 b.如果发现多个实现,分别打印
public void invoker(){
    ServiceLoader<MyDriver>  serviceLoader = ServiceLoader.load(MyDriver.class);
    Iterator<MyDriver> drivers = serviceLoader.iterator();
    boolean isNotFound = true;
    while (drivers.hasNext()){
        isNotFound = false;
        drivers.next().load();
    }
    if(isNotFound){
        throw new RuntimeException("一个驱动实现类都不存在");
    }
}
spi-test
public class App 
{
    public static void main( String[] args )
    {
        DriverFactory factory = new DriverFactory();
        factory.invoker();
    }
}
1.引入spi-core 包,执行结果

2.引入spi-core,spi-jd-driver 包

3.引入spi-core,spi-jd-driver,spi-ali-driver

四、原理解析

看看我们刚刚是怎么拿到具体的实现类的?
就两行代码:
ServiceLoader<MyDriver>  serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();
所以,首先我们看ServiceLoader 类:
public final class ServiceLoader<S> implements Iterable<S>{
//配置文件的路径
 private static final String PREFIX = "META-INF/services/";
    // 代表被加载的类或者接口
    private final Class<S> service;
    // 用于定位,加载和实例化providers的类加载器
    private final ClassLoader loader;
    // 创建ServiceLoader时采用的访问控制上下文
    private final AccessControlContext acc;
    // 缓存providers,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 懒查找迭代器,真正加载服务的类
    private LazyIterator lookupIterator;
  
 //服务提供者查找的迭代器
    private class LazyIterator
        implements Iterator<S>
{
 .....
private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
//全限定名:com.xxxx.xxx
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
//通过反射获取
                c = Class.forName(cn, false, loader);
            }
            if (!service.isAssignableFrom(c)) {
                fail(service, "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            }
        }
........
大概的流程就是下面这张图:

  • 应用程序调用ServiceLoader.load 方法
  • 应用程序通过迭代器获取对象实例,会先判断providers对象中是否已经有缓存的示例对象,如果存在直接返回
  • 如果没有存在,执行类转载
    • 将实例化类缓存至providers对象中,同步返回。
    • 并用Instance() 方法示例化类
    • 通过反射方法Class.forName()加载对象
    • 读取META-INF/services 下的配置文件,获取所有能被实例化的类的名称,可以跨越JAR 获取配置文件

五、总结

优点:解耦

SPI 的使用,使得第三方服务模块的装配控制逻辑与调用者的业务代码分离,不会耦合在一起,应用程序可以根据实际业务情况来启用框架扩展和替换框架组件。
SPI 的使用,使得无须通过下面几种方式获取实现类
  • 代码硬编码import 导入
  • 指定类全限定名反射获取,例如JDBC4.0 之前;Class.forName("com.mysql.jdbc.Driver")
缺点:
虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。

六、对比

JDK SPI DUBBO SPI Spring SPI
文件方式 每个扩展点单独一个文件 每个扩展点单独一个文件 所有的扩展点在一个文件
获取某个固定的实现 不支持,只能按顺序获取所有实现 有“别名”的概念,可以通过名称获取扩展点的某个固定实现,配合Dubbo SPI的注解很方便 不支持,只能按顺序获取所有实现。但由于Spring Boot ClassLoader会优先加载用户代码中的文件,所以可以保证用户自定义的spring.factoires文件在第一个,通过获取第一个factory的方式就可以固定获取自定义的扩展
其他 无 支持Dubbo内部的依赖注入,通过目录来区分Dubbo 内置SPI和外部SPI,优先加载内部,保证内部的优先级最高 无
文档完整度 文章 & 三方资料足够丰富 文档 & 三方资料足够丰富 文档不够丰富,但由于功能少,使用非常简单
IDE支持 无 无 IDEA 完美支持,有语法提示

 

-end-

 

本文仅供学习!所有权归属原作者。侵删!文章来源: 京东云开发者

标签: 京东 插件 Java dubbo spi
最后更新:2023-05-03

coder

分享干货文章,学习先进经验。

打赏 点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

最新 热点 推荐
最新 热点 推荐
浅析设计模式1 —— 工厂模式 设计模式在外卖营销业务中的实践 Routing Elasticsearch架构VI:路由 京东平台研发朱志国:领域驱动设计(DDD)理论启示 大家一直在谈的领域驱动设计(DDD),我们在互联网业务系统是这么实践的 基于DDD的微服务设计和开发实战
mysql-connect-java驱动从5.x升级到8.x的CST时区问题系统设计-业务表5要素聊聊spring事务失效的12种场景,太坑了基于DDD的微服务设计和开发实战大家一直在谈的领域驱动设计(DDD),我们在互联网业务系统是这么实践的京东平台研发朱志国:领域驱动设计(DDD)理论启示
2000 字教你画项目架构图(建议收藏) 如何设计一款高性能分布式锁,实现数据的安全访问? 笔记 | JVM内存区域结构:一计两栈一堆一区 聊聊spring事务失效的12种场景,太坑了 可插拔组件设计机制—SPI 你所说的“事件驱动”是什么? What do you mean by “Event-Driven”?

@Autowired (1) @Resource (1) AI (1) cache (1) ChatGPT (1) cola (1) dsl (2) dubbo (1) fsm (1) go (1) GPT (1) IDC (1) JSR (1) Lombok (1) properties (1) spi (1) Spring (2) SQL (1) SQL优化 (1) 中文 (1) 事务 (1) 事务失效 (1) 事务管理 (1) 依赖注入 (1) 分片 (1) 单点 (1) 后端 (5) 容灾 (1) 富途 (1) 布隆过滤器 (1) 异地双活 (1) 得物 (1) 接口优化 (2) 故障转移 (1) 数据一致性 (1) 整洁架构 (1) 时区 (1) 最佳实践 (1) 状态机 (2) 系统容灾 (1) 系统设计 (4) 缓存击穿 (1) 编码规范 (1) 编程 (1) 网关 (1) 美团 (2) 路由 (1) 配置文件 (1) 限流 (1) 高可用 (1)

COPYRIGHT © 2014-2023 verysu.com . ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

粤ICP备15033072号-2