SpringBoot对接物联网设备的设计思路和SpringBoot框架中实现MQTT多个主题订阅和发布

1,183 阅读11分钟

需求:

在开发管理系统时,需要在项目中 控制一些物联网设备,因为我们开发的是管理系统,所以在管理设备的时候 需要有通用性和扩展性!

比如 空调设备 A客户用的是 美的品牌 B客户用的是 格力 ,过两天又来一个供应商,我们怎么扩展

这个时候 在页面上控制这些设备的时候 就需要 根据 设备品牌的不同 调用不同的 实现类 来操作 空调

不止空调 比如 断路器、 门禁 等等 供应商不同 控制他们的设备方法不同、 参数不同、 协议也有可能不同

但是经过我的观察 我发现 :

比如 空调设备 虽然空调设备 的供应商不一样,但是空调设备 提供的功能 一般都是一样的 只有个别的有区别 :开关,调节风速、模式 基本的设备功能

那别的 设备 肯定也有这种特性

利用这种特性 我觉得觉得可以给我们对接的设备 定义一个 接口类

下面已断路器为例子 协议是MQTT 协议 分享一下我的 设计思路 利用工厂模式 根据品牌不同,调用不同的实现类 框架是 springboot

mqtt 依赖用的是 :

      <!--mqtt相关依赖-->
        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-mqtt</artifactId>
            <version>5.5.14</version>

        </dependency>

第一步 :定义接口类 接口类 主要定义 我们的管理系统里需要实现的功能
IBreakerCommandService

package com.testweb.testweb.mqtt.web.third.breaker;



import com.testweb.testweb.mqtt.web.request.BreakerDataRequest;
import org.eclipse.paho.client.mqttv3.MqttMessage;

/**
 * User:Json
 * Date: 2024/6/18
 * 断路器 下发命令
 **/
public interface IBreakerCommandService {

    //下发命令 定时任务自动执行的命令
    boolean commandBreaker(BreakerDataRequest breakerDataRequest, boolean isAuto, boolean isLog);

    //下发命令 手动执行
    boolean commandBreaker(BreakerDataRequest breakerDataRequest, boolean isLog);

    //批量下达命令 手动执行
    boolean commandBreakerBatch(BreakerDataRequest breakerDataRequest);

    //给网关下发上报时间
    boolean commandGatewayTimeReporting(String gatewayCode, int minute);

    //断路器回调
    void breakerCallback(String topic, MqttMessage mqttMessage);


}

第二步:
断路器 A供应商来了 根据A供应商提供的参数 支持的协议 来实现这些接口

package com.testweb.testweb.mqtt.web.third.breaker.kaiyue.impl;



import com.testweb.testweb.mqtt.web.controller.MqttTestController;

import com.testweb.testweb.mqtt.web.request.BreakerDataRequest;
import com.testweb.testweb.mqtt.web.third.breaker.IBreakerCommandService;

import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;



/**
 * User:Json
 * Date: 2024/6/18
 * 悦悦一体机 控制断路器
 * 要把他交给 springboot 管理 方便后续 对数据库进行操作
 **/
@Slf4j
@Component
public class KaiYueCommandServiceImpl implements IBreakerCommandService {



    @Override
    public boolean commandBreaker(BreakerDataRequest breakerDataRequest, boolean isAuto , boolean isLog) {
           System.out.println("我执行的是悦悦设备的命令");
            return true;
    }

    @Override
    public boolean commandBreaker(BreakerDataRequest breakerDataRequest, boolean isLog) {
        return commandBreaker(breakerDataRequest, false, isLog);
    }

    //写入日志 isLog 是否写入
    private void writeLog(BreakerDataRequest breakerDataRequest, Integer orgId, Integer userId, String username, boolean isLog) {
        if (isLog) {

        }

    }

    @Override
    public boolean commandGatewayTimeReporting(String gatewayCode, int minute) {
        BreakerDataRequest breakerDataRequest = new BreakerDataRequest();
        return commandBreaker(breakerDataRequest, false);
    }

    @Override
    public void breakerCallback(String topic, MqttMessage mqttMessage) {
        String payload = new String(mqttMessage.getPayload());
        System.out.println("我收到悦悦设备的消息了:"+ payload);
    }

    @Override
    public boolean commandBreakerBatch(BreakerDataRequest breakerDataRequest) {


        return false;
    }



    //验证远程操作指令
    private boolean checkingCommandData(BreakerDataRequest breakerDataRequest) {


            return true;
    }

    //拼装数据
    private String getCommandData(BreakerDataRequest breakerDataRequest, String type) {

        return null;
    }


}

断路器 B供应商来了 根据B供应商提供的参数 支持的协议 来实现这些接口

package com.testweb.testweb.mqtt.web.third.breaker.xbl.impl;

import com.testweb.testweb.files.web.entities.FileDetail;
import com.testweb.testweb.files.web.service.IFileDetailService;
import com.testweb.testweb.mqtt.web.request.BreakerDataRequest;
import com.testweb.testweb.mqtt.web.third.breaker.IBreakerCommandService;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * User:Json
 * Date: 2024/8/5
 * 小白龙品牌 控制断路器
 **/
@Slf4j
@Component
public class XblCommandServiceImpl implements IBreakerCommandService {

    @Autowired
    IFileDetailService iFileDetailService;

    @Override
    public boolean commandBreaker(BreakerDataRequest breakerDataRequest, boolean isAuto, boolean isLog) {
        List<FileDetail> list = iFileDetailService.list();
        list.forEach(item->{
            System.out.println("我是Id:"+item.getId());
        });
        System.out.println("我执行的是小白龙设备的命令");
        return true;
    }

    @Override
    public boolean commandBreaker(BreakerDataRequest breakerDataRequest, boolean isLog) {
        return false;
    }

    @Override
    public boolean commandBreakerBatch(BreakerDataRequest breakerDataRequest) {
        return false;
    }

    @Override
    public boolean commandGatewayTimeReporting(String gatewayCode, int minute) {
        return false;
    }

    @Override
    public void breakerCallback(String topic, MqttMessage mqttMessage) {
        String payload = new String(mqttMessage.getPayload());
        System.out.println("我收到小白龙设备的消息了:"+ payload);
    }
}

在各自供应商的实现类里 具体实现 如果都用的MQTT 那就都走MQTT 参数不同,如果一个是MQTT 一个是http:api的形式 那直接在不同的实现类里分别实现即可

第三步:
定义一个 断路器的工厂类 根据设备品牌不同 去找不同的服务

package com.testweb.testweb.mqtt.web.third.breaker;



import com.testweb.testweb.mqtt.web.config.MqttConfig;
import com.testweb.testweb.mqtt.web.third.breaker.kaiyue.impl.KaiYueCommandServiceImpl;
import com.testweb.testweb.mqtt.web.third.breaker.xbl.impl.XblCommandServiceImpl;
import com.testweb.testweb.mqtt.web.utils.AppContextUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * User:Json
 * Date: 2024/6/20
 * 后续有不同的断路器供应商 调用方式不同的话 可在此扩展
 * 实现 IBreakerCommandService 接口即可
 **/
@Slf4j
public class BreakerFactory {

    public static final String kaiyue = "kaiyue";

    public static final String xbl = "xbl";

    public static IBreakerCommandService getBreakerCommandService(String brand) {
        switch (brand.toLowerCase()) {
            case kaiyue:
                return AppContextUtil.getBean(KaiYueCommandServiceImpl.class);
            case xbl:
                return AppContextUtil.getBean(XblCommandServiceImpl.class);
            default:
                log.warn("未找到相对应的第三方断路器!启动了默认悦悦的断路器!");
                return AppContextUtil.getBean(KaiYueCommandServiceImpl.class);
        }
    }
}

AppContextUtil 工具类

package com.testweb.testweb.mqtt.web.utils;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * User:Json
 * Date: 2024/8/5
 **/
@Component
public class AppContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

    /**
     * 获取静态变量中的ApplicationContext.
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 从静态变量applicationContext中得到Bean, 自动转型为所赋值对象的类型.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) {
        return (T) applicationContext.getBean(name);
    }

    /**
     * 从静态变量applicationContext中得到Bean, 自动转型为所赋值对象的类型.
     */
    public static <T> T getBean(Class<T> requiredType) {
        return applicationContext.getBean(requiredType);
    }

    /**
     * 清除SpringContextHolder中的ApplicationContext为Null.
     */
    public static void clearHolder() {
        applicationContext = null;
    }

    /**
     * 实现ApplicationContextAware接口, 注入Context到静态变量中.
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }


}

测试 比如 页面上有2个断路器设备 一个是A厂商 一个是B厂商 点击页面按钮 开关 后 触发后端一个接口 我们就可以在这个接口里 根据供应商类型不同 来调用不同的实现类

   //控制器测试
    @GetMapping("command")
    @CrossOrigin(origins = "*")
    public void command(@RequestParam("type") String type){
        IBreakerCommandService breakerCommandService = BreakerFactory.getBreakerCommandService(type);
        breakerCommandService.commandBreaker(new BreakerDataRequest(),false,true);
    }

以后再来一个C供应商,我们就再实现 IBreakerCommandService 接口 然后再工厂类里加一个类型 即可 我觉得这样会减少很多 if 判断 以上代码 只是我想象一个设计思路 具体方法里的实现 根据业务来

最后再分享一下MQtt协议的实现方式: 我们是 一个项目 只会连接一个MQTT服务端 不会连接多个 但是 项目会订阅多个 MQTT主题 所以也会 有多个 主题回调监听。

以前 我分享过一篇 MQTT的文章 但是写法是 只订阅了一个主题 一个回调地址 jsonll.blog.csdn.net/article/det… 下面是我对MQTT进行了扩展 分享一下 订阅多个 MQTT主题 多个回调地址的处理方案

配置文件

# mqtt
mqtt:
  mqttUrl: tcp://127.0.0.1
  mqttPort: 1883
  mqttUsername: admin
  mqttPassword: public

MqttConfig 配置类

package com.testweb.testweb.mqtt.web.config;


import com.testweb.testweb.mqtt.web.callback.PublicCallback;
import lombok.extern.slf4j.Slf4j;

import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import javax.annotation.PreDestroy;


/**
 * User:Json
 * Date: 2024/6/17
 **/
@Configuration
@Slf4j
public class MqttConfig {

    @Value("${mqtt.mqttUsername:}")
    private String mqttUsername;

    @Value("${mqtt.mqttPassword:}")
    private String mqttPassword;

    @Value("${mqtt.mqttUrl:}")
    private String mqttUrl;

    @Value("${mqtt.mqttPort:}")
    private Integer mqttPort;



    //客户端id
    private String mqttClientId = "work-iot-java-switch";




    /**
     * 客户端对象
     */
    private MqttClient client;


    /**
     * 客户端连接服务端
     * 目前只支持一个 MQTT服务端
     */
    public boolean connect() {
        if (isMqtt()) {
            return false;
        }
        try {
            // 检查客户端是否已经连接
            if (client != null && client.isConnected()) {
                log.info("MQTT客户端已连接");
                return true;
            }
            //new MemoryPersistence() 使用内存持久化
            // 优点:不会在文件系统中创建任何文件(如 .lck 文件),适合对会话持久性没有要求的场景。
            // 缺点:  客户端断开连接或重启后,会话数据会丢失,无法保留订阅信息和未发送的消息

            // String persistenceDirectory = "/path/to/your/mqtt/persistence";
            //new MqttDefaultFilePersistence(persistenceDirectory) 使用文件持久化
            //如果persistenceDirectory 不写 他默认创建 根目录 linux要给权限
            // 优点: 客户端断开连接或重启后,能够保留订阅信息和未发送的消息。这对于需要保持会话状态的应用非常重要
            // 缺点 会在指定的目录中创建文件(如 .lck 文件),需要确保指定的目录是有效的,并且应用有权限访问该目录

            //创建MQTT客户端对象
            client = new MqttClient(mqttUrl + ":" + mqttPort, mqttClientId, new MemoryPersistence());
            //连接设置
            MqttConnectOptions options = new MqttConnectOptions();
            //是否清空session,设置false表示服务器会保留客户端的连接记录(订阅主题,qos),客户端重连之后能获取到服务器在客户端断开连接期间推送的消息
            //设置为true表示每次连接服务器都是以新的身份
            options.setCleanSession(false);
            //设置连接用户名
            options.setUserName(mqttUsername);
            //设置连接密码
            options.setPassword(mqttPassword.toCharArray());


            options.setAutomaticReconnect(true);  // 启用自动重连
            //设置超时时间,单位为秒  如果在指定的时间内未能建立连接,客户端会放弃连接尝试并抛出异常。
            options.setConnectionTimeout(100);
            //设置心跳时间 单位为秒,表示服务器每隔 1.5*20秒的时间向客户端发送心跳判断客户端是否在线
            options.setKeepAliveInterval(20);
            //设置遗嘱消息的话题,若客户端和服务器之间的连接意外断开,服务器将发布客户端的遗嘱信息
            //  options.setWill("willTopic",(mqttClientId + ":与服务器断开连接").getBytes(),0,false);


            //配置公共的回调地址用于处理全局异常
             client.setCallback(new PublicCallback());

            client.connect(options);
            return true;
        } catch (MqttException e) {
            log.error("MQTT启动报错:" + e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

    /**
     * qos
     * 0  最多一次传递【适用于对消息丢失不敏感的场景,如传感器数据频繁发送,可以接受偶尔的数据丢失】
     * 1 至少一次传递  【消息至少传递一次,但可能会重复(即重复消息)】
     * 2 仅一次传递 【消息确保仅传递一次,既不会丢失也不会重复。】
     * retained
     * 保留消息:如果 retained 参数设置为 true,消息会被代理保留。代理将记住这个消息,并在新客户端订阅该主题时立即发送这个消息。
     * 非保留消息:如果 retained 参数设置为 false,消息不会被保留,只会发送给当前在线并订阅该主题的客户端。
     * topic    主题
     * message  内容
     */
    public boolean publish(int qos, boolean retained, String topic, String message) {
        if (isMqtt()) {
            return false;
        }
        log.info("topic为:【" + topic + "】,qos为:【" + qos + "】 mqtt 发布数据为:" + message);

        MqttMessage mqttMessage = new MqttMessage();
        mqttMessage.setQos(qos);
        mqttMessage.setRetained(retained); //代理将记住这个消息,并在新客户端订阅该主题时立即发送这个消息。
        mqttMessage.setPayload(message.getBytes());
        //主题的目的地,用于发布信息
        MqttTopic mqttTopic = client.getTopic(topic);

        MqttDeliveryToken token;
        try {
            //将指定消息发布到主题,但不等待消息传递完成,返回的token可用于跟踪消息的传递状态
            token = mqttTopic.publish(mqttMessage);
            //token.waitForCompletion(); // 等待完成 会堵塞
            return true;
        } catch (MqttException e) {
            log.warn("ClientId【" + mqttClientId + "】发布失败!主题【" + topic + "】,发布数据为:" + message);
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 断开连接
     */
    public void disConnect() {
        try {
           if(client!=null && client.isConnected()){
               client.disconnect();
           }
        } catch (MqttException e) {
            log.error("MQTT断开连接失败: {}", e.getMessage());
            e.printStackTrace();
        }
    }

    /***
     *  手动连接
     *  可用于断线后 手动重连
     * ***/
    public boolean againConnect() {
        try {
            if (client != null && !client.isConnected()) {
                connect();
            }
            return true;
        } catch (Exception e) {
            log.error("MQTT重连失败: {}", e.getMessage());
            return false;
        }
    }

    //验证是否启动mqtt连接
    private boolean isMqtt() {
        if (StringUtils.isEmpty(mqttUrl) || StringUtils.isEmpty(mqttPort)
                || StringUtils.isEmpty(mqttUsername) || StringUtils.isEmpty(mqttPassword)
                || StringUtils.isEmpty(mqttClientId)
        ) {
            log.info("==========mqtt 参数不全,无需启动MQTT连接==================");
            return true;
        }
        return false;
    }


    /**
     * 订阅指定主题
     *
     * @param topic 订阅的主题
     * @param qos   订阅的服务质量
     */
    public boolean subscribe(String topic, int qos,IMqttMessageListener listener) {
        if (isMqtt()) {
            return false;
        }
        try {
            if (client != null && client.isConnected()) {
                client.subscribe(topic, qos,listener);

                log.info("订阅主题 {} 成功!", topic);
            } else {
                log.error("MQTT客户端尚未连接,无法订阅主题 {}!", topic);
            }
            return true;
        } catch (MqttException e) {
            log.error("订阅主题 {} 失败:{}", topic, e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 批量订阅主题
     * 消息等级,和主题数组一一对应,服务端将按照指定等级给订阅了主题的客户端推送消息
     *
     * @param topic 订阅的主题集合
     * @param qos   订阅的服务质量集合
     */
    public boolean subscribe(String[] topic, int[] qos) {
        if (isMqtt()) {
            return false;
        }
        try {
            if (client != null && client.isConnected()) {
                client.subscribe(topic, qos);
                log.info("订阅主题 {} 成功!", topic);
            } else {
                log.error("MQTT客户端尚未连接,无法订阅主题 {}!", topic);
            }
            return true;
        } catch (MqttException e) {
            log.error("订阅主题 {} 失败:{}", topic, e.getMessage());
            e.printStackTrace();
            return false;
        }
    }

    @PreDestroy
    public void cleanUp() {
        log.info("关闭MQTT连接!");
        disConnect();
    }
}

公共回调类

package com.testweb.testweb.mqtt.web.callback;

import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttAsyncClient;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;


/**
 * User:Json
 * Date: 2024/8/5
 **/
@Slf4j
public class PublicCallback implements MqttCallback {


    /**
     * 与服务器断开的回调
     */
    @Override
    public void connectionLost(Throwable throwable) {
        //throwable.printStackTrace();
        log.error("MQTT连接有异常:" + throwable.getMessage());
    }

    /**
     * 订阅的回调
     * 消息到达的回调
     */
    @Override
    public void messageArrived(String topic, MqttMessage mqttMessage) {
        // 这里不再处理消息,交给各个主题的 IMqttMessageListener 处理
    }


    /**
     * 发布的回调
     * 消息发布成功的回调
     */
    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
        IMqttAsyncClient client = token.getClient();
        log.info(client.getClientId() + "发布消息成功!");
    }


}

各个主题 回调类

package com.testweb.testweb.mqtt.web.callback.breaker;



import com.testweb.testweb.mqtt.web.third.breaker.IBreakerCommandService;
import com.testweb.testweb.mqtt.web.third.breaker.kaiyue.impl.KaiYueCommandServiceImpl;
import com.testweb.testweb.mqtt.web.utils.AppContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;

import java.time.LocalDateTime;


/**
 * User:Json
 * Date: 2024/6/17
 **/
@Slf4j
public class BreakerCallback implements IMqttMessageListener {

    private IBreakerCommandService iBreakerCommandService;

    public IBreakerCommandService getIBreakerCommandService() {
        if (iBreakerCommandService == null){
            iBreakerCommandService = AppContextUtil.getBean(KaiYueCommandServiceImpl.class);
        }
        return iBreakerCommandService;
    }

    /**
     * 订阅的回调
     * 消息到达的回调
     */
    @Override
    public void messageArrived(String topic, MqttMessage mqttMessage) {
        System.out.println("悦悦 上报时间:" + LocalDateTime.now());
        System.out.println(String.format("接收消息主题 : %s", topic));
        System.out.println(String.format("接收消息Qos : %d", mqttMessage.getQos()));
        System.out.println(String.format("接收消息内容 : %s", new String(mqttMessage.getPayload())));
        System.out.println(String.format("接收消息retained : %b", mqttMessage.isRetained()));
        try {
            getIBreakerCommandService().breakerCallback(topic, mqttMessage);
        } catch (Exception e) {
            log.warn("MQTT消息处理异常:" + e.getMessage());
        }
    }

}

MqttUtils 工具类

package com.testweb.testweb.mqtt.web.utils;


import com.testweb.testweb.mqtt.web.callback.breaker.BreakerCallback;
import com.testweb.testweb.mqtt.web.callback.breaker.BreakerXblCallback;
import com.testweb.testweb.mqtt.web.config.MqttConfig;
import lombok.extern.slf4j.Slf4j;

/**
 * User:Json
 * Date: 2024/6/17
 **/
@Slf4j
public class MqttUtils {

    private static MqttConfig mqttConfig;

    public static MqttConfig getMqttConfig() {
        if (mqttConfig == null)
            mqttConfig = AppContextUtil.getBean(MqttConfig.class);
        return mqttConfig;
    }



    //初始化 订阅
    public  static void subscribeInit(){
        // 一个mqtt 不可能只服务一种类型的设备  也许还有别的 所以 回调地址 根据需求 一一扩展即可
        // 只是以断路器 举例  也许项目里还有空调设备 门禁设备 都在这里扩展即可在回调地址里 找不同的实现类处理业务
         //  断路器 品牌1
         getMqttConfig().subscribe("light/+/up", 0,new BreakerCallback());
         //  断路器 品牌2
         getMqttConfig().subscribe("xbl/+/bb",0,new BreakerXblCallback());


    }




    /**
     * 发送消息
     * qos 0 最多一次传递  1 至少一次传递  2 仅一次传递
     * retained  true 保留消息  false 非保留消息
     * topic    主题
     * message  内容
     */
    public static boolean sendMqttMsg(int qos, boolean retained, String topic, String message) {
        try {
           return getMqttConfig().publish(qos, retained, topic, message);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("MQtt发送消息报错:" + e.getMessage());
            return false;
        }
    }

    /*
     * topic 主题
     * message 内容
     * */
    public static boolean sendMqttMsg(String topic, String message) {
        return sendMqttMsg(1, false, topic, message);
    }

}

springboot启动时 订阅主题

package com.testweb.testweb.mqtt.web.init;


import com.testweb.testweb.mqtt.web.config.MqttConfig;
import com.testweb.testweb.mqtt.web.utils.MqttUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * User:Json
 * Date: 2024/6/17
 **/
@Slf4j
@Order(1)
@Component
public class StartInit implements CommandLineRunner {


    @Autowired
    MqttConfig mqttConfig;



    @Override
    public void run(String... args) throws Exception {
        log.info("==========mqtt启动 init==================");
        if(mqttConfig.connect()){
            MqttUtils.subscribeInit();
        }
    }
}

大功告成