Su的技术博客

  • 首页
  • 原创
  • 视频
  • Java
  • MySQL
  • DDD
  • 事故复盘
  • 架构方案
  • AI
  • Other
  • 工具
    • AI工具集
    • 工具清单
    • JSON在线格式化
    • JSON在线比较
    • SQL在线格式化
  • 打赏
  • 关于
路很长,又很短
  1. 首页
  2. Java
  3. 正文
                           

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

2023-05-03 2148点热度 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-

 

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

更多文章:

  1. 超实用的IDEA插件推荐!百万级下载量
  2. 接口优化的常见方案实战总结
  3. Lombok:神奇的Java插件!
  4. 从代码到设计的性能优化指南
  5. Lombok 同时使用 @Data 和 @Builder 的巨坑,千万别乱用!
  6. 单体分层应用架构剖析
  7. log4j2同步日志引发的性能问题
  8. 为啥不建议用BeanUtils.copyProperties拷贝数据
  9. 高效开发与设计:提效Spring应用的运行效率和生产力
  10. 全链路压测之影子库及ShardingSphere实现影子库源码剖析
标签: 京东 插件 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
取消回复

广告
文章目录
  • 一、SPI 是什么
  • 二、适用场景
  • 三、使用介绍
  • 四、原理解析
  • 五、总结
  • 六、对比
最新 热点 推荐
最新 热点 推荐
干货 | 论Elasticsearch数据建模的重要性 马蜂窝消息总线——面向业务的消息服务设计 基于 MySQL Binlog 实现可配置的异构数据同步 视频笔记:Google发布Agent2Agent协议 视频笔记:什么是微服务,为什么是微服务? 视频笔记:什么是AI 智能体? 视频笔记:什么是Flink? 如何秒级实现接口间“幂等”补偿:一款轻量级仿幂等数据校正处理辅助工具
Elasticsearch 使用误区之六——富文本内容写入前不清洗基于 MySQL Binlog 实现可配置的异构数据同步马蜂窝消息总线——面向业务的消息服务设计干货 | 论Elasticsearch数据建模的重要性你可以不用RxJava,但必须得领悟它的思想!如何秒级实现接口间“幂等”补偿:一款轻量级仿幂等数据校正处理辅助工具视频笔记:什么是Flink?视频笔记:什么是AI 智能体?
一次 Redis 事务使用不当引发的生产事故 理解领域驱动设计DDD 预测 2025 AI Spring 七种事务传播性介绍 云音乐贵州机房迁移总体方案回顾 Elasticsearch 使用误区之五——单次请求获取大量数据 DDD系列第五讲:聊聊如何避免写流水账代码 分布式事务场景、概念和方案整理(含概念图)

CRUD (1) Event Sourcing (1) graphql (1) id (1) NoSQL (1) quarkus (1) rest (1) RocketMQ (2) Spring Boot (1) zk (1) zookeeper (1) 上下文 (1) 事务消息 (1) 二级缓存 (1) 值对象 (1) 关系数据库 (1) 分布式缓存 (1) 原子性 (1) 唯一ID (1) 商品 (1) 多对多 (1) 子域 (1) 字符集 (1) 客户端心跳 (1) 幂等 (2) 干货 (1) 并发 (1) 应用场景 (1) 应用架构图 (1) 康威定律 (2) 异步复制 (1) 微服务架构 (2) 总体方案 (1) 技术方案 (2) 技术架构 (2) 技术架构图 (1) 技能 (1) 持续集成 (1) 支撑域 (1) 故障恢复 (1) 数据架构图 (1) 方案选型 (1) 日记 (1) 服务发现 (1) 服务治理 (1) 服务注册 (2) 机房 (1) 核心域 (1) 泄漏 (1) 洋葱架构 (1) 消息队列 (5) 源码剖析 (1) 灰度发布 (1) 熔断 (1) 生态 (1) 画图工具 (1) 研发团队 (1) 线程 (2) 组织架构 (1) 缓存架构 (1) 编码 (1) 视频 (18) 读写分离 (1) 贵州 (1) 软件设计 (1) 迁移 (1) 通用域 (1) 集群化 (1) 雪花算法 (1) 顺序消息 (1)

推荐链接🔗
  • AI工具集
  • 工具箱🛠️

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

Theme Kratos Made By Seaton Jiang

粤ICP备15033072号-2

x