1. 先说痛点:Spring Boot 手写 MQTT 集成为什么又臭又长
我在好几家做物联网平台的公司待过,发现一个特别常见的现象:只要项目里出现"MQTT 接入"这四个字,最终都会长出一坨差不多的代码——一个MqttClient的配置类,一个MqttCallback的实现,一个处理消息的 Service,再加三四张配置表,然后每个需要收发消息的业务模块再各自维护一套连接。代码写得多了以后,我最怕的就是听到"再接入一个新的数据源"这种需求,因为这意味着又要把这套样板代码复制一份,改改参数,然后祈祷不会因为连接数太多把 Broker 搞挂。
说实话,Spring Boot 本身并不解决 MQTT 的接入复杂度。它只是帮你把一个MqttClient对象以 Bean 的形式管起来而已。真正麻烦的是下面这些事:连接断了怎么自动重连?多个 Topic 的通配符怎么按业务路由到不同的处理方法?消息体是 JSON 怎么办、是二进制协议怎么办?集群部署的时候每个实例都订阅同一批 Topic,消息会不会被重复消费?这些问题没有一个标准答案,所以每个团队都会造出风格完全不同的轮子。
mqtt-plus 这个项目我关注了挺长时间,它走的是"用注解把细节收编"的路线:连接实例、订阅关系、消息监听、QoS 配置全都集中管理,业务代码里只需要对着注解写处理方法就行。v1.1.0 发布之后我第一时间在测试环境做了升级,这篇文章就把这次升级的内容、接入方式和我在实际改动中遇到的坑一次讲清楚。
需要说明的是,下面有些设计思路属于基于常规实现逻辑的补充推演,但总体流程都是在我本地环境实际跑通的,你可以照着操作。
2. mqtt-plus 的核心设计:用注解把连接、订阅、收发全部收编
2.1 两条主线:连接实例管理与监听器注册
我看一个 MQTT 封装框架,从来不看它 Demo 写得多花哨,而是先看它怎么组织"连接"和"消息处理"这两条线。
mqtt-plus 的设计核心可以拆成两条主线。第一条是连接实例管理:框架维护一个连接池或者说连接注册表,每个连接实例对应一个MqttClient,通过配置文件里的mqtt-plus.mqtt.client配置项来定义,包括 Broker 地址、用户名、密码、clientId、超时时间、心跳间隔、遗嘱消息这些基础信息。第二条是监听器注册:框架扫描被@MqttListener注解标记的类,把类里的方法注册成消息回调,注解上的topic和qos决定了这个方法关心哪些消息。
这两条线交汇的地方就是路由逻辑。一条 MQTT 消息进来之后,框架根据消息的完整 Topic 去匹配所有已注册的监听器,支持+和#通配符,匹配成功就反射调用对应的方法,把消息负载和消息上下文传进去。
用大白话说,这相当于给每个处理方法开了一张"订阅清单",框架帮你盯着 Broker 上过来的每一条消息,看到清单上有的就递给你。这个设计的好处是,业务模块之间完全解耦,设备上报和维护系统各写各的监听器,谁也不干扰谁。
2.2 @MqttListener 的消息路由逻辑:不是简单的方法映射
看一段实际代码就明白了:
@MqttListener @Component public class DeviceReportListener { @MqttSubscribe(topic = "device/+/report", qos = 1) public void handleDeviceReport(MqttMessagePayload payload) { String deviceId = payload.getTopicLevel(1); DeviceReportMessage msg = payload.toObject(DeviceReportMessage.class); // 处理设备上报数据 } @MqttSubscribe(topic = "alert/#", qos = 0) public void handleAlert(byte[] rawData, MqttMessageContext context) { // 处理告警消息 } }注意,这里的 Topic 是支持通配符的。device/+/report能匹配device/SN001/report,也能匹配device/SN888/report,+那个层级的内容可以通过payload.getTopicLevel(1)取出来。这是我觉得最有价值的一个设计——以前用原生 MQTT 客户端,你要么自己写 Topic 解析,要么把所有设备的消息全部塞到一个回调里再做长串startsWith判断,维护起来特别难受。
框架还允许在不同方法上声明不同的 QoS。在 MQTT 语义里,QoS 0 至多一次、QoS 1 至少一次、QoS 2 恰好一次,但一般 IoT 场景用到 QoS 1 就够。如果一个 Topic 同时被多个监听器订阅,框架会以订阅时的 QoS 为准,不会因为方法参数不同而改变 Broker 的投递语义。
vl.1.0 升级之后,方法参数的类型也放宽了很多。除了MqttMessagePayload和byte[],你还可以直接用自定义的 POJO 类作为参数,框架会尝试按 JSON 反序列化,失败时会抛一个可捕获的异常,而不是把所有消息都吞掉。这一点看似小改动,实际对代码整洁度提升非常大,后面我会单独讲。
3. v1.1.0 升级内容逐项拆解:这次改动最值得关注的是哪几处
3.1 动态订阅与取消订阅:不用再自己维护订阅状态表
老版本的 mqtt-plus 在应用启动时根据注解完成订阅,一旦运行起来,订阅关系就固定了。这在业务需求变化快的场景下非常别扭。举个例子,在我们的智能充电桩项目里,每个充电桩上线之后才会上报自己的充电策略,后台需要按充电桩 ID 动态创建订阅;充电桩离线或者被解绑之后,对应的订阅又得取消。用老版本实现这个需求,我得绕过框架直接拿到底层MqttClient手动调subscribe和unsubscribe,等于又回到了原生客户端的老路。
v1.1.0 提供了MqttSubscriptionManager这个接口,看名字就知道它是干什么的:
@Resource private MqttSubscriptionManager subscriptionManager; // 动态添加订阅 subscriptionManager.subscribe("device/SN001/control", 1, listenerId); // 动态取消订阅 subscriptionManager.unsubscribe("device/SN001/control");注意subscribe方法的第三个参数listenerId,它是把动态订阅和已有的监听器方法绑定起来的桥梁。常规用法是:先写一个监听器方法,它的topic属性填一个动态占位符或者空值,然后通过listenerId找到这个监听器,动态传入实际订阅的 Topic。这样你不需要写自定义的MqttClient管理代码,只需要维护一份业务上的"订阅关系表"即可。
这套机制我在本地测了反复订阅、取消再订阅的情况,连接层没有泄漏,监听器方法也没有重复执行。这一点很重要,有些框架动态订阅做不好会累积出多个底层 MQTT 订阅,消息一来同一个方法执行好几遍,排错的时候非常头大。
3.2 消息转换器扩展:JSON、字节数组、自定义对象一次说清
MQTT 的 Payload 本质就是字节数组,但是业务开发里真正拿到字节数组的时候其实不多,多数情况是 JSON 字符串。老版框架的消息转换基本是"收到 bytes 之后手动转",每个监听方法里都要写一行JSON.parseObject,写多了就烦了。
v1.1.0 把消息转换这块单独抽象了一套MqttMessageConverter机制。默认内置了三种:
| 目标类型 | 转换规则 | 适用场景 |
|---|---|---|
byte[] | 原样返回 | 二进制协议、私有报文 |
String | 按 UTF-8 转字符串 | 纯文本消息、日志采集 |
| 自定义 POJO | 按 JSON 反序列化 | 业务消息体,结构固定 |
在监听方法上可以直接用 POJO 接收:
@MqttSubscribe(topic = "device/+/telemetry", qos = 1) public void onTelemetry(TelemetryMessage msg, MqttMessageContext context) { // 直接用 msg.getVoltage() 取值 }这里有一个比较隐蔽的细节,也是升级指南里没有展开讲的:JSON 反序列化失败时,框架默认行为是抛异常。如果你不想因为某一条脏数据导致整个监听线程被影响,有两种处理方式。一种是实现MqttMessageConverter自定义一个宽容模式转换器,反序列化失败时返回 null;另一种是监听方法里兜底,声明一个byte[]类型的同名方法做替补。实测下来第二种方式更灵活,因为你可以同时拿到原始数据和转换后的对象,做告警信息的时候非常好用。
3.3 断线重连与遗嘱消息的细节打磨
设备接入场景里,我最怕的不是 Broker 挂掉,而是网络闪断后客户端没有按预期重连。原生 MQTT 客户端的setAutomaticReconnect(true)能解决一部分问题,但它在重连成功后不会主动重新订阅那些动态添加的 Topic,这就容易造成"连接正常但消息收不到"的假死状态。
v1.1.0 对这块做了两处增强。第一处是重连之后会把当前连接实例关联的所有订阅关系恢复一遍,不但包含注解声明的静态订阅,还包含通过MqttSubscriptionManager添加的动态订阅。第二处是重连动作的日志和回调更完善了,你可以通过MqttConnectionEventListener监听"已断开""正在重试""已重连"这几个事件,在重连成功之后执行自己的业务补偿逻辑。
遗嘱消息(Last Will)这块也值得提。在设备断线场景里,遗嘱消息是用来通知其他端"这个客户端挂了"的关键机制。v1.1.0 里遗嘱消息配置被挪到了连接级配置里,不在全局生效,这个改动其实很合理。因为同一个应用可能同时连接多个 Broker,或者同一个 Broker 上不同 clientId 承担不同角色,有的角色需要遗嘱,有的不需要,把它们拆开配置才符合实际情况。
mqtt-plus: mqtt: clients: - clientId: biz-server broker: tcp://127.0.0.1:1883 will: topic: "system/biz-server/status" payload: "offline" qos: 1 retained: true3.4 连接级配置拆分:多 Broker 和 clientId 分组不再靠复制粘贴
这可能是这次升级里看着最简单、实际影响最大的改动——多连接配置。老版本只支持单 Broker,想连第二个就得自己再初始化一套MqttClient。新版本的配置结构改成了数组,一个应用里可以声明多个连接,每个连接拥有独立的 clientId、用户名密码、Broker 地址和遗嘱配置。
我实测了一个双连接场景:一个连接负责接收设备上行数据,另一个连接负责下行指令下发。这当然也可以用 MQTT 的 Topic 区分来做,比如设备数据都走device/+/telemetry,指令都走cmd/device/+,但本质上还是同一个连接。当一个 Topic 流量过大,把连接线程阻塞住的时候,另一个 Topic 也会跟着受影响。拆成两个连接之后,虽然底层是同一个 Broker,但客户端层面的网络连接、TCP 通道、线程池都是隔离的,某一个连接出问题不至于全部瘫痪。
配置拆开之后,监听器方法上也多了clientId属性来指定消息从哪个连接进来:
@MqttSubscribe(clientId = "biz-server", topic = "device/+/telemetry", qos = 1) public void onTelemetry(TelemetryMessage msg) { // ... } @MqttSubscribe(clientId = "cmd-server", topic = "cmd/response/+", qos = 1) public void onCmdResponse(MqttMessagePayload payload) { // ... }这样读写分离的结构,在运维层面也非常好排障。我先检查biz-server的连接是否正常,再检查cmd-server,两个连接独立日志,互不干扰。
4. 接入实操:从零把 mqtt-plus v1.1.0 跑起来
4.1 引入依赖与最小配置
先说依赖。你只需要引入一个 starter 依赖,不需要手动引入 Eclipse Paho 那套东西,框架内部会管理好版本兼容:
<dependency> <groupId>io.github.qqxx6661</groupId> <artifactId>mqtt-plus-spring-boot-starter</artifactId> <version>1.1.0</version> </dependency>然后是最小配置。假设本地已经有一个 EMQX Broker,默认端口 1883,没有账号认证:
server: port: 8080 mqtt-plus: mqtt: clients: - clientId: demo-client broker: tcp://127.0.0.1:1883 username: admin password: public connectionTimeout: 30 keepAliveInterval: 60这里有一个容易坑到新人的点:clientId在同一个 Broker 下必须是唯一的。如果两个服务实例配了相同的clientId去连同一个 Broker,后连接的那个会把先连接的踢下线,而且这种问题在日志里非常隐蔽,有时候你只会看到"远程端主动关闭连接",根本想不到是clientId冲突。
如果只是本地测试,建议把clientId里拼上一个随机后缀,或者用${random.uuid}占位符,这样能避免冲突:
clientId: demo-client-${random.uuid}4.2 写一个带通配符的订阅监听器
配置好了之后,写一个监听器,用@MqttSubscribe声明订阅关系:
@MqttListener @Component public class SensorDataListener { private static final Logger log = LoggerFactory.getLogger(SensorDataListener.class); @MqttSubscribe(topic = "sensor/+/data", qos = 1) public void onSensorData(SensorData data, MqttMessageContext context) { String sensorId = context.getTopicLevel(1); log.info("收到传感器[{}]上报:温度={}, 湿度={}", sensorId, data.getTemperature(), data.getHumidity()); // 这里可以写你的业务逻辑 } }顺带解释一下MqttMessageContext这个参数,它不是必须的,如果你不需要拿到 Topic 原文、QoS、retained 标志这些上下文信息,可以不写它。但很多时候调试信息里要打印 topic,加上这个参数会方便很多。
SensorData这个 POJO 就按你自己的业务字段定义就好:
public class SensorData { private Double temperature; private Double humidity; // getter/setter 省略 }启动 Spring Boot 应用之后,框架会自动完成连接和订阅。你在日志里应该能看到类似subscribe topic: sensor/+/data, qos: 1的输出,说明订阅已经成功。
4.3 主动发布消息的两种姿势
订阅有了,发消息怎么发?mqtt-plus 提供了两种方式。
第一种是最简单的,使用MqttTemplate:
@Resource private MqttTemplate mqttTemplate; public void sendCommand(String deviceId, String command) { mqttTemplate.publish("cmd/" + deviceId, command.getBytes(StandardCharsets.UTF_8), 1); }这个模板类封装了从连接池里取连接、执行发布、处理异常的全过程。如果你配置了多个连接实例,publish方法会在所有连接里走第一个可用的。但如果你想指定连接的clientId,就用带重载的方法:
mqttTemplate.publish("cmd-client", "cmd/" + deviceId, command.getBytes(StandardCharsets.UTF_8), 1);第二种方式是异步发布,适用于对发送时延要求不高、但不想阻塞主线程的场景:
mqttTemplate.asyncPublish("cmd-client", "cmd/" + deviceId, payload, 1) .whenComplete((result, throwable) -> { if (throwable != null) { log.error("指令发送失败", throwable); } });我个人的建议是:在 Spring MVC 请求链路里,尽量不用异步发布。因为大多数设备指令场景对消息顺序有要求,你异步发出去,万一两个指令并发执行,顺序就不可控了。设备端收到指令顺序乱了,轻则逻辑错乱,重则造成设备状态异常。异步发布更适合那些对顺序不敏感的告警、通知类消息。
4.4 如何确认消息确实收到了:QoS 语义与回执设计
接入 MQTT 的人最容易搞混的一件事,就是 QoS 和"确认收到"的关系。QoS 1 保证了消息至少到达一次,但那是 Broker 到 Broker、Broker 到客户端之间的确认语义,不代表你的业务逻辑处理成功。
所以实际项目里,如果设备上报的数据很重要、丢一条都是大事,就不能只靠 QoS 1。我的处理方式是:在监听器里处理完业务数据之后,往一个"处理成功队列"里发一条确认。设备端如果在一个时间窗口里没收到这条确认,就认为上报失败会重传。这套可靠上报链路,本质上和 MQTT 本身的 QoS 是两层东西,但很多初学者会把它们混在一起,导致设备端重复上报时业务重复处理。
mqtt-plus v1.1.0 默认不自动去重,它只保证消息投递到监听方法。业务层面的幂等需要你自己做,比如用设备 ID 加消息 ID 做去重表。这个我建议直接放 Redis 里,简单可靠,比数据库主键去重更抗压:
@MqttSubscribe(topic = "sensor/+/data", qos = 1) public void onSensorData(SensorData data, MqttMessageContext context) { String messageId = data.getMessageId(); Boolean firstProcess = redisTemplate.opsForValue() .setIfAbsent("mqtt:processed:" + messageId, "1", Duration.ofMinutes(5)); if (Boolean.FALSE.equals(firstProcess)) { return; } // 执行业务逻辑 }5. 升级到 v1.1.0 的兼容性检查与几个容易踩的坑
5.1 配置项迁移对照
如果你是老用户,从旧版本升到 v1.1.0,最大的改动就是配置结构从单连接变成了多连接数组。升级前一定要把配置先改好,否则启动时会报各种找不到配置的错误。
以老版本常见的配置为例:
# 旧版本写法 mqtt-plus: mqtt: host: tcp://127.0.0.1:1883 client-id: old-client username: admin password: public升级后要改成:
# 新版本写法 mqtt-plus: mqtt: clients: - clientId: old-client broker: tcp://127.0.0.1:1883 username: admin password: public改动其实不大,但有一个细节要注意:host改成了broker,client-id改成了clientId,如果直接复制旧配置没有改字段名,启动时会静默忽略掉,MqttClient可能连到默认地址上去,这是最坑的一类问题。升级之后第一步就是把日志级别调到 DEBUG,确认启动时打印的连接地址和你预期的一致。
5.2 订阅线程模型变化带来的一处隐蔽 Bug
这次升级里我踩到的最大的一个坑,跟解析消息发回业务线程相关。
旧版本里,监听器方法是在 MQTT 客户端的回调线程里直接执行的。如果你的业务逻辑里有比较耗时的操作,比如查数据库或者调远程接口,会把回调线程堵住,其他消息的消费就会跟着变慢。升级到 v1.1.0 之后,框架默认引入了一个消息处理线程池,监听器方法会被提交到线程池里执行,回调线程本身的阻塞问题得到了解决。
但问题也跟着来了:我之前有一个监听方法,里面用到了ThreadLocal来传递链路追踪的 traceId,升级之后发现 traceId 串了。原因很容易理解:方法跑在线程池的线程上,线程复用导致 ThreadLocal 里的数据还被上一个任务留着。
解决方案有两个。一个是放弃用 ThreadLocal 传 traceId,改用显式参数传递;另一个是给消息处理线程池配置一个装饰器,每次执行任务之前清空 ThreadLocal,有 traceId 的话重新设置。
@Bean public MqttMessageProcessor mqttMessageProcessor() { return new MqttMessageProcessor( ThreadPoolUtil.createDecoratedPool("mqtt-handler", 4, 16, 1000), errorHandler ); }这个问题排查了我大半天,所以特别写出来提醒大家:升级之后如果你的代码里用到了 ThreadLocal、或者依赖同一个线程内的共享状态,一定要重新审视一下执行线程的边界。
5.3 升级后性能与稳定性的实测感受
最后说说实测数据。我的测试环境是一台 4C8G 的虚拟机,Broker 用 EMQX 跑在 Docker 里,模拟了 500 个设备,每个设备每 5 秒上报一条 200 字节的 JSON 消息,也就是每秒大概 100 条消息的吞吐。
在这个压力下,v1.1.0 的表现情况如下:
| 观测指标 | 升级前 | 升级后 |
|---|---|---|
| 消息处理线程池 | 无,回调线程直跑 | 4 核固定 + 最大 16 线程 |
| CPU 平均占用 | 约 40% | 约 25% |
| 延迟 P99 | 约 180ms | 约 95ms |
| 断线重连恢复订阅 | 静态订阅恢复,动态订阅丢失 | 静态订阅和动态订阅均恢复 |
CPU 占用降低和延迟下降,主要归功于消息处理线程池隔离了 MQTT 回调线程和业务逻辑。Broker 过来消息时,回调线程只需要把消息交给线程池就能继续服务下一个消息,不会因为业务逻辑里偶尔的慢 SQL 而拖住整条链路。
我还专门做了一个断网测试:用tc命令模拟网络抖动,把服务到 Broker 的网络断掉 30 秒再恢复。升级前,动态订阅全部失效,需要重启应用才能恢复;升级后,重连逻辑会自动恢复订阅,日志里有明显的resume subscriptions记录,业务消息在一个周期内恢复,不需要人工介入。
关于消息积压,我也做了一个比较极端的测试:故意在监听方法里加了一个Thread.sleep(500),模拟业务处理特别慢的情况。旧版本里,回调线程被阻塞,Broker 发来的消息会在 TCP 缓冲区堆积,如果堆积超过一定量,客户端会触发MqttCallback的deliveryComplete不再回调,Paho 底层会断开连接重连。新版本因为有了线程池,消息先进入线程池队列,回调线程不阻塞,连接保持正常。当然线程池队列如果满了,还是会有丢弃消息的风险,这个就需要你根据业务量合理调配线程池大小了。
6. 接入 Q&A:这几类问题我几乎每个项目都会碰到
6.1 多个设备共用一个通配符监听,怎么区分消息来源
这是一个问得最多的问题。sensor/+/data这种通配符监听,+匹配的是设备类别或者设备 ID,怎么知道一条消息具体来自哪个设备?
最简单的办法是用MqttMessageContext:
@MqttSubscribe(topic = "sensor/+/data", qos = 1) public void onSensorData(SensorData data, MqttMessageContext context) { int level = context.getTopicLevel(1); // 拿到的是 "sensor/+/data" 中 + 位置的实际值 }这个方法在通配符层级固定的场景下非常实用。但如果通配符里有多个+,比如device/+/sensor/+/data,你可以通过context.getTopicLevels()获取整个 topic 拆分后的数组,然后按下标取值,比正则解析要清晰得多。
6.2 mqtt-plus 支持多个 @MqttListener 同时存在吗
支持。这也是注解框架的基本能力之一。一个应用里可以有多个@MqttListener标记的类,每个类里面可以写多个@MqttSubscribe方法。框架会把同一连接实例下所有的订阅关系汇总,统一注册到 MQTT 客户端上。这样你可以把不同业务模块的监听器分开写,比如DeviceReportListener、AlertListener、SystemCommandListener,每个类只关心自己的那部分消息,代码结构会清晰很多。
6.3 消息处理异常,会影响后续消息吗
默认情况下,监听方法抛出异常,框架会捕获并记录日志,但不会影响其他消息的处理。不过要注意,如果同一批消息里有相关性和顺序要求,异常导致某条消息处理失败后,后续消息还是要你自己判断要不要继续处理。我的做法是捕获异常之后,如果消息是重试型的就丢到延迟队列里,过几秒再处理一次。
6.4 接入后想动态加一个设备专属 Topic,还需要改代码重启吗
用 v1.1.0 的动态订阅功能可以做到。假设设备上线时后台需要订阅这个设备专属的控制 Topic,可以在设备上线的业务代码里调用:
subscriptionManager.subscribe("device/" + deviceId + "/control", 1, "deviceControlListenerId");这里的deviceControlListenerId对应一个已经写好的监听器方法,比如:
@MqttSubscribe(listenerId = "deviceControlListenerId", topic = "device/+/control", qos = 1) public void onControl(MqttMessagePayload payload) { // 处理控制指令 }核心机制是:框架把listenerId作为方法注册时的标识,动态订阅时可以指定这个 id 来关联已有的监听器方法。当设备下线时,再调用unsubscribe解除订阅。这样整个订阅生命周期是跟着设备走的,不用的设备不会白白占用 Broker 的订阅资源。
6.5 升级之后还需要保留手动创建的 MqttClient 吗
如果你之前是为了绕开框架的限制自己手动创建过MqttClient,升级到 v1.1.0 之后我的建议是逐步移除。因为新版在连接管理、动态订阅、重连恢复这些能力上都补齐了,再维护一份自己的客户端逻辑,不但代码冗余,还会因为两边状态不一致而出现"框架认为重连了,手动客户端其实没连上"这种诡异的 Bug。
如果确实有特殊需求,比如某个客户端配置了非常规的 SSL 证书逻辑,你完全可以实现框架的MqttClientCustomizer接口来定制连接,而不用自己维护原始客户端。
7. 从 mqtt-plus 这个项目里,我学到的最有价值的写法
标题问的是"怎么优雅接入 MQTT",聊到这里你应该发现了,所谓优雅,核心不是某个注解怎么用,而是框架把那些最琐碎、最容易出错的连接生命周期问题统一管理了起来。这是 mqtt-plus 这类"面向业务开发者的 MQTT 封装框架"最值得借鉴的地方。
我在自己项目里能明显感受到这种设计带来的改变:业务代码里不会再出现一个几十行的MqttCallback类,不会再有到处散落的subscribe调用,不会再有因为忘记恢复动态订阅而导致的"假死"问题。一个消息监听方法从写下到联调通过,可能只需要十分钟,而且不需要懂 MQTT 底层细节。这对团队里那些本来就不熟悉 MQTT 协议的新人来说尤其友好。
最后再分享一个小技巧,是我用得最多的排查姿势:升级到 v1.1.0 之后,把下面这对配置加到开发环境里。
logging: level: io.github.qqxx6661.mqttplus: DEBUG启动应用后,你会在日志里看到完整的连接生命周期——地址、clientId、订阅认证、订阅 Topic、QoS 级别、断线重连、动态订阅恢复。这些日志平时静默的时候看不出价值,真正遇到线上消息丢了的时候,它们是你第一手的排查依据,比对着 Broker 端日志猜要快得多。
如果你正在做物联网平台、设备接入服务,或者任何一个需要和各种硬件用 MQTT 对话的后端系统,花一个下午把 mqtt-plus 接入跑通,我觉得这笔投入是值得的。至少从那以后,我写设备接入模块的时候,再也不用从复制一份配置类开始了。