当前位置: 首页 > news >正文

Sa-Token 自定义插件 —— SPI 机制讲解(一)

前言


博主在使用 Sa-Token 框架的过程中,越用越感叹框架设计的精妙。于是,最近在学习如何给 Sa-Token 贡献自定义框架。为 Sa-Token 的开源尽一份微不足道的力量。我将分三篇文章从 0 到 1 讲解如何为 Sa-Token 自定义一个插件,这一集将是前沿知识 —— SPI

那为什么要学 SPI 呢?[Sa-Token 官方描述](https://gitee.com/sa-tokens/sa-token-three-plugin/blob/master/README_PR_STEP.md)
由此可见,Sa-Token 的第三方插件是基于 SPI 机制实现的装配,我们要知其然且知其所以然,不仅要学会开发插件还要学会大佬们的设计思路

废话不多说,现在正式开始!

1. 什么是 SPI?

SPI全称是Service Provider Interface服务提供者接口),是一种 "插件化" 架构思想

它是一种服务发现机制,它允许第三方提供者为核心库或主框架提供实现或扩展。这种设计允许核心库/主框架在不修改自身代码的情况下,通过第三方实现来增强现有功能

举个例子

SPI 机制就像 USB 接口:

  1. 由你定义 USB 接口规范,规定这个 USB 接口需要做什么(SPI)
  2. 不同的 USB 厂商按照规范做 U 盘/鼠标/键盘(不同实现)
  3. 将 USB 接口插上电脑就能使用(自动加载)

在 JDK 中提供了原生的 SPI,在 Spring 框架中也有一套自己的 SPI 机制。下面,我将分别给大家介绍下这两套 SPI 机制

  1. JDK 原生的 SPI
  • 定义和发现JDKSPI主要通过在META-INF/services/目录下放置特定的文件来指定哪些类实现了给定的服务接口。这些文件的名称要命名为接口的全限定名,内容为实现该接口的全限定类名
  • 加载机制ServiceLoader类使用Java的类加载器机制从META-INF/services/目录下加载和实例化服务提供者。例如,ServiceLoader.load(MyServiceInterface.class)会返回一个实现了MyServiceInterface的实例迭代器
  • 缺点JDK原生的SPI每次通过ServiceLoader加载时都会初始化一个新的实例,没有实现类的缓存,也没有考虑单例等高级功能

  1. Spring 框架的 SPI
  • 更加灵活SpringSPI不仅仅是服务发现,它提供了一套完整的插件机制。例如,可以为Spring定义新的PropertySourceApplicationContextInitializer
  • 与 IoC 集成:与JDKSPI不同,SpringSPI与其IoC (Inversion of Control) 容器集成,使得在SPI实现中可以利用Spring的全部功能,如依赖注入
  • 条件匹配Spring提供了基于条件的匹配机制,这允许在某些条件下只加载特定的SPI实现,例如,可以基于当前运行环境的不同来选择加载哪个数据库驱动
  • 配置Spring允许通过spring.factories文件在META-INF目录下进行配置,这与JDKSPI很相似,但它提供了更多的功能和灵活性

2. 为什么需要 SPI?

上节介绍了 SPI 机制,那我们为什么需要 SPI 机制呢?

假设有如下需求:电商平台现在要集成支付功能(支付宝、微信支付、银联),但未来可能会扩充新的支付方式

第一种实现方式

大家想到的第一种实现方式是什么?是不是使用一个枚举类来维护支付类型,在具体的代码中根据不同的支付类型调用不同的逻辑呢?

// 支付方式枚举
public enum PaymentType {
    ALIPAY,
    WECHAT_PAY,
    UNION_PAY
}

// 支付服务类(紧耦合)
public class PaymentService {
    public void pay(String orderId, PaymentType type) {
        switch (type) {
            case ALIPAY:
                new AlipayService().pay(orderId);
                break;
            case WECHAT_PAY:
                new WechatPayService().pay(orderId);
                break;
            case UNION_PAY:
                new UnionPayService().pay(orderId); // 新增支付方式需要修改这里
                break;
            default:
                throw new IllegalArgumentException("不支持的支付方式");
        }
    }
}

// 使用示例
public class OrderController {
    public void createOrder() {
        PaymentService paymentService = new PaymentService();
        paymentService.pay("ORDER_456", PaymentType.ALIPAY);
    }
}

这种方式没有使用 SPI 机制,那大家思考下这样的实现真的合适吗?

弊端:

  1. 违反开闭原则:每新增一种支付方式都要修改PaymentService
  2. 循环依赖风险:支付服务类需要知道所有具体实现
  3. 编译期依赖:必须提前引入所有支付 SDK 的 jar 包
  4. 测试困难:无法单独测试某个支付方式的实现

第二种实现方式

使用 SPI 机制解耦实现

// 1. 定义SPI接口(与方案1相同)
public interface PaymentService {
    void pay(String orderId);
}

// 2. 各支付实现类(与方案1相同)
public class AlipayService implements PaymentService { /*...*/ }
public class WechatPayService implements PaymentService { /*...*/ }

// 3. 注册服务提供者(新增支付方式只需添加文件)
// META-INF/services/com.example.PaymentService
// 文件内容:
// com.example.AlipayService
// com.example.WechatPayService

// 4. 动态加载服务(核心优势)
public class PaymentGateway {
    public void processPayment(String orderId) {
        ServiceLoader<PaymentService> services = ServiceLoader.load(PaymentService.class);
        
        // 自动发现所有支付方式
        for (PaymentService service : services) {
            service.pay(orderId);
        }
    }
}

// 使用示例(完全解耦)
public class OrderController {
    public void createOrder() {
        PaymentGateway gateway = new PaymentGateway();
        gateway.processPayment("ORDER_456");
    }
}

大家思考下,这样实现的优势在哪?

优势:

  1. 开闭原则:新增支付方式只需添加实现类 + 注册文件,无需修改已有代码
  2. 运行时发现:通过ServiceLoader动态加载所有实现
  3. 模块化部署:每个支付渠道可以独立打包为 jar,按需加载
  4. 热插拔:可通过类加载器实现运行时替换实现(高级用法)

通过上面的真实案例,相信大家能够很明显的感受到 SPI 机制的优点。但需要注意的是,没有任何一种完美的机制,一切都要以自己公司的需求为主。不要为了用而用!

SPI 机制实现的代码由于涉及到动态加载,所以性能是比不过硬编码这种方式,给出证据:

方案

平均耗时

内存占用

启动速度

硬编码实现

28ms

45MB

1.2s

SPI动态加载

35ms

48MB

1.5s


3. SPI 在 JDK 中的应用示例

Java的生态系统中,SPI 是一个核心概念,允许开发者提供扩展和替代的实现,而核心库或应用不必更改。下面,我将通过代码来说明

实现步骤:

  1. 创建一个 SpringBoot 项目(省略)
  2. 定义一个服务接口
/**
 * @Description SPI接口 —— 支付服务
 * @Author Mr.Zhang
 * @Date 2025/4/12 20:36
 * @Version 1.0
 */
public interface PaymentService {

    /**
     * 支付,具体实现由实现类实现
     *
     * @param orderId 订单号
     */
    void pay(String orderId);
}

  1. 根据不同支付厂商定义不同实现类,为服务接口提供具体实现
/**
 * @Description 微信支付实现类
 * @Author Mr.Zhang
 * @Date 2025/4/12 20:40
 * @Version 1.0
 */
public class WechatServiceImpl implements PaymentService {

    @Override
    public void pay(String orderId) {
        System.out.println("微信支付");
    }
}
/**
 * @Description 支付宝支付服务实现类
 * @Author Mr.Zhang
 * @Date 2025/4/12 20:38
 * @Version 1.0
 */
public class AlipayServiceImpl implements PaymentService {

    @Override
    public void pay(String orderId) {
        System.out.println("支付宝支付");
    }
}

  1. 注册服务提供者

在资源目录(通常是src/main/resources/)下创建一个名为META-INF/services/的文件夹。在这个文件夹中,创建一个名为com.zhang.spijdkdemo.service.PaymentService的文件(这是我们接口的全限定名),这个文件没有任何文件扩展名,所以不要加上.txt这样的后缀!文件的内容应为我们所有实现类的全限定名,每个类路径占一行

注意:

  • META-INF/services/Java SPI机制中约定俗成的特定目录!!它不是随意选择的,而是SPI规范中明确定义的。因此,在使用JDKServiceLoader类来加载服务提供者时,它会特意去查找这个路径下的文件
  • 请确保文件的每一行只有一个名称,并且没有额外的空格或隐藏的字符,文件使用UTF-8编码。

  1. 在程序启动时使用ServiceLoader.load()加载和使用服务
public class SpiJdkDemoApplication {
    
    public static void main(String[] args) {
        // load() 方法 会自动加载 META-INF/services/com.zhang.spijdkdemo.service.PaymentService 文件
        ServiceLoader<PaymentService> loaders = ServiceLoader.load(PaymentService.class);

        for (PaymentService loader : loaders) {
            loader.pay("281729172817");
        }
    }
    
}

运行结果如下:

Alipay finish... >281729172817
Wechat pay finish... >281729172817

相关文章:

  • vue3 异步组件的使用
  • 局域网下ESP32-S3 LED灯的UDP控制
  • 【leetcode hot 100 416】分割等和子集
  • MCU刷写——Hex文件格式详解及Python代码
  • AI识别与雾炮联动:工地尘雾治理新途径
  • Win32++ 使用初探
  • 程序化广告行业(79/89):技术革新与行业发展脉络梳理
  • 公开赛Web-ssrfme
  • 【异常处理】Clion IDE中cmake时头文件找不到 头文件飘红
  • 解决2080Ti使用节点ComfyUI-PuLID-Flux-Enhanced中遇到的问题
  • lvs+keepalived+dns高可用
  • 使用nuxt3+tailwindcss4+@nuxt/content3在页面渲染 markdown 文档
  • 红宝书第四十讲:React 核心概念:组件化 虚拟 DOM 简单教程
  • forms+windows添加激活水印
  • 塔能科技解节能密码,工厂成本“效益方程式”精准破题
  • AF3 ProteinDataset类的_process方法解读
  • 操作系统之进程同步
  • python的flask框架连接数据库
  • 区块链从专家到小白
  • GAS:车载体验的智能革新力量
  • wordpress 自定义搜索/百度首页优化排名
  • 一 一个甜品网站建设目标/合肥网络seo推广服务
  • 新网站为什么做的这么难/seo优化工具推荐
  • 天津疫情最新通报/廊坊百度快照优化排名
  • 英文网站首页优化/网站排行榜前十名
  • 天天联盟广告网站如何做/营销网络推广