概述
本项目实现了基于Spring Integration MQTT的设备数据采集功能,用于接收地热监测设备通过MQTT协议发送的实时数据。系统支持微功耗设备和市电设备两种类型的数据接入,并将数据存储到PostgreSQL数据库中。
依赖配置
Maven 依赖
在 /pom.xml 中添加以下依赖:
xml
<!-- MQTT -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
</dependency>
该依赖包含了Eclipse Paho MQTT客户端和Spring Integration MQTT的支持。
配置说明
application-dev.yml 配置项
yaml
spring:
# mqtt 的相关配置服务
mqtt:
username: admin # MQTT服务器用户名
password: xxx # MQTT服务器密码
# 指定MQTT服务器的URI,包括协议类型和服务器地址及端口
server-uri: tcp://127.0.0.1:1883 # MQTT服务器地址
# 客户端ID,用于在MQTT服务器上唯一标识这个客户端
client-id: consumerClient-paho # 客户端唯一标识
# 配置需要订阅的主题
topic:
# 用于订阅"boat"主题 - 现在没有,用于测试
dr_data: dr_data
# 用于订阅"collector"主题
collector: collector
# 用于订阅 "DZSDCQETZ/p867297065936977" 主题
DZSDCQETZ/s867297065936981: DZSDCQETZ/s867297065936981
# 使用通配符订阅所有以"sensor"开头的主题
sensor: '+/sensor'
# 指定默认的主题,当没有指定具体主题时,客户端将订阅这个主题
default-topic: command # 默认主题
配置项说明
| 配置项 | 说明 | 示例值 |
|---|---|---|
username |
MQTT服务器认证用户名 | superadmin |
password |
MQTT服务器认证密码 | drzyjg@131 |
server-uri |
MQTT服务器连接地址 | tcp://127.0.0.1:1883 |
client-id |
客户端唯一标识符 | consumerClient-paho |
topic |
需要订阅的主题列表 | 多个主题配置 |
default-topic |
默认发布消息的主题 | command |
核心组件
1. MqttConfig - MQTT配置类
位于: /mqtt/config/MqttConfig.java
该类负责配置MQTT客户端的连接参数、消息收发通道以及消息处理器。
主要功能
- MQTT客户端工厂配置: 创建并配置MqttPahoClientFactory
- 消息输入通道: 创建DirectChannel用于接收MQTT消息
- 消息订阅配置: 配置MqttPahoMessageDrivenChannelAdapter订阅指定主题
- 消息处理器: 定义ServiceActivator处理接收到的MQTT消息
- 消息输出通道: 创建用于发送消息到MQTT服务器的通道
关键代码片段
java
@Configuration
public class MqttConfig {
@Value("${spring.mqtt.server-uri}")
private String serverUri;
@Value("${spring.mqtt.client-id}")
private String clientId;
@Value("${spring.mqtt.username}")
private String username;
@Value("${spring.mqtt.password}")
private String password;
// 主题列表数据缓存对象
public static List<String> topicList = new CopyOnWriteArrayList<>();
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
MqttConnectOptions options = new MqttConnectOptions();
options.setServerURIs(new String[]{serverUri});
options.setUserName(username);
options.setPassword(password.toCharArray());
factory.setConnectionOptions(options);
return factory;
}
@Bean
public MessageProducer inbound() {
// 从数据库加载设备主题并订阅
LambdaQueryWrapper<TbSbjbxx> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.select(TbSbjbxx::getZt);
List<TbSbjbxx> resultList = tbSbjbxxMapper.selectList(lambdaQueryWrapper);
resultList.forEach(entity -> topicList.add(entity.getZt()));
String[] topicsArray = topicList.toArray(new String[0]);
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(clientId, mqttClientFactory(), topicsArray);
adapter.setCompletionTimeout(5000);
adapter.setQos(2);
adapter.setOutputChannel(mqttInputChannel());
return adapter;
}
@Bean
@ServiceActivator(inputChannel = "mqttInputChannel")
public MessageHandler handler() {
return message -> {
String payload = message.getPayload().toString();
String topic = message.getHeaders().get("mqtt_receivedTopic").toString();
// 处理MQTT消息
boolean insertSuccess = this.mqttMessageProcessMethod(topic, payload);
if (insertSuccess) {
log.info("主题为:{} 的设备信息接收完成,并成功入库", topic);
} else {
log.info("丢弃消息:主题[" + topic + "], 负载:" + payload);
}
};
}
}
2. 数据实体类
TbSbjbxx - 设备基本信息实体
位于: /src/main/java/com/mqtt/domain/entity/TbSbjbxx.java
对应数据库表 tb_sbjbxx,存储设备的基本信息。
java
@Data
@TableName("tb_sbjbxx")
@ApiModel(value = "TbSbjbxx", description = "设备基本信息实体类")
public class TbSbjbxx {
@TableId(type = IdType.AUTO)
private Integer id;
private String sbxlh; // 设备序列号
private String azwz; // 安装位置
private Integer sblx; // 设备类型(1:市电设备;2:微功耗设备)
private Integer drjId; // 地热井id
private String zt; // 主题(设备唯一固定的主题)
private Date xgsj; // 设备信息更新时间
private String yw; // 液位(冗余字段)
private String jwd; // 井温度(冗余字段)
private Integer jcjlx; // 监测井类型
private String jmc; // 井名称
private String xmc; // 县名称
private String smc; // 市名称
private String lon; // 经度
private String lat; // 纬度
public Integer sbzt; // 设备状态(0:正常;1:异常)
private Double cgqxs; // 传感器下深(米)
}
TbSbxxjl - 设备信息记录实体
位于: /src/main/java/com/mqtt/domain/entity/TbSbxxjl.java
对应数据库表 tb_sbxxjl,存储设备上报的历史数据记录。
java
@Data
@TableName("tb_sbxxjl")
@ApiModel("设备信息记录实体")
public class TbSbxxjl {
@TableId(type = IdType.AUTO)
private Integer id;
private String sbxlh; // 设备序列号
private Date sjscsj; // 数据上传时间
private String dcdy; // 电池电压
private String jwd; // 井温度
private String yw; // 液位
private String lon; // 经度
private String lat; // 纬度
private String sbzt; // 主题
private Integer sbjbxxId; // 设备基本信息ID
}
TbMicroEmqxDataEntity - 微功耗设备数据实体
用于解析微功耗设备发送的JSON格式数据。
java
@Data
public class TbMicroEmqxDataEntity {
@Data
public class LocationEntity {
private Double latitude;
private Double longitude;
private Double altitude;
}
@Data
public class dataDict {
private Double level; // 液位
private Double temp; // 传感器温度
private Double vdd; // 电池电压
}
private String id;
private String event;
private LocationEntity location;
private String time;
private dataDict dict;
private String imei;
private String subtopic;
private String devicename;
private String softversion;
}
3. 数据处理工具类
TbMicroEmqxDataEntityToTbSbxxjlEntityUtil
负责将微功耗设备的原始数据转换为数据库实体对象。
java
public class TbMicroEmqxDataEntityToTbSbxxjlEntityUtil {
public static TbSbxxjl toTbSbxxjlEntity(TbMicroEmqxDataEntity tbMicroEmqxDataEntity) {
if (tbMicroEmqxDataEntity == null) {
return null;
}
TbSbxxjl target = new TbSbxxjl();
// 设备序列号
target.setSbxlh(tbMicroEmqxDataEntity.getImei());
// 数据上传时间
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
target.setSjscsj(sdf.parse(tbMicroEmqxDataEntity.getTime()));
} catch (ParseException e) {
e.printStackTrace();
}
// 电池电压
target.setDcdy(String.valueOf(tbMicroEmqxDataEntity.getDict().getVdd()));
// 井温度
target.setJwd(String.valueOf(tbMicroEmqxDataEntity.getDict().getTemp()));
// 液位
target.setYw(String.valueOf(tbMicroEmqxDataEntity.getDict().getLevel()));
// 经纬度
if (tbMicroEmqxDataEntity.getLocation() != null) {
target.setLon(String.valueOf(tbMicroEmqxDataEntity.getLocation().getLongitude()));
target.setLat(String.valueOf(tbMicroEmqxDataEntity.getLocation().getLatitude()));
}
return target;
}
}
工作流程
1. 系统启动阶段
- Spring容器启动,加载MqttConfig配置类
- 从数据库
tb_sbjbxx表中查询所有设备的主题(zt字段) - 将主题列表缓存到
topicList静态变量中 - 创建MQTT客户端并订阅所有设备主题
- 建立与MQTT服务器的连接
2. 消息接收阶段
- 设备通过MQTT协议向指定主题发布消息
- MqttPahoMessageDrivenChannelAdapter接收到消息
- 消息通过mqttInputChannel传递到handler处理器
- handler提取消息的主题(topic)和负载(payload)
3. 消息处理阶段
- 验证主题是否在允许的主题列表中
- 根据主题查询设备基本信息(tb_sbjbxx)
- 根据设备类型(sblx)选择不同的解析方式:
- 市电设备(sblx=1): 直接解析JSON获取IMEI、level、temp、time等字段
- 微功耗设备(sblx=2): 解析复杂的JSON结构,提取imei、dict、location、time等字段
- 将解析后的数据转换为TbSbxxjl实体对象
- 更新设备基本信息表中的冗余字段(yw、jwd)
- 插入设备信息记录到
tb_sbxxjl表 - 保存原始消息到
tb_sbxxjl_ys表
4. 数据流转图
设备 → MQTT Broker → Spring Integration → 消息处理器 → 数据解析 → 数据库存储
↓
主题验证 → 设备类型判断 → 不同解析策略
↓
tb_sbjbxx (设备基本信息)
tb_sbxxjl (设备历史记录)
tb_sbxxjl_ys (原始消息)
使用示例
设备端发送数据
微功耗设备数据格式
json
{
"id": "msg_001",
"event": "upload",
"imei": "867297065936981",
"time": "20240809143025",
"devicename": "微功耗设备01",
"softversion": "v1.0",
"subtopic": "DZSDCQETZ/s867297065936981",
"location": {
"latitude": 38.0428,
"longitude": 114.5149,
"altitude": 50.5
},
"dict": {
"level": 12.5,
"temp": 25.8,
"vdd": 3.6
}
}
市电设备数据格式
json
{
"IMEI": "867297065936977",
"time": "2024-08-09 14:30:25",
"level": 15.3,
"temp": 26.5
}
后端接收处理
系统会自动处理接收到的消息,无需额外的代码调用。消息处理逻辑在 MqttConfig.handler() 方法中自动执行。
查询设备数据
可以通过以下Controller接口查询设备数据:
TbSbjbxxController - 设备基本信息管理
java
@RestController
@RequestMapping("/deviceInfoOperation")
@Api(tags = {"====== 获取设备基本信息接口 ======"})
public class TbSbjbxxController {
/**
* 分页查询设备基本信息
*/
@PostMapping("/pageList")
public R pageList(@RequestBody @Validated SbjbxxPageListParam param) {
Page<TbSbjbxx> page = tbSbjbxxService.pageList(param);
return R.ok(page);
}
/**
* 新增设备基本信息
*/
@PostMapping("/insert")
public R insert(@RequestBody @Validated SbjbxxInsertParam param) {
tbSbjbxxService.insert(param);
return R.ok();
}
/**
* 更新设备基本信息
*/
@PostMapping("/update")
public R update(@RequestBody @Validated SbjbxxUpdateParam param) {
tbSbjbxxService.update(param);
return R.ok();
}
/**
* 删除设备基本信息
*/
@PostMapping("/delete")
public R delete(@RequestBody @Validated SbjbxxDeleteParam param) {
tbSbjbxxService.delete(param);
return R.ok();
}
}
TbSbxxjlController - 设备消息记录查询
java
@RestController
@RequestMapping("/getDeviceMessage")
public class TbSbxxjlController {
@PostMapping("/getDrjTemperatureAndWaterLevel")
public R getDrjTemperatureAndWaterLevel(@RequestBody DrjParam param) {
List<DrjTemperatureAndWaterLevelVO> list =
tbSbxxjlService.getDrjTemperatureAndWaterLevel(param);
return R.ok(list);
}
}
tb_sbxxjl - 设备信息记录表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键ID(自增) |
| sbxlh | VARCHAR | 设备序列号 |
| sjscsj | TIMESTAMP | 数据上传时间 |
| dcdy | VARCHAR | 电池电压 |
| jwd | VARCHAR | 井温度 |
| yw | VARCHAR | 液位 |
| lon | VARCHAR | 经度 |
| lat | VARCHAR | 纬度 |
| sbzt | VARCHAR | 设备主题 |
| sbjbxx_id | INTEGER | 设备基本信息ID |
tb_sbxxjl_ys - 设备原始消息表
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 主键ID(自增) |
| sbzt | VARCHAR | 设备主题 |
| ysxx | TEXT | 原始消息内容 |
| jssj | TIMESTAMP | 接收时间 |
注意事项
1. 主题管理
- 系统启动时会自动从
tb_sbjbxx表加载所有设备主题 - 只有主题列表中的设备消息才会被处理和存储
- 新增设备后需要重启应用或手动刷新主题列表
2. 设备类型区分
- 市电设备(sblx=1): 数据结构简单,直接包含IMEI、level、temp、time字段
- 微功耗设备(sblx=2): 数据结构复杂,包含嵌套的dict、location对象
3. QoS设置
- 系统使用QoS 2级别,确保消息恰好传递一次
- 适用于对数据完整性要求较高的场景
4. 异步发送
- 消息发送采用异步模式(
setAsync(true)) - 提高系统吞吐量,避免阻塞主线程
5. 错误处理
- 消息处理过程中捕获所有异常,防止单个消息失败影响整体运行
- 无效主题的消息会被丢弃并记录日志
6. 性能优化建议
- 对于高并发场景,考虑使用消息队列缓冲
- 定期清理历史数据,避免数据库过大
- 监控MQTT连接状态,实现断线重连机制
常见问题
Q1: 如何添加新的设备主题?
A: 在 tb_sbjbxx 表中插入新的设备记录,包含正确的主题(zt字段),然后重启应用。
Q2: 如何修改MQTT服务器地址?
A: 修改 application-dev.yml 中的 spring.mqtt.server-uri 配置项。
Q3: 设备消息未入库怎么办?
A: 检查以下几点:
- 设备主题是否在
tb_sbjbxx表中存在 - MQTT服务器连接是否正常
- 查看应用日志,确认是否有异常信息
- 验证设备发送的数据格式是否正确
Q4: 如何查看接收到的原始消息?
A: 查询 tb_sbxxjl_ys 表,该表存储了所有设备发送的原始消息内容。
Q5: 如何实现消息的重试机制?
A: 当前版本未实现自动重试,可以在 handler() 方法中添加重试逻辑,或使用Spring Retry框架。