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 {@Overridepublic 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 {@Overridepublic 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/75530.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

论文精度:基于LVNet的高效混合架构:多帧红外小目标检测新突破

论文地址:https://arxiv.org/pdf/2503.02220 目录 一、论文背景与结构 1.1 研究背景 1.2 论文结构 二、核心创新点解读 2.1 三大创新突破 2.2 创新结构原理 2.2.1 多尺度CNN前端 2.2.2 视频Transformer设计 三、代码复现指南 3.1 环境配置 3.2 数据集准备 3.3 训…

解决 Ubuntu 上 Docker 安装与网络问题:从禁用 IPv6 到配置代理

解决 Ubuntu 上 Docker 安装与网络问题的实践笔记 在 Ubuntu&#xff08;Noble 版本&#xff09;上安装 Docker 时&#xff0c;我遇到了两个常见的网络问题&#xff1a;apt-get update 失败和无法拉取 Docker 镜像。通过逐步排查和配置&#xff0c;最终成功运行 docker run he…

指针的进阶2

六、函数指针数组 字符指针数组 - 存放字符指针的数组 char* arr[10] 整型指针数组 - 存放整型指针的数组 int* arr[10] 函数指针数组 - 存放函数指针的数组 void my_strlen() {} int main() {//指针数组char* ch[5];int arr[10] {0};//pa是是数组指针int (*pa)[10] &…

速盾:高防CDN节点对收录有影响吗?

引言 搜索引擎收录是网站运营中至关重要的环节&#xff0c;它直接影响着网站的曝光度和流量。近年来&#xff0c;随着网络安全威胁的增加&#xff0c;许多企业开始采用高防CDN&#xff08;内容分发网络&#xff09;来保护其网站免受DDoS攻击和其他形式的网络攻击。然而&#x…

2025蓝桥杯省赛C/C++研究生组游记

前言 至少半年没写算法题了&#xff0c;手生了不少&#xff0c;由于python写太多导致行末老是忘记打分号&#xff0c;printf老是忘记写f&#xff0c;for和if的括号也老是忘写&#xff0c;差点连&&和||都忘记了。 题目都是回忆版本&#xff0c;可能有不准确的地方。 …

Quill富文本编辑器支持自定义字体(包括新旧两个版本,支持Windings 2字体)

文章目录 1 新版&#xff08;Quill2 以上版本&#xff09;2 旧版&#xff08;Quill1版本&#xff09; 1 新版&#xff08;Quill2 以上版本&#xff09; 注意&#xff1a;新版设置 style"font-family: Wingdings 2" 这种带空格的字体样式会被过滤掉&#xff0c;故需特…

dbt:新一代数据转换工具

dbt&#xff08;Data Build Tool&#xff09;一款专为数据分析和工程师设计的开源工具&#xff0c;专注于 ETL/ELT 流程的数据转换&#xff08;Transform&#xff09;环节&#xff0c;帮助用户以高效、可维护的方式将原始数据转换为适合分析的数据模型。 用户只需要编写查询&am…

【家政平台开发(39)】解锁家政平台测试秘籍:计划与策略全解析

本【家政平台开发】专栏聚焦家政平台从 0 到 1 的全流程打造。从前期需求分析,剖析家政行业现状、挖掘用户需求与梳理功能要点,到系统设计阶段的架构选型、数据库构建,再到开发阶段各模块逐一实现。涵盖移动与 PC 端设计、接口开发及性能优化,测试阶段多维度保障平台质量,…

Java中的Map vs Python字典:核心对比与使用指南

一、核心概念 1. 基本定义 Python字典&#xff08;dict&#xff09; &#xff1a;动态类型键值对集合&#xff0c;语法简洁&#xff0c;支持快速查找。Java Map&#xff1a;接口&#xff0c;常用实现类如 HashMap、LinkedHashMap&#xff0c;需声明键值类型&#xff08;泛型&…

C语言基础之数组

1. 一维数组的创建和初始化 数组的创建 数组是一组相同类型元素的集合。 数组的创建方式&#xff1a; type_t arr_name [const_n]; //type_t 是指数组的元素类型 //const_n是一个常量表达式&#xff0c;用来指定数组的大小 数组创建的实例&#xff1a; //代码1int arr1[10]; …

虚幻引擎5-Unreal Engine笔记之“将MyStudent变量设置为一个BP_Student的实例”这句话如何理解?

虚幻引擎5-Unreal Engine笔记之“将MyStudent变量设置为一个BP_Student的实例”这句话如何理解&#xff1f; code review! 文章目录 虚幻引擎5-Unreal Engine笔记之“将MyStudent变量设置为一个BP_Student的实例”这句话如何理解&#xff1f;理解这句话的关键点1.类&#xff08…

提示词 (Prompt)

引言 在生成式 AI 应用中&#xff0c;Prompt&#xff08;提示&#xff09;是与大型语言模型&#xff08;LLM&#xff09;交互的核心输入格式。Prompt 的设计不仅决定了模型理解任务的准确度&#xff0c;还直接影响生成结果的风格、长度、结构与可控性。随着模型能力和应用场景…

十二、C++速通秘籍—静态库,动态库

上一章节&#xff1a; 十一、C速通秘籍—多线程-CSDN博客https://blog.csdn.net/weixin_36323170/article/details/147055932?spm1001.2014.3001.5502 本章节代码&#xff1a; cpp2/library CuiQingCheng/cppstudy - 码云 - 开源中国https://gitee.com/cuiqingcheng/cppst…

什么是继承?js中有哪儿些继承?

1、什么是继承&#xff1f; 继承是面向对象软件技术中的一个概念。 2、js中有哪儿些继承&#xff1f; js中的继承有ES6的类class的继承、原型链继承、构造函数继承、组合继承、寄生组合继承。 2.1 ES6中类的继承 class Parent {constructor() {this.age 18;} }class Chil…

Linux进程通信入门:匿名管道的原理、实现与应用场景

Linux系列 文章目录 Linux系列前言一、进程通信的目的二、进程通信的原理2.1 进程通信是什么2.2 匿名管道通讯的原理 三、进程通讯的使用总结 前言 Linux进程间同通讯&#xff08;IPC&#xff09;是多个进程之间交换数据和协调行为的重要机制&#xff0c;是我们学习Linux操作系…

探秘Transformer系列之(26)--- KV Cache优化 之 PD分离or合并

探秘Transformer系列之&#xff08;26&#xff09;— KV Cache优化 之 PD分离or合并 文章目录 探秘Transformer系列之&#xff08;26&#xff09;--- KV Cache优化 之 PD分离or合并0x00 概述0x01 背景知识1.1 自回归&迭代1.2 KV Cache 0x02 静态批处理2.1 调度策略2.2 问题…

十大PDF解析工具在不同文档类别中的比较研究

PDF解析对于包括文档分类、信息提取和检索在内的多种自然语言处理任务至关重要&#xff0c;尤其是RAG的背景下。尽管存在各种PDF解析工具&#xff0c;但它们在不同文档类型中的有效性仍缺乏充分研究&#xff0c;尤其是超出学术文档范畴。通过使用DocLayNet数据集&#xff0c;比…

HarmonyOS-ArkUI 装饰器V2 @ObservedV2与@Trace装饰器

参考文档: 文档中心https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V14/arkts-new-observedv2-and-trace-V14#trace%E8%A3%85%E9%A5%B0%E5%AF%B9%E8%B1%A1%E6%95%B0%E7%BB%84由于V2的装饰器比V1的装饰器更加易用,尽管学习的过程中用到的都是V1的装饰器,但…

GPT - GPT(Generative Pre-trained Transformer)模型框架

本节代码主要为实现了一个简化版的 GPT&#xff08;Generative Pre-trained Transformer&#xff09;模型。GPT 是一种基于 Transformer 架构的语言生成模型&#xff0c;主要用于生成自然语言文本。 1. 模型结构 初始化部分 class GPT(nn.Module):def __init__(self, vocab…

基于FPGA的六层电梯智能控制系统 矩阵键盘-数码管 上板仿真均验证通过

基于FPGA的六层电梯智能控制系统 前言一、整体方案二、软件设计总结 前言 本设计基于FPGA实现了一个完整的六层电梯智能控制系统&#xff0c;旨在解决传统电梯控制系统在别墅环境中存在的个性化控制不足、响应速度慢等问题。系统采用Verilog HDL语言编程&#xff0c;基于Cyclo…