MQTT 消息模板:类型保持、编译缓存与降级策略
同一份采集数据,交给不同下游时可能需要不同字段名和嵌套结构。为每种报文写一个 Java 分支容易维护失控。我在项目中实现了一个受限的结构模板:YAML 描述形状,占位符取值,渲染后再序列化为 JSON。
更新于 2026-09-09。依据 TemplateCompiler、MqttMessageFormatterImpl 和消息上下文实现整理。本文不把设计目标当作微秒级性能实测。
为什么先构造对象,再序列化
直接在 JSON 字符串中替换文本,看起来最简单,却很容易把数字变成字符串,或者破坏引号、数组和转义。
项目先把 YAML 解析为 Map/List,再将它编译成节点树。输出也是对象结构,最后统一序列化。以构造数据 temperature = 25.5 为例:
device: "${meta.deviceId}"
temperature: "${field.temperature}"
description: "当前温度 ${field.temperature} ℃"
payload: "${fields}"
上面是语法示例,输出规则如下:
| 模板值 | 输出 | 类型 |
|---|---|---|
${field.temperature} |
25.5 |
Number |
当前温度 ${field.temperature} ℃ |
当前温度 25.5 ℃ |
String |
${fields} |
采集字段对象 | Object |
纯占位符返回原始对象,混合文本才做字符串拼接。这样既能表达供程序读取的数据,也能表达供人阅读的描述。
这套规则只处理结构与取值,没有引入任意脚本执行。YAML 解析与 JSON 序列化仍依赖项目已有库,因此准确说法是“不额外引入通用模板引擎”,而不是“零第三方依赖”。
编译阶段承担结构分析
节点类型有五种:
| 类型 | 含义 | 渲染工作 |
|---|---|---|
| LITERAL | 常量或 null | 返回原值 |
| PLACEHOLDER | 纯占位符 | 从上下文取原始对象 |
| MIXED | 文本与占位符混合 | 取值并拼接文本 |
| OBJECT | Map | 递归构造属性 |
| ARRAY | List | 递归构造元素 |
一次编译把不随消息变化的结构分析移出渲染过程。渲染仍需要遍历节点、创建结果对象和执行序列化,不是没有分配或没有成本。
当前编译器对嵌套占位符会告警并按字面量处理。它不是完整表达式语言:条件分支、循环和复杂数据转换不应通过越来越难解释的字符串规则偷偷加入。如果需求演进到那一步,应重新比较映射器、受限表达式或成熟模板方案。
缓存的正确性比命中率重要
编译结果以模板对象为键缓存。仅把 System.identityHashCode 的整数结果作为唯一键不可靠,因为不同对象可能得到相同值。
当前实现使用 IdentityHashMap 按引用区分模板,并同步保护编译缓存访问。它能避免把相同哈希值误当成同一对象,但不会自动清理旧对象,也不会自动感知对象内容被原地修改。
因此,需要配合两条纪律:模板更新尽量替换对象,产品修改或重载时显式失效缓存。引用缓存和内容哈希缓存各有成本,选择前者意味着必须维护好对象生命周期。
性能验证也应分开测首次编译、命中渲染、序列化和发布。把网络耗时排除后的数字不能代表整个 MQTT 通道;本文不保留没有测试条件的“低于 50μs”结论。
三层优先级解决配置复用
当前模板按产品级、全局级、内置默认结构选择。设备 ID 和标签通过上下文注入,使同一模板能服务多台设备。
这满足当前项目的复用需求,不需要先实现每台设备单独配置报文。原文“覆盖 95% 场景”缺少统计依据,本次删除。未来若同一产品下确有独立报文要求,再评估增加设备覆盖或目标配置;不应仅为格式差异随意拆分物模型。
DATA、ALARM、RECOVERY 共用编译和渲染机制,但上下文不同。模板预览需要使用对应消息类型的样例,避免在数据消息里引用只存在于告警中的字段。
降级有代价,不能只看有没有发出去
当前格式化失败会回退默认结构,这有助于维持基础输出。但是,下游可能只接受自定义结构:默认 JSON 成功发出,不意味着对方成功接收和解析。
因此,降级应可观察,日志与监控需要表达“格式化失败后使用默认结构”。严格接口可以考虑拒绝发送并记录失败,宽松消费端可以接受默认结构;具体选择取决于契约,不存在统一最优答案。
缺失变量同样不能只靠 null 兜底。例如设备标识是必填字段时,输出 null 可能比明确拒绝更难排查。当前支持取值,不代表已经实现下游 Schema 的完整校验。
模板不是连接器
格式化层负责 payload,不负责 Broker 连接、鉴权、QoS、重连或多目标路由。支持多种报文格式,也不代表已经同时向多个 Broker 独立发送。
在TDengine 与 All-in-One 的集成分析中,MQTT 可以承担数据传递;是否入库、由谁消费、失败怎样补偿属于另一层设计。
有价值的验证样例
我会用数字、布尔、数组、嵌套对象、中文文本和缺失字段验证输出类型;对同模板并发渲染与连续重载检查结果隔离;对格式错误检查实际降级形状,而不仅断言没有异常。
这套实现的重点是把灵活性限制在可解释范围内。与规则引擎一样,先明确小语言的语义和边界,再增加新语法,比不断补特殊分支更容易维护。