SpringBoot整合MQTT(mosquitto)

SpringBoot基于integration整合MQTT

部署MQTT

部署MQTT可以参照博客:Linux安装mosquitto mqtt几种方式 这里不做阐述。
本文提供了高并发情况下消息推送方案,代码仅供参考,请结合实际情况进行重载。

1.引入依赖

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-integration</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-stream</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-mqtt</artifactId>
        </dependency>

2.yml配置

spring:
  mqtt:
    url: "tcp://ip:port"
    clientId: "clientId"
    username: "admin"
    password: "admin"
    defaultTopic: "default"
    completion-timeout: 10000

3.客户端配置(消息生产者)

3.1.核心参数配置

package com.zbwd.esms.edge.config;

import com.zbwd.esms.edge.enums.TopicEnum;
import com.zbwd.esms.edge.mqtt.handler.MultiMqttMessageHandler;
import com.zbwd.esms.edge.mqtt.handler.MyMqttPahoMessageHandler;
import com.zbwd.esms.edge.util.SnowFlakeUtil;
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.context.event.EventListener;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent;
import org.springframework.integration.mqtt.event.MqttMessageDeliveredEvent;
import org.springframework.integration.mqtt.event.MqttMessageSentEvent;
import org.springframework.integration.mqtt.event.MqttSubscribedEvent;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;

import java.util.Date;

/**
 * mqtt连接信息
 *
 * @author : mwx
 * @date : 2022/12/28 13:39
 */
@Slf4j
@Configuration
@IntegrationComponentScan
public class MqttConfig {

    @Value("${spring.mqtt.url}")
    private String url;

    @Value("${spring.mqtt.clientId}")
    private String clientId;

    @Value("${spring.mqtt.username}")
    private String userName;

    @Value("${spring.mqtt.password}")
    private String passWord;

    @Value("${spring.mqtt.completion-timeout}")
    private int completionTimeout;

    @Value("${spring.mqtt.defaultTopic}")
    private String defaultTopic;

    private SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil(0, 0);

    @Bean
    public MqttConnectOptions getMqttConnectOptions() {
        // MQTT的连接设置
        MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
        // 设置连接的用户名
        mqttConnectOptions.setUserName(userName);
        // 设置连接的密码
        mqttConnectOptions.setPassword(passWord.toCharArray());
        // 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,
        // 把配置里的 cleanSession 设为false,客户端掉线后 服务器端不会清除session,
        // 当重连后可以接收之前订阅主题的消息。当客户端上线后会接受到它离线的这段时间的消息
        mqttConnectOptions.setCleanSession(false);
        // 设置发布端地址,多个用逗号分隔, 如:tcp://111:1883,tcp://222:1883
        // 当第一个111连接上后,222不会在连,如果111挂掉后,重试连111几次失败后,会自动去连接222
        String[] urls = new String[]{url};
        mqttConnectOptions.setServerURIs(urls);
        // 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
        mqttConnectOptions.setKeepAliveInterval(20);
        mqttConnectOptions.setAutomaticReconnect(true);
        // 设置“遗嘱”消息的话题,若客户端与服务器之间的连接意外中断,服务器将发布客户端的“遗嘱”消息。
        //mqttConnectOptions.setWill(willTopic, willContent.getBytes(), 2, false);
        mqttConnectOptions.setMaxInflight(1000000);
        return mqttConnectOptions;
    }

    @Bean
    public MqttPahoClientFactory mqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        factory.setConnectionOptions(getMqttConnectOptions());
        return factory;
    }

    @Bean
    @ServiceActivator(inputChannel = "mqttOutboundChannel")
    public MessageHandler mqttOutbound() {
        //==========方案一(基本普通方案)==========
        //clientId每个连接必须唯一,否则,两个相同的clientId相互挤掉线
        //踩坑记录:1.相同clientId相互挤掉线导致mqtt重复断线重连和消息无法正常消费
        //String clientIdStr = clientId + snowFlakeUtil.nextId();
        //MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(clientIdStr, mqttClientFactory());
//        //async如果为true,则调用方不会阻塞。而是在发送消息时等待传递确认。默认值为false(发送将阻塞,直到确认发送)
        //messageHandler.setAsync(true);
        //messageHandler.setAsyncEvents(true);
        //messageHandler.setDefaultTopic(defaultTopic);
        //messageHandler.setDefaultQos(0);
        //return messageHandler;
        //==========方案二(并发生产消息方案)==========
        return new MultiMqttMessageHandler();
    }

    public MessageHandler create() {
        String clientIdStr = clientId + snowFlakeUtil.nextId();
        MyMqttPahoMessageHandler myMqttPahoMessageHandler = new MyMqttPahoMessageHandler(url, clientIdStr, mqttClientFactory());
        myMqttPahoMessageHandler.setAsync(true);
        myMqttPahoMessageHandler.setAsyncEvents(true);
        myMqttPahoMessageHandler.setDefaultTopic(defaultTopic);
        myMqttPahoMessageHandler.setDefaultQos(0);
        myMqttPahoMessageHandler.onInit();
        return myMqttPahoMessageHandler;
    }

    /**
     * 发消息通道
     */
    @Bean
    public MessageChannel mqttOutboundChannel() {
        return new DirectChannel();
    }

    /**
     * 接收消息通道
     */
    @Bean
    public MessageChannel mqttInputChannel() {
        return new DirectChannel();
    }

    @Bean
    public MessageProducer inbound() {
        //clientId每个连接必须唯一,否则,两个相同的clientId相互挤掉线
        String serverIdStr = clientId + snowFlakeUtil.nextId();
        //动态设置adapter的的topic值
        String[] allTopics = TopicEnum.getAllValues();
        MqttPahoMessageDrivenChannelAdapter adapter =
                new MqttPahoMessageDrivenChannelAdapter(serverIdStr, mqttClientFactory(), allTopics);
        adapter.setCompletionTimeout(completionTimeout);
        adapter.setConverter(new DefaultPahoMessageConverter());
        //这里设置为0 是因为代码对上传失败的消息做了处理
        adapter.setQos(0);
        adapter.setOutputChannel(mqttInputChannel());
        return adapter;
    }

    /**
     * 通过通道获取数据 订阅的数据
     */
    @Bean
    @ServiceActivator(inputChannel = "mqttInputChannel")
    public MessageHandler handler() {
        return message -> {
            //String payload = message.getPayload().toString();
            //String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC).toString();
            //边缘计算不做接收消息处理
        };
    }

    @EventListener(MqttConnectionFailedEvent.class)
    public void mqttConnectionFailedEvent(MqttConnectionFailedEvent event) {
        log.error("mqttConnectionFailedEvent连接mqtt失败: " +
                        "date={}, hostUrl={}, username={}, error={}",
                new Date(), url, userName, event.getCause().getMessage());
    }

    /**
     * 备注:当使用并发产生消息时下列日志监听时间无法触发
     **/

    @EventListener(MqttMessageSentEvent.class)
    public void mqttMessageSentEvent(MqttMessageSentEvent event) {
        log.info("mqttMessageSentEvent发送信息: date={}, info={}", new Date(), event.toString());
    }

    @EventListener(MqttMessageDeliveredEvent.class)
    public void mqttMessageDeliveredEvent(MqttMessageDeliveredEvent event) {
        //log.info("mqttMessageDeliveredEvent发送成功信息: date={}, info={}", new Date(), event.toString());
    }

    @EventListener(MqttSubscribedEvent.class)
    public void mqttSubscribedEvent(MqttSubscribedEvent event) {
        log.info("mqttSubscribedEvent订阅成功信息: date={}, info={}", new Date(), event.toString());
    }


}

代码中包含了两个消息推送方案,根据并发量进行选择。(这里发送的荷载主要是字节数组数据,本文会提供压缩数据的工具类)

3.2.自定义消息处理器

package com.zbwd.esms.edge.mqtt.handler;

import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.messaging.Message;

/**
 * mqtt并发生产消息优化配置项
 *
 *
 * @author : mwx
 * @date : 2023/1/13 11:08
 */

public class MyMqttPahoMessageHandler extends MqttPahoMessageHandler {

    public MyMqttPahoMessageHandler(String url, String clientId, MqttPahoClientFactory clientFactory) {
        super(url, clientId, clientFactory);
    }

    @Override
    public void doStop() {
        super.doStop();
    }

    @Override
    public void handleMessageInternal(Message<?> message) throws Exception {
        super.handleMessageInternal(message);
    }

    @Override
    public void onInit() {
        try {
            super.onInit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

3.3.自定义消息转发器

package com.zbwd.esms.edge.mqtt.handler;

import com.zbwd.esms.edge.config.MqttConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.Lifecycle;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * mqtt并发生产消息优化项配置
 *
 * <p>通知实例化多个消息处理器并随机取实现负载均衡产生消息</p>
 *
 * @author : mwx
 * @date : 2023/1/13 11:11
 */
@Component
@Slf4j
public class MultiMqttMessageHandler extends AbstractMessageHandler implements Lifecycle {
    private final AtomicBoolean running = new AtomicBoolean();

    private volatile Map<Integer, MessageHandler> mqttHandlerMap;

    /**
     * 模拟3个Client端
     */
    private Integer handlerCount = 3;

    @Autowired
    private MqttConfig senderConfig;

    @Override
    public void start() {
        if (!this.running.getAndSet(true)) {
            doStart();
        }
    }

    private void doStart() {
        mqttHandlerMap = new ConcurrentHashMap<>();
        for (int i = 0; i < handlerCount; i++) {
            mqttHandlerMap.put(i, senderConfig.create());
        }
    }

    @Override
    public void stop() {
        if (this.running.getAndSet(false)) {
            doStop();
        }
    }

    private void doStop() {
        for (Map.Entry<Integer, MessageHandler> e : mqttHandlerMap.entrySet()) {
            MessageHandler handler = e.getValue();
            ((MyMqttPahoMessageHandler) handler).doStop();
        }
    }

    @Override
    public boolean isRunning() {
        return this.running.get();
    }

    @Override
    protected void handleMessageInternal(Message<?> message) throws Exception {
        //负载均衡
        Random random = new Random();
        MyMqttPahoMessageHandler messageHandler = (MyMqttPahoMessageHandler) mqttHandlerMap.get(random.nextInt(handlerCount));
        log.info("开始处理信息:{}", message.toString());
        messageHandler.handleMessageInternal(message);
    }

}

3.4.消息推送Component

package com.zbwd.esms.edge.component;

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;

/**
 * mqtt发送网关
 *
 * @author : mwx
 * @date : 2022/12/28 14:02
 */
@Component
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttGatewayComponent {

    /**
     * 发送消息至指定topic
     * <p color = red>此方法不推荐使用!!发送消息时请先调用GzipUtil内的数据压缩方法发送字节数组</p>
     *
     * @param payload 消息内容
     * @param topic   topic
     * @see com.zbwd.esms.edge.util.GzipUtil
     */
    @Deprecated
    void sendToMqtt(String payload, @Header(MqttHeaders.TOPIC) String topic);

    /**
     * 发送使用Gzip压缩后的字节数组
     *
     * <p color = green>推荐使用此方法!!发送消息时请先调用GzipUtil内的数据压缩方法发送字节数组</p>
     *
     * @param payload 消息内容
     * @param topic   topic
     * @see com.zbwd.esms.edge.util.GzipUtil
     */
    void sendToMqtt(byte[] payload, @Header(MqttHeaders.TOPIC) String topic);

    /**
     * 发送消息至指定topic 能指定qos
     *
     * @param payload 消息内容
     * @param topic   topic
     * @param qos     值为 0 1 2
     */
    void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) int qos, String payload);
}

4.消息消费者配置

4.1.核心配置

package com.xaaef.shark.mqtt.config;

import com.xaaef.shark.mqtt.enums.TopicEnum;
import com.xaaef.shark.mqtt.handler.MqttMessageHandler;
import com.xaaef.shark.util.SnowFlakeUtil;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.internal.wire.MqttConnect;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent;
import org.springframework.integration.mqtt.event.MqttMessageSentEvent;
import org.springframework.integration.mqtt.event.MqttSubscribedEvent;
import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;
import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler;
import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;

import javax.annotation.Resource;
import java.util.Date;

/**
 * mqtt连接信息
 *
 * @author : mwx
 * @date : 2022/12/28 13:39
 */
@Slf4j
@Configuration
@IntegrationComponentScan
public class MqttConfig {

    @Value("${spring.mqtt.url}")
    private String url;

    @Value("${spring.mqtt.clientId}")
    private String clientId;

    @Value("${spring.mqtt.username}")
    private String userName;

    @Value("${spring.mqtt.password}")
    private String passWord;

    @Value("${spring.mqtt.completion-timeout}")
    private int completionTimeout;

    @Value("${spring.mqtt.defaultTopic}")
    private String defaultTopic;

    private SnowFlakeUtil snowFlakeUtil = new SnowFlakeUtil(1, 0);

    @Resource
    private MqttMessageHandler mqttMessageHandler;


    @Bean
    public MqttConnectOptions getMqttConnectOptions() {
        // MQTT的连接设置
        MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
        // 设置连接的用户名
        mqttConnectOptions.setUserName(userName);
        // 设置连接的密码
        mqttConnectOptions.setPassword(passWord.toCharArray());
        // 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,
        // 把配置里的 cleanSession 设为false,客户端掉线后 服务器端不会清除session,
        // 当重连后可以接收之前订阅主题的消息。当客户端上线后会接受到它离线的这段时间的消息
        mqttConnectOptions.setCleanSession(false);
        // 设置发布端地址,多个用逗号分隔, 如:tcp://111:1883,tcp://222:1883
        // 当第一个111连接上后,222不会在连,如果111挂掉后,重试连111几次失败后,会自动去连接222
        String[] urls = new String[]{url};
        mqttConnectOptions.setServerURIs(urls);
        // 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
        mqttConnectOptions.setKeepAliveInterval(20);
        //该属性设置后能完成重连但是要另外实现订阅
        //经过对topic的测试,发现只注册到default时也能接收到消息 是因为对入站消息进行了处理(消息先入站再获取topic进行处理)
        mqttConnectOptions.setAutomaticReconnect(true);
        // 设置“遗嘱”消息的话题,若客户端与服务器之间的连接意外中断,服务器将发布客户端的“遗嘱”消息。
        //mqttConnectOptions.setWill(willTopic, willContent.getBytes(), 2, false);
        mqttConnectOptions.setMaxInflight(1000000);
        return mqttConnectOptions;
    }

    @Bean
    public MqttPahoClientFactory mqttClientFactory() {
        DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
        factory.setConnectionOptions(getMqttConnectOptions());
        return factory;
    }

    @Bean
    @ServiceActivator(inputChannel = "mqttOutboundChannel")
    public MessageHandler mqttOutbound() {
        //clientId每个连接必须唯一,否则,两个相同的clientId相互挤掉线
        String clientIdStr = clientId + snowFlakeUtil.nextId();
        MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler(clientIdStr, mqttClientFactory());
        //async如果为true,则调用方不会阻塞。而是在发送消息时等待传递确认。默认值为false(发送将阻塞,直到确认发送)
        messageHandler.setAsync(true);
        messageHandler.setAsyncEvents(true);
        messageHandler.setDefaultTopic(defaultTopic);
        messageHandler.setDefaultQos(0);
        //发送默认按字节类型发送消息
        // defaultPahoMessageConverter.setPayloadAsBytes(true);
        return messageHandler;
    }

    /**
     * 发消息通道
     */
    @Bean
    public MessageChannel mqttOutboundChannel() {
        return new DirectChannel();
    }

    /**
     * 接收消息通道
     */
    @Bean
    public MessageChannel mqttInputChannel() {
        return new DirectChannel();
    }

    @Bean
    public MessageProducer inbound() {
        //clientId每个连接必须唯一,否则,两个相同的clientId相互挤掉线
        String serverIdStr = clientId + snowFlakeUtil.nextId();
        //动态设置adapter的的topic值
        String[] allTopics = TopicEnum.getAllValues();
        MqttPahoMessageDrivenChannelAdapter adapter =
                new MqttPahoMessageDrivenChannelAdapter(serverIdStr, mqttClientFactory(), allTopics);
        adapter.setCompletionTimeout(completionTimeout);
        DefaultPahoMessageConverter defaultPahoMessageConverter = new DefaultPahoMessageConverter();
        //接收字节数组类型有效荷载
        defaultPahoMessageConverter.setPayloadAsBytes(true);
        adapter.setConverter(defaultPahoMessageConverter);
        adapter.setQos(2);
        adapter.setOutputChannel(mqttInputChannel());
        return adapter;
    }

    /**
     * 通过通道获取数据 订阅的数据
     */
    @Bean
    @ServiceActivator(inputChannel = "mqttInputChannel")
    public MessageHandler handler() {
        return message -> {
            //接收到的消息由这个处理器进行处理
            mqttMessageHandler.handleMessage(message);
        };
    }

    @EventListener(MqttConnectionFailedEvent.class)
    public void mqttConnectionFailedEvent(MqttConnectionFailedEvent event) {
        log.error("mqttConnectionFailedEvent连接mqtt失败: " +
                        "date={}, hostUrl={}, username={}, error={}",
                new Date(), url, userName, event.getCause().getMessage());
    }

    @EventListener(MqttConnect.class)
    public void reSubscribed() {

    }

    @EventListener(MqttMessageSentEvent.class)
    public void mqttMessageSentEvent(MqttMessageSentEvent event) {
        log.info("mqttMessageSentEvent发送信息: date={}, info={}", new Date(), event.toString());
    }

//    @EventListener(MqttMessageDeliveredEvent.class)
//    public void mqttMessageDeliveredEvent(MqttMessageDeliveredEvent event) {
//        //log.info("mqttMessageDeliveredEvent发送成功信息: date={}, info={}", new Date(), event.toString());
//    }

    @EventListener(MqttSubscribedEvent.class)
    public void mqttSubscribedEvent(MqttSubscribedEvent event) {
        log.info("mqttSubscribedEvent订阅成功信息: date={}, info={}", new Date(), event.toString());
    }
}

4.2消息处理器

package com.xaaef.shark.mqtt.handler;


import cn.hutool.core.util.ObjectUtil;
import com.xaaef.shark.mqtt.enums.TopicEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * mqtt topic中转
 *
 * @author : mwx
 * @date : 2022/12/28 15:02
 */
@Slf4j
@Component
public class MqttMessageHandler implements MessageHandler {

    @Resource
    private  MqttTopicHandler topicHandler;


    /**
     *
     *消息会在这进行对topic的处理
     * <p>重连后</p>
     * */
    @Override
    public void handleMessage(Message<?> message) throws MessagingException {
        //从这里来对消息进行分topic处理
        String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC).toString();
        boolean exist = TopicEnum.isExist(topic);
        if (exist == false) {
            log.info("当前topic:{}未注册!不做处理!",topic);
            return;
        }
        //MqttTopicHandler topicHandler = SpringUtil.getBean(mqttTopicEnum.getTopicHandlerBeanName(), TopicHandler.class);
        if (ObjectUtil.isNull(topicHandler)) {
            log.warn("消息处理器未被实现,无法消费消息!");
            return;
        }
        // topic消费处理
        topicHandler.handler(message);
    }
}

4.3.根据Topic进行分类处理Handler

package com.xaaef.shark.mqtt.handler;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import com.xaaef.shark.entity.Heap;
import com.xaaef.shark.service.AlgorithmModelService;
import com.xaaef.shark.service.EmsService;
import com.xaaef.shark.service.SysHeapService;
import com.xaaef.shark.util.GzipUtil;
import com.xaaef.shark.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 消息处理器
 *
 * @author : mwx
 * @date : 2022/12/28 15:00
 */
@Slf4j
@Service
public class MqttTopicHandler {

    @Autowired
    private SysHeapService sysHeapService;

    @Autowired
    private AlgorithmModelService algorithmModelService;

    @Autowired
    private EmsService emsService;

    /**
     * 这一步主要是对消息的有效荷载进行非空校验以及日志打印
     */
    public void handler(Message<?> message) {
        String topic = message.getHeaders().get(MqttHeaders.RECEIVED_TOPIC).toString();
        log.info("topic:{}-->message received!", topic);
        Object payload = message.getPayload();
        Assert.notNull(payload, "接收到的消息为空!");
        byte[] data = (byte[]) payload;
        dispose(data, topic);
    }

    /**
     * 对mqtt传来的消息有效荷载进行数据处理
     * <p color = yellow>这里的字节数据是Gzip压缩后的字节数组,需要使用GzipUtil进行解析</p>
     *
     * @see GzipUtil
     */
    private void dispose(byte[] payload, String topic) {
        String strDecompress = GzipUtil.strDecompress(payload);
       if (!ObjectUtil.isNull(strDecompress)){
           try {
               switch (topic) {
                   //基础数据 以及 重传数据
                   case "base":
                       Heap baseHeap = JsonUtils.jsonToPojo(strDecompress, Heap.class);
                       //若接收到的base数据该属性为空则为实时数据
                       Integer flag = Optional.ofNullable(baseHeap.getDataUpload()).orElse(1);
                       if (flag == 1) {
                           sysHeapService.insertHeapData(baseHeap);
                       } else if (flag == 0) {
                           sysHeapService.reuploadHeapData(baseHeap);
                       } else {

                       }
                       break;
                   //安全模型数据
                   case "safety":
                       List<Map<String, Object>> safetyData = JsonUtils.jsonToListMap(strDecompress, String.class, Object.class);
                       algorithmModelService.insertSafetyResultData(safetyData);
                       break;
                   //健康寿命模型算法数据
                   case "rul":
                       List<Map<String, Object>> rulData = JsonUtils.jsonToListMap(strDecompress, String.class, Object.class);
                       algorithmModelService.insertRulResultData(rulData);
                       break;
                   //遥测数据
                   case "yc":
                       Map<String, Object> ycData = JsonUtils.jsonToMap(strDecompress, String.class, Object.class);
                       emsService.saveEms(ycData);
                       break;
                   //遥信数据
                   case "yx":
                       Map<String, Object> yxData = JsonUtils.jsonToMap(strDecompress, String.class, Object.class);
                       emsService.saveYxInfo(yxData);
                       break;
                   default:
                       log.warn("未知topic:{}的消息,请检查您的配置!", topic);
                       break;
               }
               log.info("来自topic:{}的消息处理完毕!", topic);
           } catch (Exception e) {
               log.error("来自topic:{}的消息处理异常!原因:{}", topic, e);
           }
       }

    }

}

5.公共类

5.1.Topic枚举

package com.xaaef.shark.mqtt.enums;

import java.util.ArrayList;
import java.util.List;

/**
 * mqtt消息发送topic枚举
 * <p color = red>注意:所有在mqttServer上面需要用到的topic都要在这里有值,否则绑定不了topic接收不到消息</p>
 *
 * @author : mwx
 * @date : 2022/12/28 14:18
 */
public enum TopicEnum {

    /**
     * 基础数据 包含了重传数据
     */
    BASE("base"),
    /**
     * 安全模型算法数据
     */
    SAFETY("safety"),
    /**
     * 健康寿命模型算法数据
     */
    RUL("rul"),

    /**
     * 遥信.
     */
    YX("yx"),

    /**
     * 遥测.
     */
    YC("yc"),

    /**
     * 测试分支用
     */
    TEST("test");

    private String topic;

    public String getTopic() {
        return topic;
    }

    TopicEnum(String topic) {
        this.topic = topic;
    }

    public static boolean isExist(String topic) {
        for (TopicEnum topicEnum : TopicEnum.values()) {
            if (topicEnum.getTopic().equalsIgnoreCase(topic)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取该枚举类的所有值
     */
    public static String[] getAllValues() {
        List<String> values = new ArrayList<>();
        for (TopicEnum topicEnum : TopicEnum.values()) {
            values.add(topicEnum.getTopic());
        }
        return values.toArray(new String[values.size()]);
    }
}

5.2.数据压缩工具

package com.xaaef.shark.util;

import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * 压缩工具类
 *
 * @author : mwx
 * @date : 2023/1/12 16:32
 */
@Slf4j
public class GzipUtil {

    private final static String CHARACTER_PROTOCOL = "ISO-8859-1";

    /**
     * 字符串压缩
     *
     * <p>实测长度为24000的字符串能压缩到9000左右</p>
     *
     * @param str 压缩的字符串
     * @return byte[] 压缩后的字节数组
     * @author mwx
     */
    public static byte[] StrCompress(String str) {
        if (StringUtils.isNotBlank(str)) {
            ByteArrayOutputStream byteArrayOutputStream = null;
            GZIPOutputStream gzipOutputStream = null;
            try {
                byteArrayOutputStream = new ByteArrayOutputStream();
                gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
                gzipOutputStream.write(str.getBytes(CHARACTER_PROTOCOL));
                gzipOutputStream.finish();
                return byteArrayOutputStream.toByteArray();
            } catch (Exception e) {
                log.error("【异常-GzipUtil】字符串压缩(StrCompress)发生异常:{}", e.getMessage(), e);
                return null;
            } finally {
                Close(byteArrayOutputStream, gzipOutputStream);
            }
        } else {
            return null;
        }

    }

    /**
     * 字符串解压缩
     *
     * @param target 解压缩的字节数组
     * @return String 解压缩后的字符串
     * @author mwx
     */
    public static String strDecompress(byte[] target) {
        if (!ObjectUtil.isNull(target)) {
            ByteArrayOutputStream byteArrayOutputStream = null;
            GZIPInputStream gzipInputStream = null;
            try {
                byteArrayOutputStream = new ByteArrayOutputStream();
                gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(target));
                byte[] res = new byte[10240];
                int n;
                while ((n = gzipInputStream.read(res)) > -1) {
                    byteArrayOutputStream.write(res, 0, n);
                }
                byteArrayOutputStream.flush();
                return new String(byteArrayOutputStream.toByteArray(), CHARACTER_PROTOCOL);
            } catch (Exception e) {
                log.error("【异常-GzipUtil】字符串解压缩(strDecompress)发生异常:{}", e.getMessage(), e);
                return null;
            } finally {
                Close(byteArrayOutputStream, gzipInputStream);
            }
        } else {
            return null;
        }
    }

    /**
     * 关闭流方法
     *
     * @param stream 关闭流的集合
     */
    private static void Close(Closeable... stream) {
        try {
            for (Closeable closeable : stream) {
                if (ObjectUtil.isNotNull(closeable)) {
                    closeable.close();
                }
            }
            log.info("【GzipUtil】关闭流成功!");
        } catch (Exception e) {
            log.error("【异常-GzipUtil】流关闭失败!原因:{}", e.getMessage(), e);
        }
    }
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值