一个开源低代码框架的协作 SPI 是怎么设计的 ForgeAdmin 拆解 + 实战接入新平台

一、为什么我要拆这个 SPI

去年我们公司做企业协同整合,对接了 4 个平台 :企业微信、钉钉、飞书、自研 IM。4 套认证、4 套回调、4 套用户体系,每接一个就要改 20 多个文件 ,核心业务层多了 4 个 if/else if/else 分支。

接手那个项目时我盯着那堆 switch (platform) 代码看了 3 天,决定做一次彻底的重构。

这次重构让我把 forge-starter-collaboration 的 SPI 设计读了 6 个周末,真的可以用"漂亮"来形容 。今天把这套设计拆给你看------再教你如何自己接入一个新平台(我用飞书举例),可以当成 SPI 落地实战模板用。

注:本文所有源码均来自开源项目 ForgeAdmin(Gitee: ForgeLab/forge-admin),非商业定制。


二、SPI 解耦的 3 个核心目标

在拆代码前,先明确一个好的"协作平台 SPI"应该满足什么------

目标 含义 失败的反面教材
平台无关的抽象 把"用户""组织""消息""待办""回调"等概念抽象成统一模型 各平台模型名都不同(WecomUser vs DingTalkUser vs FeishuUser)
编排层不出现平台分支 调用方只跟"能力"打交道,看不见具体平台 switch (platform) / if (platform.equals("wecom"))
开闭原则:新增不改核心 接新平台只新增、不修改 改一个 if 分支,所有业务都要回归

接下来你会看到 ForgeAdmin 是怎么把这 3 个目标一一落地的


三、ForgeAdmin 协作 SPI 全景图

下面是完整的 SPI 分层结构(建议保存):

复制代码

核心就两个模块:

  • forge-starter-collaboration:抽象层 + 注册中心 + 能力 SPI 接口
  • forge-plugin-collaboration:平台实现(目前已有 wecom,feishu/dingtalk 是新接入的位置)

四、最核心的 3 个接口

4.1 CollaborationProvider ------ 平台元数据

这是入口:每个平台注册一个 Provider,告诉框架"我是谁、我能干哪些事"。

复制代码

4.2 CollaborationCapability ------ 5 种能力枚举

复制代码

为什么只有 5 种?

这是这套 SPI 最克制的地方------它只抽象业务高频需要的能力(登录、同步、消息、待办、回调)。其他不通用的高级能力(如企微的"审批模板"、飞书的"文档协同")不进 SPI,留在具体 Connector 内部。这样保 SPI 的"长期稳定",避免经常增减枚举。

4.3 CollaborationConnector ------ 能力承载基础接口

复制代码

每个能力类型都有自己的"专项接口",基础接口只是"声明自己承担哪个能力"------

复制代码

五、真正的"主角":注册中心

看完上面 3 个接口,你会发现它们都很薄------真正精彩的是 CollaborationProviderRegistry 这个注册中心

5.1 它做了什么?

它把"声明"和"实现"绑死,并构造期验证。下面是它的核心契约:

构造期完成全部校验并失败关闭:

  • 同平台注册多个 Provider 直接抛错
  • 同平台同能力注册多个 Connector 直接抛错
  • Connector 所属平台无 Provider 或能力未声明直接抛错
  • 编排层通过 requireConnector 获取能力实现,不出现平台 switch 分支

5.2 完整源码拆解

我把关键代码贴出来(去掉了判空和日志):

复制代码

5.3 这段代码解决的 3 个问题

问题 1:声明与实现分离,容易"漏实现"

很多人写 SPI 习惯这样:

复制代码

然后业务方调用 paymentProvider.pay()实现者忘了实现 pay() 时编译器不会报错 ------你部署到线上才发现 NullPointerException

ForgeAdmin 的解法是构造期双向校验

  • Provider 不声明能力 → 启动直接失败
  • Provider 声明的能力没有 Connector → 启动直接失败
  • Connector 注册的能力 Provider 没声明 → 启动直接失败

应用启动失败永远好过线上崩溃

问题 2:编排层要写平台分支

调用方如果写:

复制代码

每次新接一个平台就要补 else if,漏一个就是 bug。ForgeAdmin 强迫你写:

复制代码

只有"能力"两个字的差异,没有平台分支。新接平台,业务代码不动。

问题 3:同能力多实现导致歧义

如果不约束"同平台同能力唯一",你可能注册两个 WecomLoginConnector(一个新版本一个老版本),框架不知道该调用哪个。putIfAbsent + 抛错强约束唯一性。


六、模型层 - 跨平台抽象协议

SPI 的另一面是协议数据模型。下面这几个最关键:

模型 作用
ExternalUser 跨平台用户抽象(含平台编码、平台内 id、手机号、邮箱、组织归属)
DirectorySnapshot 一次目录同步的快照(含部门、用户、标签三个集合)
DirectorySyncScope 同步范围(按部门/按时间窗/全量)
ProviderMessageRequest 跨平台消息投递请求(标题/正文/链接/按钮)
VerifiedSocialIdentity 登录后验证过的身份(含平台 openId / unionId / mobile)
ProviderError 平台错误分类(鉴权/参数/权限/限流/系统),业务层用它决定重试策略
CollaborationExecutionContext 调用上下文(含 enterpriseId、agentId、超时参数)

举例 ExternalUser

复制代码

各平台 User 模型都不一样(WecomUser vs FeishuUser,字段命名也不同),但同步回来都翻译成 ExternalUser,业务层只认它


七、真实实现拆解 - 企业微信 Provider

看完抽象层,我们看真实实现。这是 ForgeAdmin 内置的 forge-plugin-collaboration 模块:

复制代码

7.1 元数据声明

复制代码

注意:TODO 没有声明 。源码注释里写了二期再加入------这就是 Provider 的精髓:能力按需声明,未实现就先不写

7.2 登录 Connector

复制代码

关键点

  • 参数验证在前(enterpriseId/agentId/authCode 都不可空)
  • 用 Builder 模式构建请求,统一管理 tokenType/path/queryParams
  • AccessTokenProvider.TokenType.APP 是策略模式------APP/Agent/Corp 各有自己的令牌源
  • 错误明确:外部用户登录场景明确失败关闭

业务层调用:

复制代码

八、实战 - 接入飞书

假设产品要新支持飞书。和接企微比起来,简单到你想不到

8.1 准备 - 项目结构

复制代码

8.2 Step 1 - 写 FeiShuProvider 元数据

复制代码

8.3 Step 2 - 写 FeiShuLoginConnector

复制代码

8.4 Step 3 - 业务层加一个 platform 路由(仅一处!)

复制代码

到此为止,业务层完工了 。新平台接进来,就只动了 4 行代码 (这个 controller 的 platform 参数化)。其他业务模块(组织同步、消息发送、待办卡片、回调处理)因为编排层完全 SPI 化,自动支持飞书

8.5 Step 4 - 验证启动时不报错

把你的新模块加到 admin-server 的依赖里,启动项目,日志里你会看到:

复制代码

如果声明与实现对不上(比如 Provider 声明 LOGIN 但没写 LoginConnector),Spring 上下文直接失败启动------这正是你想要的效果。

8.6 还可以更进一步

如果某个能力的飞书实现还不够(比如飞书特有的"群卡片"),就在 FeiShuMessageConnector 内部扩展平台专有方法,SPI 公开的是 send(ProviderMessageRequest) 一个方法 ,但实现类内部可以加自己的 sendToChatGroup(...) 方法。框架不限制 platform-specific 扩展。


九、SPI 设计的几个大坑(也顺便谈谈企微实现的细节)

这一段是经验之谈。如果你正在写 SPI,下面这几个坑几乎都会遇到:

坑 1:access_token 不一致

平台 有效时长 共享方式
企微 7200s APP / Agent / Corp 三种 token 类型,互相独立
飞书 7200s tenant_access_token / user_access_token
钉钉 7200s(旧版) corp_secret / app_secret

ForgeAdmin 的解法是 AccessTokenProvider.TokenType 枚举 + 自适应定时刷新wecomAPP/Agent/Corpfeishutenant/app,每个实现内部做缓存和续期。

坑点:写 SPI 时一定要保留"token 类型"这一维度,不然 4 个月后改飞书会动到老逻辑。

坑 2:回调验签 - 各家都不一样

平台 验签方式
企微 URL 参数 msg_signature + AES-256-CBC 加解密(用 EncodingAESKey)
飞书 HMAC-SHA256(Encrypt Key)
钉钉 HMAC-SHA256(AppSecret)

ForgeAdmin 的解法是 CallbackConnector.parseEvent(headers, body) 接口拿到原始回调验签和加解密由各平台 Connector 内部处理------SPI 不定义"统一验签模型",因为真统一不了。

坑点:不要在 SPI 层定义"验签"或"回调解密"------这会把不同平台的差异强行对外暴露。验签细节完全留在 Connector 内部。

坑 3:幂等性 - 同一事件投 2 次

IM 平台经常因网络问题重试回调,业务上同一事件可能收到 2 次。ForgeAdmin 的解法:

  • CollaborationTaskEvent 模型里强制带上 eventId:每条事件唯一
  • 回调处理入表前先 INSERT IGNORE:依赖 DB 唯一索引
  • 不要用"业务对象 hash"做幂等 key:组合字段变化会让你踩坑

坑点:写 SPI 时把"幂等契约"放在 SPI 抽象层而不是具体实现层,因为这是业务需求不是平台特性。

坑 4:错误分类错误会引发雪崩

复制代码

ForgeAdmin 提供了 ProviderError 模型和 WeComErrorClassifier 这种错误分类器------分类决定了"要不要重试、要不要刷新 token、要不要人工介入"。不分类直接重试,是 IM 集成的最大隐患。


十、SPI 设计的 3 个我学到的"诀窍"

看完 ForgeAdmin 的协作 SPI,我提炼了 3 个值得带回到自己项目的诀窍:

诀窍 1:能力枚举别膨胀

如果你做个"AI Agent SPI"恨不得 30 个能力枚举------收敛到业务高频 3-7 个。能力枚举一变,所有 Provider 都要改。常见错误是把"审批/工作流/计算/分析"全列进去。

诀窍 2:fail-fast 在构造期而不是使用期

很多项目写 SPI 都是"等到第一次调用才发现有问题"。ForgeAdmin 把校验提前到 Spring 上下文初始化 ------你的项目也要这样做。能用构造函数抛错解决的,别留到运行时

诀窍 3:编排层只接能力,不接平台

复制代码

这条规则能强制架构师在 SPI 设计时思考:"平台差异到底在哪一层吸收?"------如果业务层还有分支,说明 SPI 不够抽象。


十一、总结

回顾一下 ForgeAdmin 协作 SPI 的设计精髓:

维度 关键设计 给你的启示
抽象粒度 5 个能力枚举,只抽象业务高频部分 别把 SPI 设计成"万能借口"
角色分层 Provider(声明)/ Connector(实现)/ Registry(调度) 每个角色单一职责
校验时机 构造期 fail-fast,启动失败好过线上崩溃 不要把"配置错"拖到运行时
数据模型 协议化建模(ExternalUser 等 13 个 model) 业务只见"跨平台模型",不见具体平台
新平台接入 加模块 + Spring 自动扫描 注册即用,业务零改动

最重要的 3 句话

  1. 平台无关的抽象 = 业务只见能力,不见平台
  1. 注册中心 = 声明与实现绑死,启动时验证
  1. 模型层 = 跨平台协议,所有原始字段翻译成协议字段

这套设计在任何需要"接入多个外部能力"的项目里都适用------支付通道、消息通道、AI Agent 工具调用网关......不局限于企业协同


十二、参考资料 & 扩展阅读

类别 地址
项目仓库 gitee.com/ForgeLab/fo...
GitHub 镜像 github.com/yaomindong1...
在线文档 www.dlforgelab.com:8084/forge-docs/
在线演示(admin/123456) www.dlforgelab.com:8084/forge/login
关键源码 forge-server/forge-framework/forge-starter-parent/forge-starter-collaboration/
企微实现参考 forge-server/forge-framework/forge-plugin-parent/forge-plugin-collaboration/provider/wecom/
相关规范 AGENTS.md(项目级 AI 编码指引)/ code-copilot/rules/conventions.md

写在最后

如果你也想给自己的项目做一套"SPI 抽象"------先回答 3 个问题:

  1. 我有多少"我方不知道调哪个外部"的能力? (多就是有 SPI 价值)
  1. 这些能力的差异能被几个稳定的枚举吸收吗? (能就能写 SPI)
  1. 业务层是真的需要看平台差异,还是平台差异只是噪音? (后者就该 SPI 化)

如果 3 个回答都是肯定的------你来读 ForgeAdmin 这套 SPI 会很有共鸣。

如果仅 1-2 个是肯定的------先把它当"普通接口开发"做,等真的接多个平台那天再抽象。

过早抽象和没有抽象同样危险


如果这篇文章帮你理解了 SPI 拆解,记得点赞 + 收藏 ❤️ 评论留下你的 SPI 设计心得,你踩过什么坑? 下一篇文章我会拆 ForgeAdmin 的能力开放网关------双出口(REST 网关 + MCP Tool)的 SPI 设计,比协作这套更复杂。

相关推荐
蓝山61644 分钟前
Python 字典(Dictionary)完全指南:从入门到实战
后端
foggyprojects1 小时前
在 DeepSeek Harness 里跑通 Foggy:从安装插件到第一次问数
后端
不甘先生1 小时前
Go 中 type、方法与指针接收者:从 str_name.Name() 看懂 Go 的类型系统
开发语言·后端·golang
OPEN-F1 小时前
C++20新特性精讲:概念、范围与三路比较
算法·c++20
智碳能碳管理平台1 小时前
企业能碳管理系统的月度锁账、防篡改与留痕怎么架构
架构·github·能碳管理系统·智碳能碳管理平台·企业能碳管理系统·绿色工厂申报saas·能碳管理平台
@MMiL2 小时前
ST-link与J-link通俗易懂讲解
算法
RAOY的AI笔记2 小时前
GPT-6打破孪生素数猜想最新纪录:AI正在进入数学研究新时代?
人工智能·gpt·算法
Nil2082 小时前
leetcode 994腐烂的橘子
算法·leetcode·职场和发展
闻缺陷则喜何志丹2 小时前
【动态规划】P10726 [GESP202406 八级] 空间跳跃|普及+
c++·算法·动态规划·洛谷