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

87 阅读10分钟

一、为什么我要拆这个 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 不一致

平台有效时长共享方式
企微7200sAPP / Agent / Corp 三种 token 类型,互相独立
飞书7200stenant_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 设计,比协作这套更复杂。