需求:
在开发管理系统时,需要在项目中 控制一些物联网设备,因为我们开发的是管理系统,所以在管理设备的时候 需要有通用性和扩展性!
比如 空调设备 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();
}
}
}
大功告成