参考大疆上云API - 大疆上云api是二级路由方案,我这做开发直接使用一级路由满足当前系统要求
mqttconfig 定义mqtt连接工厂,消息处理通道和路由定义,只监听需要的topic
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.central.common.utils.CustomThreadPoolTaskExecutor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.ExecutorChannel;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@Slf4j
public class MqttConfig {
@Value("${mqtt.broker-url}")
private String brokerUrl;
@Value("${mqtt.client-id}")
private String clientId;
@Value("${mqtt.username}")
private String username;
@Value("${mqtt.password}")
private String password;
@Value("${mqtt.keep-alive-interval}")
private int keepAliveInterval;
@Value("${mqtt.default-topic}")
private String defaultTopic;
// 显式定义所有通道
@Bean
public MessageChannel osdChannel() {
return new DirectChannel();
}
@Bean
public MessageChannel stateChannel() {
return new DirectChannel();
}
@Bean
public MessageChannel eventsChannel() {
return new DirectChannel();
}
@Bean
public MessageChannel defaultChannel() {
return new DirectChannel();
}
/**
* 接收reply消息
*/
@Bean
public MessageChannel replyChannel() {
return new DirectChannel();
}
/**
* 连接工厂
*/
@Bean
public MqttPahoClientFactory mqttClientFactory() {
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
MqttConnectOptions options = new MqttConnectOptions();
options.setServerURIs(new String[]{brokerUrl});
options.setUserName(username);
options.setPassword(password.toCharArray());
options.setConnectionTimeout(10);
options.setKeepAliveInterval(keepAliveInterval);
options.setAutomaticReconnect(true);
factory.setConnectionOptions(options);
return factory;
}
/**
* 消息路由
*/
@Bean
public MessageChannel mqttRouterInputChannel() {
return new ExecutorChannel(mqttRouterExecutor());
}
/**
* 多线程处理器
*/
@Bean
public Executor mqttRouterExecutor() {
CustomThreadPoolTaskExecutor executor = new CustomThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setDaemon(true);
executor.setAllowCoreThreadTimeOut(false);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("mqtt-router-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
/**
* 发送处理器
*/
@Bean
@ServiceActivator(inputChannel = IMqttMessageGateway.SEND_CHANNEL)
public MessageHandler mqttOutbound(MqttPahoClientFactory mqttPahoClientFactory) {
MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(
UUID.randomUUID().toString(), mqttPahoClientFactory);
DefaultPahoMessageConverter converter = new DefaultPahoMessageConverter();
// use byte types uniformly
converter.setPayloadAsBytes(true);
messageHandler.setAsync(true);
messageHandler.setDefaultQos(0);
messageHandler.setConverter(converter);
return messageHandler;
}
/**
* 接收消息适配器
*/
@Bean
public MqttPahoMessageDrivenChannelAdapter mqttInbound(MqttPahoClientFactory mqttPahoClientFactory) {
// 代理接收指定的topic消息 根据路径匹配
MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(
"inboundClient-" + UUID.randomUUID().toString(), mqttPahoClientFactory);
String[] split = defaultTopic.split(",");
for (String s : split) {
adapter.addTopic(s, 1);
}
DefaultPahoMessageConverter converter = new DefaultPahoMessageConverter();
// use byte types uniformly 使用字节类型处理消息
converter.setPayloadAsBytes(true);
// 将接收到的 MQTT 消息转换为 Spring Integration 消息
adapter.setConverter(converter);
// 设置消息的服务质量为 1,表示消息至少会被传递一次。
adapter.setQos(1);
// 将接收到的消息发送到 inboundChannel 消息通道中。
adapter.setOutputChannel(mqttRouterInputChannel());
return adapter;
}
@Bean
public IntegrationFlow routerFlow() {
return IntegrationFlows.from(mqttRouterInputChannel())
.route(Message.class, message -> {
String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC, String.class);
// 使用正则匹配动态主题 此处return返回的结果 在下方 channelMapping 第一个参数进行匹配使用
if (topic.matches("thing/product/.+/osd")) {
return "osdChannel";
} else if (topic.matches("thing/product/.+/events")) {
return "eventsChannel";
} else if (topic.matches("thing/product/.+/state")) {
return "stateChannel";
}
return "defaultChannel";
}, mapping -> {
mapping.channelMapping("osdChannel", "osdChannel");
mapping.channelMapping("eventsChannel", "eventsChannel");
mapping.channelMapping("stateChannel", "stateChannel");
mapping.defaultOutputChannel("defaultChannel");
})
.get();
}
/**
* 回复消息的入站适配器
*/
@Bean
public MqttPahoMessageDrivenChannelAdapter mqttReplyInbound(MqttPahoClientFactory mqttPahoClientFactory) {
// 这里使用通配符订阅所有可能的回复主题
MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(
"replyClient-" + UUID.randomUUID().toString(), mqttPahoClientFactory);
adapter.addTopic("thing/product/+/services_reply", 1); // 订阅所有主题,实际应用中应该更精确
DefaultPahoMessageConverter converter = new DefaultPahoMessageConverter();
converter.setPayloadAsBytes(true);
adapter.setConverter(converter);
adapter.setQos(1);
adapter.setOutputChannel(replyChannel());
return adapter;
}
@Bean
@ServiceActivator(inputChannel = IMqttMessageGateway.REPLY_CHANNEL)
public MessageHandler replyMessageHandler() {
return message -> {
try {
byte[] payload = (byte[]) message.getPayload();
JSONObject jo = JSONObject.parseObject(new String(payload, StandardCharsets.UTF_8));
String tid = jo.getString("tid");
if (StrUtil.isNotBlank(tid)) {
// 关键修改:始终创建通道(true)
Chan.getInstance(tid, true).put(tid, jo);
log.debug("Processed MQTT reply, tid={}, topic={}, data={}",
tid,
message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC),
jo.toJSONString());
}
} catch (Exception e) {
log.error("MQTT reply processing failed. Payload: {}",
new String((byte[]) message.getPayload()), e);
}
};
}
}
发送消息网关
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
@Component
@MessagingGateway(defaultRequestChannel = IMqttMessageGateway.SEND_CHANNEL)
public interface IMqttMessageGateway {
/**
* 回复topic
*/
String REPLY_TOPIC = "REPLY_TOPIC";
String SEND_CHANNEL = "outbound";
String REPLY_CHANNEL = "replyChannel";
String REPLY_TIMEOUT_MS = "5000";
/**
* Publish a message to a specific topic.
*
* @param topic target
* @param payload message
*/
void publish(@Header(MqttHeaders.TOPIC) String topic, byte[] payload);
/**
* Use a specific qos to push messages to a specific topic.
*
* @param topic target
* @param payload message
* @param qos qos
*/
void publish(@Header(MqttHeaders.TOPIC) String topic, byte[] payload, @Header(MqttHeaders.QOS) int qos);
}
处理消息 handler 自定义实现逻辑,可以选择写入redis或者入库,未后续业务做前置数据准备
import cn.geoyt.business.common.PatrolConstant;
import cn.geoyt.business.mapper.DeviceOsdInfoMapper;
import cn.geoyt.business.mapper.ReturnHomeInfoMapper;
import cn.geoyt.business.model.entity.DeviceOsdInfo;
import cn.geoyt.business.model.entity.ReturnHomeInfo;
import cn.geoyt.business.utils.helper.PSDKUtil;
import cn.geoyt.business.utils.invoke.CloudApiInvoke;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.util.TypeUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.MessageHandler;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.LinkedHashMap;
@Configuration
@Slf4j
public class ChannelHandlerConfig {
@Autowired
private XXXMapper xxxmapper;
@Autowired
private Redistemplate redistemplate;
@Bean
@ServiceActivator(inputChannel = "stateChannel")
public MessageHandler stateMessageHandler() {
return message -> {
String topic = message.getHeaders().get("mqtt_receivedTopic").toString();
// 截取topic thing/product/+/osd
String sb = topic.substring("thing/product/".length());
String sn = sb.substring(0, sb.length() - "/state".length());
byte[] bytes = (byte[]) message.getPayload();
String stateJson = new String(bytes, StandardCharsets.UTF_8);
JSONObject jo = JSONObject.parseObject(stateJson);
boolean dataFlag = jo.containsKey("data");
// 处理业务逻辑
};
}
@Bean
@ServiceActivator(inputChannel = "osdChannel")
public MessageHandler handler() {
return message -> {
String topic = message.getHeaders().get("mqtt_receivedTopic").toString();
// 截取topic thing/product/+/osd
String sb = topic.substring("thing/product/".length());
String sn = sb.substring(0, sb.length() - "/osd".length());
byte[] bytes = (byte[]) message.getPayload();
// System.out.println("Received message from topic: " + topic + ", payload: " + new String(bytes, StandardCharsets.UTF_8));
};
}
// Events通道处理器
@Bean
@ServiceActivator(inputChannel = "eventsChannel")
public MessageHandler eventsHandler() {
return message -> {
String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC, String.class);
// 截取topic thing/product/+/osd
String sb = topic.substring("thing/product/".length());
String sn = sb.substring(0, sb.length() - "/events".length());
byte[] bytes = (byte[]) message.getPayload();
String json = new String(bytes, StandardCharsets.UTF_8);
JSONObject jo = JSONObject.parseObject(json);
// System.out.println("Received message from topic: " + topic + ", payload: " + new String(bytes, StandardCharsets.UTF_8));
};
}
@Bean
@ServiceActivator(inputChannel = "defaultChannel")
public MessageHandler defaultHandler() {
return message -> {
System.out.println("Received message on default channel: " + message);
};
}
}
处理发送消息接收回复消息监听类
import com.alibaba.fastjson.JSONObject;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
public class Chan {
private static final ConcurrentHashMap<String, Chan> CHANNEL_MAP = new ConcurrentHashMap<>();
private static final ScheduledExecutorService CLEANER = Executors.newSingleThreadScheduledExecutor();
private final AtomicReference<JSONObject> data = new AtomicReference<>();
private final AtomicReference<Thread> waitingThread = new AtomicReference<>();
private volatile ScheduledFuture<?> cleanupTask;
private Chan() {
scheduleCleanup(30_000);
}
public static Chan getInstance(String channelId, boolean createIfAbsent) {
return createIfAbsent ?
CHANNEL_MAP.computeIfAbsent(channelId, k -> new Chan()) :
CHANNEL_MAP.get(channelId);
}
public JSONObject get(String channelId, long timeoutMillis) {
Chan chan = CHANNEL_MAP.get(channelId);
if (chan == null) return null;
if (!chan.waitingThread.compareAndSet(null, Thread.currentThread())) {
return null; // 已经有线程在等待
}
chan.cancelCleanup();
try {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);
while (chan.data.get() == null) {
if (System.nanoTime() >= deadline) {
chan.waitingThread.compareAndSet(Thread.currentThread(), null);
return null;
}
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10)); // 更高效的等待
}
JSONObject result = chan.data.get();
CHANNEL_MAP.remove(channelId);
return result;
} finally {
chan.waitingThread.compareAndSet(Thread.currentThread(), null);
}
}
public void put(String channelId, JSONObject responseData) {
Chan chan = CHANNEL_MAP.computeIfAbsent(channelId, k -> new Chan());
chan.data.set(responseData);
chan.cancelCleanup();
Thread waiter = chan.waitingThread.getAndSet(null);
if (waiter != null) {
LockSupport.unpark(waiter);
} else {
chan.scheduleCleanup(10_000);
}
}
private void scheduleCleanup(long delayMillis) {
this.cleanupTask = CLEANER.schedule(() -> {
if (waitingThread.get() == null && data.get() == null) {
CHANNEL_MAP.values().removeIf(c -> c == this);
}
}, delayMillis, TimeUnit.MILLISECONDS);
}
private void cancelCleanup() {
ScheduledFuture<?> task = this.cleanupTask;
if (task != null) {
task.cancel(false);
cleanupTask = null;
}
}
public static void shutdown() {
CLEANER.shutdownNow();
CHANNEL_MAP.clear();
}
}
// 发送和回复消息使用示例 在MqttConfig配置类中有 replyMessageHandler() 方法使用到了
import cn.geoyt.business.config.mqtt.Chan;
import cn.geoyt.business.config.mqtt.IMqttMessageGateway;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.util.TypeUtils;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
/**
* @author sean
* @version 1.7
* @date 2023/5/23
*/
public class Common {
private static final JsonMapper.Builder MAPPER_BUILDER = JsonMapper.builder();
static {
JavaTimeModule timeModule = new JavaTimeModule();
timeModule.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
timeModule.addSerializer(LocalDateTime.class,
new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
MAPPER_BUILDER.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
.disable(MapperFeature.IGNORE_DUPLICATE_MODULE_REGISTRATIONS)
.addModule(timeModule)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
}
public static ObjectMapper getObjectMapper() {
return MAPPER_BUILDER.build();
}
public static JSONObject publish(IMqttMessageGateway mqttMessageGateway, String topic, Map<String, Object> map) {
try {
byte[] payload = Common.getObjectMapper().writeValueAsBytes(map);
mqttMessageGateway.publish(topic, payload, 1);
String tid = TypeUtils.castToString(map.get("tid"));
// 5秒超时
return Chan.getInstance(tid, true).get(tid, 5000);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public static String convertSnake(String key) {
StringBuilder sb = new StringBuilder();
boolean isChange = false;
for (char c : key.toCharArray()) {
if (c == '_') {
isChange = true;
continue;
}
if (isChange) {
sb.append((char) (c - 32));
isChange = false;
continue;
}
sb.append(c);
}
return sb.toString();
}
}
// Controller
import cn.geoyt.business.config.mqtt.IMqttMessageGateway;
import cn.geoyt.business.utils.Common;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/demo")
public class DemoController {
@Resource
private IMqttMessageGateway mqttMessageGateway;
@PostMapping("/test")
public String test(@RequestParam("sn") String dockSn) {
Map<String, Object> map = new HashMap<>();
String uuid = IdUtil.simpleUUID();
map.put("bid", uuid);
map.put("method", "psdk_input_box_text_set");
map.put("tid", uuid);
map.put("timestamp", System.currentTimeMillis());
HashMap<String, Object> data = new HashMap<>();
data.put("psdk_index", 2);
data.put("value", "111111");
map.put("data", data);
JSONObject result = Common.publish(mqttMessageGateway, "thing/product/" + dockSn + "/services", map);
return result == null ? "获取结果失败" : result.toJSONString();
}
}