mqtt-plus 架构解析(十):从内部项目到开源框架,mqtt-plus 的抽取过程与决策
摘要
很多框架的开源并不是"把内部代码推到 GitHub"这么简单。真正困难的地方在于:哪些能力是通用框架能力,哪些仍然带着业务包袱;哪些 API 在内部能凑合用,到了开源之后却必须重新定义;哪些范围应该先做稳,哪些需求应该刻意延后。mqtt-plus 的形成就是这样一次抽取与重设边界的过程。本文会基于整个系列前 9 篇已经拆开的模块与实现,回到更高层的问题:mqtt-plus 为什么能从 drone-framework 里的 MQTT 能力沉淀成一个独立开源框架,以及这一路上做了哪些关键取舍。
项目地址
项目地址:
https://github.com/mqttplus/mqtt-plus
配套的示例工程:
https://github.com/mqttplus/mqtt-plus-examples
如果你对这个方向感兴趣,欢迎关注、试用,也欢迎一起交流 issue 和 PR。
如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎给项目一个 Star。
到了第 10 篇,系列终于回到了起点。
前面 9 篇其实一直在做同一件事:
- 把 mqtt-plus 现在的结构拆开
- 把每个模块边界讲清楚
- 把每一处实现取舍落到真实源码上
但这些文章还有一个更大的隐含问题:
- 为什么 mqtt-plus 会长成现在这样?
- 为什么它不是一个"顺手做出来的工具包",而是一组边界比较清楚的模块?
- 为什么它的很多实现看起来都偏克制,像是刻意砍掉了一些"也许能做"的能力?
答案其实藏在它的来源里:
mqtt-plus 不是从空白画布上设计出来的理想框架,而是从内部项目里的重复能力、混杂边界和长期维护痛点里,一步步抽出来的。
这也决定了第 10 篇应该讲的,不是"起源故事",而是抽取方法本身。
一、这篇文章到底想回答什么?
这一篇只回答三个问题:
- 哪些能力应该从内部项目里抽出来,成为通用框架能力
- 哪些业务包袱必须留在原项目里,而不能带进开源框架
- mqtt-plus 为什么会选择今天这种边界清楚、范围克制、模块分层明确的形态
如果只记住一句话,那就是:
mqtt-plus 的开源,不是把内部代码复制出来,而是先把"什么属于框架、什么属于业务"这条边界重新画了一遍。
二、先看从内部项目到开源框架的演进图
先把整个演进过程压成一张图。
drone-framework: 4 MQTT-related modules
Repeated connection / listener / bridge logic
Identify reusable framework concerns
Separate business logic from framework logic
Redesign stable public abstractions
Split into layered modules
mqtt-plus open-source framework
这张图里最重要的,不是"最后开源了",而是中间那三步:
- 识别可复用能力
- 把业务逻辑和框架逻辑切开
- 重新设计稳定抽象
如果少了任何一步,最终都很容易变成:
- 不是框架,而是一份"脱敏过的内部代码"
- 看起来能复用,实际上边界还是内聚在原业务模型上
- 开源之后 API 很快失稳,因为它本来就不是为外部用户设计的
这也是 mqtt-plus 现在很多"克制感"的真正来源。
三、第一步不是开源,而是先识别"什么算框架能力"
从第 1 篇开始我们就反复提到,mqtt-plus 的前身来自 drone-framework 里 4 个 MQTT 相关模块。
那一步真正暴露出来的问题,不是"模块数量多",而是:
- 连接能力在重复
- listener 注册和调用逻辑在重复
- 订阅恢复逻辑在重复
- 错误处理、转换链和 broker 管理都开始散落在不同模块里
一旦系统走到这里,就会出现一个很典型的信号:
这些东西已经不再是"某个业务场景的实现细节",而是开始表现出框架级共性。
也就是说,真正值得抽出来的,不是"某个设备模型"或者"某个领域 topic 规范",而是这些在不同模块里都要重复回答的问题:
- broker 连接如何抽象
- listener 如何注册和路由
- payload 如何转换
- 连接恢复和动态订阅如何协调
- Spring 如何与 core 能力粘合
这一层识别非常关键,因为很多内部项目的开源失败,不是因为代码差,而是因为第一步就抽错了:
- 把业务场景里的偶然性误当成了框架能力
- 把内部项目里临时可用的 API 误当成了通用抽象
mqtt-plus 现在能维持比较稳定的模块边界,本质上就是因为第一步抽出来的,基本都是"跨场景重复成立"的问题。
四、第二步更难:哪些东西必须留在原项目里?
一个内部项目要开源,最难的地方往往不是"什么能抽出来",而是:
什么不能带出来。
如果把这一步做不好,开源项目很容易出现两种问题:
- 明明是框架,却带着大量业务命名、业务默认值和场景耦合
- 明明想做通用能力,却不断为原项目的历史兼容包袱服务
mqtt-plus 现在的边界其实已经很能说明这个取舍。
比如当前仓库里被明确放进"当前范围之外"的就有:
mqtt-plus-hivemq- MQTT 5.0 支持
- 运行时动态修改 broker 连接信息
这些能力不是不重要,而是它们要么还没有达到稳定抽象状态,要么还不适合进入当前这一版框架的公共承诺。
同样,像设备领域对象、行业 topic 规范、无人机场景里的专属桥接逻辑,也没有跟着一起进入开源框架。
这说明 mqtt-plus 在抽取时其实做了一个很重要的动作:
- 把"框架共性"抽出来
- 把"原项目负担"留在原项目里
Yes
No
Yes
No
Existing internal MQTT code
Business-specific?
Keep in original project
Reusable framework concern?
Extract into mqtt-plus
Discard or redesign
这张图的价值在于,它说明了一个很现实的开源原则:
不是所有内部代码都值得开源;很多代码更适合被留在原项目里,或者干脆在抽取时重写。
设计决策: mqtt-plus 没有试图把内部项目里所有 MQTT 相关代码一并带出来,而是刻意把业务模型、场景特定桥接逻辑和尚未稳定的能力留在原项目或延后范围之外。这样做的重点,是让开源框架只对真正稳定、可复用的能力负责。
五、第三步:开源不是复制代码,而是重新设计公共抽象
真正决定 mqtt-plus 能不能成立的,其实不是"有没有代码",而是:
- 有没有重新设计可以对外承诺的抽象
- 有没有把内部实现里的偶然写法,收敛成稳定 API
这一点,从整个系列里已经很容易看出来。
比如这些对象与接口,本质上就不是"随手提炼"的结果,而是一次重新设计后的公共边界:
MqttClientAdapterMqttClientAdapterFactoryMqttListenerRegistryMqttMessageRouterPayloadConverter/PayloadSerializerMqttSubscriptionManagerMqttSubscriptionReconcilerMqttTemplate
这些抽象有一个共同特点:
- 它们不带业务词汇
- 它们不直接绑定某个具体客户端实现
- 它们足够小,但能拼成完整链路
这其实就是"内部可用代码"和"开源框架代码"的本质区别。
内部代码很多时候只需要:
- 先让当前项目跑起来
- 兼容现有调用方
- 对外部使用者没有稳定承诺压力
但开源框架不一样。它需要的是:
- 抽象名字清楚
- 依赖方向长期稳定
- 模块边界足够清晰
- 扩展点能让别人理解并接入
这也是为什么 mqtt-plus 最终会长成:
coreadapterspringstartertest
这类结构,而不是简单把原来那 4 个内部模块换个名字重新发布。
六、第四步:开源后的边界为什么反而更克制?
很多人对开源有一个误解,觉得一旦开源,就应该"把能做的都做进去"。
但对框架来说,真正更成熟的做法往往相反:
- 越是对外承诺,就越要控制范围
- 越是想稳定演进,就越要先收住边界
mqtt-plus 当前的范围其实就很能说明这一点。
从 README 可以看到,这一版明确写了:
- 已包含:
mqtt-plus-core、mqtt-plus-paho、mqtt-plus-spring-integration、mqtt-plus-spring、mqtt-plus-spring-boot-starter、mqtt-plus-test - 暂缓:
mqtt-plus-hivemq、MQTT 5.0、运行时动态修改 broker 连接信息
这说明开源后的第一优先级并不是"覆盖一切场景",而是:
- 先把当前边界内的能力做到结构稳定
- 让使用者知道什么已经是承诺,什么还不是
- 避免在还没稳定之前就把过多路线一口气暴露成公共 API
如果结合前 9 篇一起看,你会发现这种克制几乎贯穿全系列:
- interceptor 只做前后钩子,不承担错误决策
ErrorAction已经建模,但协议层动作闭环还没完全打通- 动态订阅恢复模型已经有了,但动态 qos 还没被完整持久化
- starter 聚焦装配,不把所有策略都参数化
这些都不是"没做完"的简单表象,更像是一种很清楚的开源节奏:
先把边界定义对,再逐步扩张能力。
设计决策: mqtt-plus 开源后的范围比内部实现更克制,不是因为能力不够,而是因为对外 API 一旦形成承诺,后续演进成本会急剧上升。先收住范围、先稳住边界,是比"功能先做满"更理性的开源策略。
七、从整个系列回看,哪些取舍最能说明"这是一个框架",而不是"一份内部代码导出"?
如果把前 9 篇一起回看,我觉得最能体现这个转变的,至少有 5 个点。
1. core 零框架依赖
这说明开源后的第一承诺是稳定内核,而不是先服务某个生态接入方式。
2. adapter 可插拔
这说明 transport 选择不是框架公共语义的一部分,而是可替换实现的一部分。
3. Spring 层只做粘合
这说明框架没有把 Spring 经验直接写死进核心抽象里。
4. 测试体系单独成模块
这说明"如何验证框架"也被当成产品化能力,而不是只在项目内部临时写点测试。
5. 范围被明确写进 README
这说明框架已经开始对外管理预期,而不是无限延展。
换句话说,mqtt-plus 真正开源出来的,不只是若干实现类,而是一套对外可解释、可约束、可维护的结构。
八、这一篇真正想给读者留下什么?
第 10 篇如果只停留在"它来自内部项目",其实信息量是不够的。
我更希望它留下的是一个更通用的判断标准:
当你想把内部项目里的某部分能力抽成开源框架时,真正该先问的不是:
- 现在这份代码能不能跑
- 有没有人会 Star
- 要不要先发一个版本
而是:
- 哪些能力跨场景重复成立
- 哪些能力只是当前业务的局部最优
- 哪些边界已经稳定到值得对外承诺
- 哪些范围还应该先留在项目内部慢慢长
如果用这个标准回头看 mqtt-plus,你会发现它最重要的地方其实不是"支持了哪些功能",而是:
它在抽取过程中先把边界想清楚了。
这也是为什么整个系列虽然讲了很多实现细节,但最后仍然会收束到一个更高层的结论:
- 好框架不是功能堆出来的
- 好框架往往是边界收出来的
九、小结
第 10 篇是整个系列的收束篇。
如果把它压缩成几句结论,大概就是:
- mqtt-plus 的起点不是空白设计,而是内部项目里长期重复出现的 MQTT 共性问题
- 真正被抽出来的,不是所有内部代码,而是那些跨场景可复用的框架能力
- 真正被留下的,不是"不重要"的部分,而是那些仍带有业务负担、边界还不稳定、或者暂时不适合公共承诺的能力
- 开源之后的 mqtt-plus 比内部实现更克制,这不是退步,而是为了让模块边界、API 承诺和后续演进都更稳
如果前 9 篇回答的是"mqtt-plus 现在为什么这样工作",那第 10 篇回答的就是:
它为什么值得以现在这种形态存在。
这也意味着整个系列到这里就闭环了。
- 第 1 篇讲为什么这样分层
- 第 2 到第 9 篇讲这些分层各自承载什么责任
- 第 10 篇则回到源头,解释这些边界为什么会被画成现在这样
系列导航
本文是 mqtt-plus 架构解析 系列的第 10/10 篇。
| # | 主题 | 链接 |
|---|---|---|
| 1 | 总览:分层架构与设计哲学 | 链接 |
| 2 | 消息路由:一条 MQTT 消息如何到达你的 @MqttListener |
链接 |
| 3 | Payload 序列化与反序列化:双链设计的取舍 | 链接 |
| 4 | 拦截器链:MqttMessageInterceptor 的扩展点设计 |
链接 |
| 5 | 错误处理:ErrorAction 聚合策略的设计逻辑 |
链接 |
| 6 | 多 Broker 管理:如何让一个应用同时连接多个 MQTT 服务 | 链接 |
| 7 | 动态订阅与重连恢复:Reconciler 的协调机制 |
链接 |
| 8 | Spring Boot 自动装配:零件是怎么被粘合起来的 | 链接 |
| 9 | 测试体系:MqttTestTemplate 与 EmbeddedBroker 的设计 |
链接 |
| 10 | 从内部项目到开源框架:mqtt-plus 的抽取过程与决策 | 本文 |
上一篇:测试体系:MqttTestTemplate 与 EmbeddedBroker 的设计
下一篇:无