一、为什么我要拆这个 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 枚举 + 自适应定时刷新 :wecom 用 APP/Agent/Corp,feishu 用 tenant/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 句话:
- 平台无关的抽象 = 业务只见能力,不见平台
- 注册中心 = 声明与实现绑死,启动时验证
- 模型层 = 跨平台协议,所有原始字段翻译成协议字段
这套设计在任何需要"接入多个外部能力"的项目里都适用------支付通道、消息通道、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 个问题:
- 我有多少"我方不知道调哪个外部"的能力? (多就是有 SPI 价值)
- 这些能力的差异能被几个稳定的枚举吸收吗? (能就能写 SPI)
- 业务层是真的需要看平台差异,还是平台差异只是噪音? (后者就该 SPI 化)
如果 3 个回答都是肯定的------你来读 ForgeAdmin 这套 SPI 会很有共鸣。
如果仅 1-2 个是肯定的------先把它当"普通接口开发"做,等真的接多个平台那天再抽象。
过早抽象和没有抽象同样危险。
如果这篇文章帮你理解了 SPI 拆解,记得点赞 + 收藏 ❤️ 评论留下你的 SPI 设计心得,你踩过什么坑? 下一篇文章我会拆 ForgeAdmin 的能力开放网关------双出口(REST 网关 + MCP Tool)的 SPI 设计,比协作这套更复杂。