一、SpringBoot整合MQTT
创建项目,引入 MQTT依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.3.12.RELEASE</version></dependency><!-- spring-integration-mqtt依赖 --><dependency><groupId>org.springframework.integration</groupId><artifactId>spring-integration-mqtt</artifactId><version>6.1.2</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.28</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.75</version></dependency>
1、yml配置文件
在 application.yml文件中,定义 MQTT连接信息。
## MQTT 基本连接参数 ##mqtt:host: tcp://192.168.xxx.xxx:1883# host: tcp://broker.emqx.io:1883userName: admin
passWord: xxxxxx
qos:1clientId: ClientId_local #ClientId_local必须唯一。timeout:10# 超时时间keepalive:30# 保持连接时间clearSession:true# 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)topic1: A/b/# # 通配符主题只能用于订阅,不能用于发布。+:表示单层通配符,#:表示多层通配符topic2: A/abc
topic3: ABC
2、MQTT配置类
创建一个 MqttConfig配置类,并获取配置文件的 MQTT的连接参数。创建 MyMqttClient类注入Spring。
@Slf4j@ConfigurationpublicclassMqttConfig{@Value("${mqtt.host}")publicString host;@Value("${mqtt.username}")publicString username;@Value("${mqtt.password}")publicString password;@Value("${mqtt.clientId}")publicString clientId;@Value("${mqtt.timeout}")publicint timeOut;@Value("${mqtt.keepalive}")publicint keepAlive;@Value("${mqtt.clearSession}")publicboolean clearSession;@Value("${mqtt.topic1}")publicString topic1;@Value("${mqtt.topic2}")publicString topic2;@Value("${mqtt.topic3}")publicString topic3;@Bean//注入SpringpublicMyMqttClientmyMqttClient(){MyMqttClient myMqttClient =newMyMqttClient(host, username, password, clientId, timeOut, keepAlive, clearSession);for(int i =0; i <10; i++){try{
myMqttClient.connect();// 这里可以订阅主题,推荐放到 MqttCallbackExtended.connectComplete方法中//myMqttClient.subscribe("ABC", 1);return myMqttClient;}catch(MqttException e){
log.error("== MqttConfig ==> MQTT connect exception, connect time = {}", i);try{Thread.sleep(2000);}catch(InterruptedException e1){
e1.printStackTrace();}}}return myMqttClient;}}
3、MQTT 客户端封装类
创建 MQTT 客户端封装类MyMqttClient。对 MQTT Broker进行操作。
@Slf4jpublicclassMyMqttClient{/**
* MQTT Broker 基本连接参数,用户名、密码为非必选参数
*/privateString host;privateString username;privateString password;privateString clientId;privateint timeout;privateint keepalive;privateboolean clearSession;/**
* MQTT 客户端
*/privatestaticMqttClient client;publicstaticMqttClientgetClient(){return client;}publicstaticvoidsetClient(MqttClient client){MyMqttClient.client = client;}publicMyMqttClient(String host,String username,String password,String clientId,int timeOut,int keepAlive,boolean clearSession){this.host = host;this.username = username;this.password = password;this.clientId = clientId;this.timeout = timeOut;this.keepalive = keepAlive;this.clearSession = clearSession;}/**
* 设置 MQTT Broker 基本连接参数
*
* @param username
* @param password
* @param timeout
* @param keepalive
* @return
*/publicMqttConnectOptionssetMqttConnectOptions(String username,String password,int timeout,int keepalive){MqttConnectOptions options =newMqttConnectOptions();
options.setUserName(username);
options.setPassword(password.toCharArray());
options.setConnectionTimeout(timeout);
options.setKeepAliveInterval(keepalive);
options.setCleanSession(clearSession);
options.setAutomaticReconnect(true);return options;}/**
* 连接 MQTT Broker,得到 MqttClient连接对象
*/publicvoidconnect()throwsMqttException{if(client ==null){
client =newMqttClient(host, clientId,newMemoryPersistence());// 设置回调
client.setCallback(newMyMqttCallback(MyMqttClient.this));}// 连接参数MqttConnectOptions mqttConnectOptions =setMqttConnectOptions(username, password, timeout, keepalive);if(!client.isConnected()){
client.connect(mqttConnectOptions);}else{
client.disconnect();
client.connect(mqttConnectOptions);}
log.info("== MyMqttClient ==> MQTT connect success");//未发生异常,则连接成功}/**
* 发布,默认qos为0,非持久化
*
* @param pushMessage
* @param topic
*/publicvoidpublish(String pushMessage,String topic){publish(pushMessage, topic,0,false);}/**
* 发布消息
*
* @param pushMessage
* @param topic
* @param qos
* @param retained:留存
*/publicvoidpublish(String pushMessage,String topic,int qos,boolean retained){MqttMessage message =newMqttMessage();
message.setPayload(pushMessage.getBytes());
message.setQos(qos);
message.setRetained(retained);MqttTopic mqttTopic =MyMqttClient.getClient().getTopic(topic);if(null== mqttTopic){
log.error("== MyMqttClient ==> topic is not exist");}MqttDeliveryToken token;//Delivery:配送synchronized(this){//注意:这里一定要同步,否则,在多线程publish的情况下,线程会发生死锁,分析见文章最后补充try{
token = mqttTopic.publish(message);//也是发送到执行队列中,等待执行线程执行,将消息发送到消息中间件
token.waitForCompletion(1000L);}catch(MqttPersistenceException e){
e.printStackTrace();}catch(MqttException e){
e.printStackTrace();}}}/**
* 订阅某个主题,qos默认为0
*
* @param topic
*/publicvoidsubscribe(String topic){subscribe(topic,0);}/**
* 订阅某个主题
*
* @param topic
* @param qos
*/publicvoidsubscribe(String topic,int qos){try{MyMqttClient.getClient().subscribe(topic, qos);}catch(MqttException e){
e.printStackTrace();}
log.info("== MyMqttClient ==> 订阅主题成功:topic = {}, qos = {}", topic, qos);}/**
* 取消订阅主题
*
* @param topic 主题名称
*/publicvoidcleanTopic(String topic){if(client !=null&& client.isConnected()){try{
client.unsubscribe(topic);}catch(MqttException e){
e.printStackTrace();}}else{
log.error("== MyMqttClient ==> 取消订阅失败!");}
log.info("== MyMqttClient ==> 取消订阅主题成功:topic = {}", topic);}}
**
说明:
**
- MqttClient: 同步调用客户端,使用阻塞方法通信。
- MqttClientPersistence: 代表一个持久的数据存储,用于在传输过程中存储出站和入站的信息,使其能够传递到指定的 QoS。
- MqttConnectOptions: 连接选项,用于指定连接的参数,下面列举一些常见的方法。 - setUserName: 设置用户名- setPassword: 设置密码- setCleanSession: 设置是否清除会话- setKeepAliveInterval: 设置心跳间隔- setConnectionTimeout: 设置连接超时时间- setAutomaticReconnect: 设置是否自动重连
4、MqttClient回调类
创建一个 MqttClient回调类MyMqttCallback。
@Slf4jpublicclassMyMqttCallbackimplementsMqttCallbackExtended{//手动注入privateMqttConfig mqttConfig =SpringUtils.getBean(MqttConfig.class);privateMyMqttClient myMqttClient;publicMyMqttCallback(MyMqttClient myMqttClient){this.myMqttClient = myMqttClient;}/**
* MQTT Broker连接成功时被调用的方法。在该方法中可以执行 订阅系统约定的主题(推荐使用)。
* 如果 MQTT Broker断开连接之后又重新连接成功时,主题也需要再次订阅,将重新订阅主题放在连接成功后的回调方法中比较合理。
*
* @param reconnect
* @param serverURI MQTT Broker的url
*/@OverridepublicvoidconnectComplete(boolean reconnect,String serverURI){String connectMode = reconnect ?"重连":"直连";
log.info("== MyMqttCallback ==> MQTT 连接成功,连接方式:{},serverURI:{}", connectMode, serverURI);//订阅主题
myMqttClient.subscribe(mqttConfig.topic1,1);
myMqttClient.subscribe(mqttConfig.topic2,1);
myMqttClient.subscribe(mqttConfig.topic3,1);List<String> topicList =newArrayList<>();
topicList.add(mqttConfig.topic1);
topicList.add(mqttConfig.topic2);
topicList.add(mqttConfig.topic3);
log.info("== MyMqttCallback ==> 连接方式:{},订阅主题成功,topic:{}", connectMode, topicList);}/**
* 丢失连接,可在这里做重连
* 只会调用一次
*
* @param throwable
*/@OverridepublicvoidconnectionLost(Throwable throwable){
log.error("== MyMqttCallback ==> connectionLost 连接断开,5S之后尝试重连: {}", throwable.getMessage());long reconnectTimes =1;while(true){try{if(MyMqttClient.getClient().isConnected()){//判断已经重新连接成功 需要重新订阅主题 可以在这个if里面订阅主题 或者 connectComplete(方法里面) 看你们自己选择
log.warn("== MyMqttCallback ==> mqtt reconnect success end 重新连接 重新订阅成功");return;}
reconnectTimes +=1;
log.warn("== MyMqttCallback ==> mqtt reconnect times = {} try again... mqtt重新连接时间 {}", reconnectTimes, reconnectTimes);MyMqttClient.getClient().reconnect();}catch(MqttException e){
log.error("== MyMqttCallback ==> mqtt断连异常", e);}try{Thread.sleep(5000);}catch(InterruptedException e1){}}}/**
* 接收到消息(subscribe订阅的主题消息)时被调用的方法
*
* @param topic
* @param mqttMessage
* @throws Exception 后得到的消息会执行到这里面
*/@OverridepublicvoidmessageArrived(String topic,MqttMessage mqttMessage)throwsException{
log.info("== MyMqttCallback ==> messageArrived 接收消息主题: {},接收消息内容: {}", topic,newString(mqttMessage.getPayload()));/**
* 根据订阅的主题分别处理业务。可以通过if-else或者策略模式来分别处理不同的主题消息。
*///topic1主题if(topic.equals("ABC")){Map maps =(Map)JSON.parse(newString(mqttMessage.getPayload(),StandardCharsets.UTF_8));//TODO 业务处理//doSomething1(maps);
log.info("== MyMqttCallback ==> messageArrived 接收消息主题: {},{}业务处理消息内容完成", topic,"TodoService1");}//topic2主题if(topic.equals("A/b/1qaz")){Map maps =(Map)JSON.parse(newString(mqttMessage.getPayload(),StandardCharsets.UTF_8));//TODO 业务处理//doSomething2(maps);
log.info("== MyMqttCallback ==> messageArrived 接收消息主题: {},{}业务处理消息内容完成", topic,"TodoService2");}}/**
* 消息发送(publish)完成时被调用的方法
*
* @param iMqttDeliveryToken
*/@OverridepublicvoiddeliveryComplete(IMqttDeliveryToken iMqttDeliveryToken){
log.info("== MyMqttCallback ==> deliveryComplete 消息发送完成,Complete= {}", iMqttDeliveryToken.isComplete());}}
MqttCallback类方法说明:
- connectionLost(Throwable cause): 连接丢失时被调用
- messageArrived(String topic, MqttMessage message): 接收到消息时被调用
- deliveryComplete(IMqttDeliveryToken token): 消息发送完成时被调用
**
MqttCallbackExtended类方法说明:该类继承MqttCallback类
**
- connectComplete(boolean reconnect, String serverURI): 连接丢失时被调用
4.1 SpringUtils工具类
@ComponentpublicclassSpringUtilsimplementsBeanFactoryPostProcessor,ApplicationContextAware{/**
* Spring应用上下文环境
*/privatestaticConfigurableListableBeanFactory beanFactory;privatestaticApplicationContext applicationContext;@OverridepublicvoidpostProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)throwsBeansException{SpringUtils.beanFactory = beanFactory;}@OverridepublicvoidsetApplicationContext(ApplicationContext applicationContext)throwsBeansException{SpringUtils.applicationContext = applicationContext;}/**
* 获取对象
*
* @param name
* @return Object 一个以所给名字注册的bean的实例
* @throws org.springframework.beans.BeansException
*/@SuppressWarnings("unchecked")publicstatic<T>TgetBean(String name)throwsBeansException{return(T) beanFactory.getBean(name);}/**
* 获取类型为requiredType的对象
*
* @param clz
* @return
* @throws org.springframework.beans.BeansException
*/publicstatic<T>TgetBean(Class<T> clz)throwsBeansException{T result =(T) beanFactory.getBean(clz);return result;}/**
* 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
*
* @param name
* @return boolean
*/publicstaticbooleancontainsBean(String name){return beanFactory.containsBean(name);}/**
* 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
*
* @param name
* @return boolean
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*/publicstaticbooleanisSingleton(String name)throwsNoSuchBeanDefinitionException{return beanFactory.isSingleton(name);}/**
* @param name
* @return Class 注册对象的类型
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*/publicstaticClass<?>getType(String name)throwsNoSuchBeanDefinitionException{return beanFactory.getType(name);}/**
* 如果给定的bean名字在bean定义中有别名,则返回这些别名
*
* @param name
* @return
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*/publicstaticString[]getAliases(String name)throwsNoSuchBeanDefinitionException{return beanFactory.getAliases(name);}/**
* 获取aop代理对象
*
* @param invoker
* @return
*/@SuppressWarnings("unchecked")publicstatic<T>TgetAopProxy(T invoker){return(T)AopContext.currentProxy();}/**
* 获取当前的环境配置,无配置返回null
*
* @return 当前的环境配置
*/publicstaticString[]getActiveProfiles(){return applicationContext.getEnvironment().getActiveProfiles();}/**
* 获取当前的环境配置,当有多个环境配置时,只获取第一个
*
* @return 当前的环境配置
*/publicstaticStringgetActiveProfile(){finalString[] activeProfiles =getActiveProfiles();if(activeProfiles ==null){returnnull;}return activeProfiles[0];}}
到此,Springboot 通过 MqttClient整合操作 MQTT Broker就可以了。
二、操作MQTT
我们在 service层创建一个 MqttService类,业务通过 MqttService类来统一操作 MqttClient。
1、自定义发送消息载体类
这里创建一个 MyXxxMqttMsg类,来约定发送消息的载体类格式。
@DatapublicclassMyXxxMqttMsgimplementsSerializable{privatestaticfinallong serialVersionUID =-8303548938481407659L;/**
* MD5值:MD5_lower(content + timestamp)
*/privateString md5;/**
* 消息内容
*/privateString content ="";/**
* 时间戳
*/privateLong timestamp;}
2、MqttService类
1)接口:
publicinterfaceMqttService{/**
* 添加订阅主题
*
* @param topic 主题名称
*/voidaddTopic(String topic);/**
* 取消订阅主题
*
* @param topic 主题名称
*/voidremoveTopic(String topic);/**
* 发布主题消息内容
*
* @param msgContent
* @param topic
*/voidpublish(String msgContent,String topic);}
2)实现类:
@ServicepublicclassMqttServiceImplimplementsMqttService{@AutowiredprivateMyMqttClient myMqttClient;@OverridepublicvoidaddTopic(String topic){
myMqttClient.subscribe(topic);}@OverridepublicvoidremoveTopic(String topic){
myMqttClient.cleanTopic(topic);}@Overridepublicvoidpublish(String msgContent,String topic){//MyXxxMqttMsg 转JsonMyXxxMqttMsg myXxxMqttMsg =newMyXxxMqttMsg();
myXxxMqttMsg.setContent(msgContent);
myXxxMqttMsg.setTimestamp(System.currentTimeMillis());// TODO Md5值
myXxxMqttMsg.setMd5(UUID.randomUUID().toString());String msgJson =JSON.toJSONString(myXxxMqttMsg);//发布消息
myMqttClient.publish(msgJson, topic);}
3、controller类
创建一个 MyMqttController类,来操作一下 MQTT。
@RestController@RequestMapping("/mqtt")@Api(value ="MyMqttController", tags ={"MQTT相关操作接口"})publicclassMyMqttController{@AutowiredprivateMqttService mqttService;@GetMapping("/addTopic")@ApiOperation(value ="添加订阅主题接口")publicvoidaddTopic(String topic){
mqttService.addTopic(topic);}@GetMapping("/removeTopic")@ApiOperation(value ="取消订阅主题接口")publicvoidremoveTopic(String topic){
mqttService.removeTopic(topic);}@PostMapping("/removeTopic")@ApiOperation(value ="发布主题消息内容接口")publicvoidremoveTopic(String msgContent,String topic){
mqttService.publish(msgContent, topic);}}
订阅和取消主题操作:MQTTX发布了一个主题消息。
发布通配符主题消息:
– 求知若饥,虚心若愚。
版权归原作者 Charge8 所有, 如有侵权,请联系我们删除。