SpringBoot-integration 使用MQTT对接大疆机场+无人机 完整示例

参考大疆上云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();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值