SpringBoot整合MQTT协议实现消息响应
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,该协议构建于TCP/IP协议 上。
MQTT 并不是消息队列,尽管两者的很多行为和特性非常接近,比如都采用发布订阅模式等,但是他们面向的场景有着显著的不同。
- 消息队列主要用于服务端应用之间的消息存储与转发,这类场景往往数据量大但接入量少。
- MQTT 面向的是 IoT 领域和移动互联网领域,这类场景的侧重点是海量的设备接入、管理与消息传输。
在实际的场景中,两者往往被结合起来使用,譬如先由 MQTT Broker 接收物联网设备上传的数据,然后通过消息队列MQ将这些数据转发到具体应用进行处理。
今天文章的内容就是使用MQTT协议整合spring boot 实现发布订阅以及接收消息处理消息之后,做一个结果消息发送。
首先!先在pom文件中引入相关依赖,此次使用的是mqtt5的版本
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>mqttPlus</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.3.0.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>2.3.12.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
<version>5.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
</dependencies>
</project>
再在application.yml配置文件中做相关参数的配置
spring:
application:
name: mqttPlus
#MQTT配置信息
mqtt:
#MQTT服务地址,端口号默认11883,如果有多个,用逗号隔开
url: tcp://localhost:1883
#用户名
username:
#密码
password:
#客户端id(不能重复)
in-client-id: ${random.value} # 随机值,使出入站 client ID 不同
out-client-id: ${random.value}
client-id: ${random.int} # 客户端Id,不能相同,采用随机数 ${random.value}
#MQTT默认的消息推送主题,实际可在调用接口是指定
default:
topic: $SYS/brokers/+/clients/+/connected,$SYS/brokers/+/clients/+/disconnected
timeout: 60 # 超时时间
keepalive: 60 # 保持连接
clearSession: true # 清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)
server:
port: 9000
#日志配置
logging:
level:
root: info
org.springframework.web: info
org.springframework.web.servlet.mvc.method.annotation: info
org.springframework.web.servlet.mvc.method: info
org.springframework.web.servlet.mvc: info
org.springframework.web.servlet.handler: info
org.springframework.web.servlet.mvc.support: info
org.springframework.web.servlet.view: info
还有一个工具类
/**
* spring工具类 方便在非spring管理环境中获取bean
*
*/
@Component
public final class SpringUtils implements BeanFactoryPostProcessor, ApplicationContextAware
{
/** Spring应用上下文环境 */
private static ConfigurableListableBeanFactory beanFactory;
private static ApplicationContext applicationContext;
public static Map<String, Object> getBeansByAnnotation(Class clsName) throws BeansException{
return beanFactory.getBeansWithAnnotation(clsName);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
{
SpringUtils.beanFactory = beanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
SpringUtils.applicationContext = applicationContext;
}
/**
* 获取对象
*
* @param name
* @return Object 一个以所给名字注册的bean的实例
* @throws org.springframework.beans.BeansException
*
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException
{
return (T) beanFactory.getBean(name);
}
/**
* 获取类型为requiredType的对象
*
* @param clz
* @return
* @throws org.springframework.beans.BeansException
*
*/
public static <T> T getBean(Class<T> clz) throws BeansException
{
T result = (T) beanFactory.getBean(clz);
return result;
}
/**
* 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
*
* @param name
* @return boolean
*/
public static boolean containsBean(String name)
{
return beanFactory.containsBean(name);
}
/**
* 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
*
* @param name
* @return boolean
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException
{
return beanFactory.isSingleton(name);
}
/**
* @param name
* @return Class 注册对象的类型
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException
{
return beanFactory.getType(name);
}
/**
* 如果给定的bean名字在bean定义中有别名,则返回这些别名
*
* @param name
* @return
* @throws org.springframework.beans.factory.NoSuchBeanDefinitionException
*
*/
public static String[] getAliases(String name) throws NoSuchBeanDefinitionException
{
return beanFactory.getAliases(name);
}
/**
* 获取aop代理对象
*
* @param invoker
* @return
*/
@SuppressWarnings("unchecked")
public static <T> T getAopProxy(T invoker)
{
return (T) AopContext.currentProxy();
}
/**
* 获取当前的环境配置,无配置返回null
*
* @return 当前的环境配置
*/
public static String[] getActiveProfiles()
{
return applicationContext.getEnvironment().getActiveProfiles();
}
}
MQTT相关的配置注入properties类
@Component
public class MqttProperties {
/**
* 用户名
*/
@Value("${spring.mqtt.username}")
private String username;
/**
* 密码
*/
@Value("${spring.mqtt.password}")
private String password;
/**
* 连接地址
*/
@Value("${spring.mqtt.url}")
private String hostUrl;
/**
* 进-客户Id
*/
@Value("${spring.mqtt.in-client-id}")
private String inClientId;
/**
* 出-客户Id
*/
@Value("${spring.mqtt.out-client-id}")
private String outClientId;
/**
* 客户Id
*/
@Value("${spring.mqtt.client-id}")
private String clientId;
/**
* 默认连接话题
*/
@Value("${spring.mqtt.default.topic}")
private String defaultTopic;
/**
* 超时时间
*/
@Value("${spring.mqtt.timeout}")
private int timeout;
/**
* 保持连接数
*/
@Value("${spring.mqtt.keepalive}")
private int keepalive;
/**是否清除session*/
@Value("${spring.mqtt.clearSession}")
private boolean clearSession;
// ...getter and setter
定义一个mqtt自带的一个用于消息发送的类,我们可以来修改注入我们需要的属性
@Component
@MessagingGateway(defaultRequestChannel = "mqttOutboundChannel")
public interface MqttGateway {
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, String data);
void sendToMqtt(@Header(MqttHeaders.TOPIC) String topic, @Header(MqttHeaders.QOS) Integer Qos, String data);
}
@Component
@Slf4j
public class MqttGatewayPublish {
@Resource
private MqttGateway mqttGateway;
public void publish(String topic , int qos , String content){
mqttGateway.sendToMqtt(topic,qos,content);
}
public void publish(String topic, String content){
mqttGateway.sendToMqtt(topic,content);
}
}
@Slf4j
@Service
public class MqttService {
@Autowired
private MqttPahoMessageDrivenChannelAdapter adapter;
public void addTopic(String topic) {
addTopic(topic, 1);
}
public void addTopic(String topic,int qos) {
String[] topics = adapter.getTopic();
if(!Arrays.asList(topics).contains(topic)){
adapter.addTopic(topic,qos);
}else {
log.info("重复订阅主题:"+topic+"失败");
}
}
public void removeTopic(String topic) {
adapter.removeTopic(topic);
}
}
接下来我们会定义一个配置类,将我们MQTT的客户端工厂进行一个配置,再实现一个管道的适配器
还有一条出站的管道也就是生产,一条入站的管道也就是消费。还有一个自定义线程池后续使用
MqttConfig
@Configuration
public class MqttConfigV2 {
@Autowired
private MqttProperties mqttProperties;
@Lazy(true)
@Autowired
private MqttMessageHandle mqttMessageHandle;
//Mqtt 客户端工厂 所有客户端从这里产生
@Bean
public MqttPahoClientFactory mqttPahoClientFactory(){
DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory();
MqttConnectOptions options = new MqttConnectOptions();
options.setServerURIs(mqttProperties.getHostUrl().split(","));
options.setUserName(mqttProperties.getUsername());
options.setPassword(mqttProperties.getPassword().toCharArray());
factory.setConnectionOptions(options);
return factory;
}
// Mqtt 管道适配器
@Bean()
public MqttPahoMessageDrivenChannelAdapter adapter(MqttPahoClientFactory factory){
return new MqttPahoMessageDrivenChannelAdapter(mqttProperties.getInClientId(),factory,mqttProperties.getDefaultTopic().split(","));
}
// 消息消费者 (接收,处理来自mqtt的消息)
@Bean
public IntegrationFlow mqttInbound(MqttPahoMessageDrivenChannelAdapter adapter) {
adapter.setCompletionTimeout(5000);
adapter.setQos(1);
//适配器->线程池获取管道
return IntegrationFlows.from( adapter)
.channel(new ExecutorChannel(mqttThreadPoolTaskExecutor()))
.handle(mqttMessageHandle)
.get();
}
// 出站处理器 (向 mqtt 发送消息 生产者)
@Bean
public IntegrationFlow mqttOutboundFlow(MqttPahoClientFactory factory) {
MqttPahoMessageHandler handler = new MqttPahoMessageHandler(mqttProperties.getOutClientId(),factory);
handler.setAsync(true);
handler.setConverter(new DefaultPahoMessageConverter());
handler.setDefaultTopic(mqttProperties.getDefaultTopic().split(",")[0]);
return IntegrationFlows.from( "mqttOutboundChannel").handle(handler).get();
}
/*
* 项目自定义线程池
*/
@Bean
public ThreadPoolTaskExecutor mqttThreadPoolTaskExecutor()
{
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 最大可创建的线程数
int maxPoolSize = 200;
executor.setMaxPoolSize(maxPoolSize);
// 核心线程池大小
int corePoolSize = 50;
executor.setCorePoolSize(corePoolSize);
// 队列最大长度
int queueCapacity = 1000;
executor.setQueueCapacity(queueCapacity);
// 线程池维护线程所允许的空闲时间
int keepAliveSeconds = 300;
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
定义两个注解作为标识方便,处理器拿到对应主题的回调
/**
* 该注解的value()方法用于指定被注解的类在组件扫描时的名称。
*/
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface MqttService {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
/**
* 该注解指定了主题的名称和是否自动订阅。在运行时,可以获取被该注解标记的方法并读取注解的属性值
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MqttTopic {
/**
* 主题名字
*/
String value() default "";
/**
* 是否自动订阅
*/
boolean autoSubscribe() default true;
}
MqttTopicHandle
定义主题收到消息之后的一系列操作,在后面消息处理的handle会体现
/**
* MqttTopicHandle
*
* @author hengzi
* @date 2022/8/24
*/
@MqttService
public class MqttTopicHandle {
public static final Logger log = LoggerFactory.getLogger(MqttTopicHandle.class);
// 如果系统上下线主题已经在配置文件中有订阅了, 那么久不需要再自动订阅
@MqttTopic(value = "$SYS/brokers/+/clients/+/connected",autoSubscribe = false)
public void connected(Message<?> message){
log.info("有什么东西连上了: {}",message.getPayload());
}
@MqttTopic(value ="$SYS/brokers/+/clients/+/disconnected",autoSubscribe = false)
public void disconnected(Message<?> message){
log.info("disconnected: {}",message.getPayload());
}
// 这里的 # 号是通配符
@MqttTopic(value = "test/#",autoSubscribe = false)
public void test(Message<?> message){
log.info("test="+message.getPayload());
}
// 这里的 + 号是通配符
@MqttTopic(value = "topic/+/+/up",autoSubscribe = false)
public void up(Message<?> message){
log.info("topic: {}, payload: {}",message.getHeaders().get("mqtt_receivedTopic",String.class),message.getPayload());
}
@MqttTopic(value = "topic/1/2/down",autoSubscribe = false)
public void down(Message<?> message){
log.info("down="+message.getPayload());
}
@Autowired
private MqttGatewayPublish mqttGatewayPublish;
@MqttTopic(value = "xjydemo",autoSubscribe = true)
public void publishTopic(Message<?> message){
log.info("订阅xjydemo主题,新消息内容为:"+message.getPayload());
String ifSuccess= Math.random() < 0.5 ? "comsumer success" : "comsumer fail";
mqttGatewayPublish.publish("IfSuccessComsumer","消息"+message.getPayload()+":"+ ifSuccess);
}
@MqttTopic(value = "IfSuccessComsumer",autoSubscribe = true)
public void IfSuccessComsumer(Message<?> message){
log.info("订阅IfSuccessComsumer主题,新消息内容为:"+message.getPayload());
}
}
定义好之后我们就写一个handle来对消息进行处理,通过自定义注解+反射的方式获取到相关的主题消息处理的方法并执行使用。
MqttMessageHandle
/**
* 消费管道绑定了这个handle,我们消费管道里面的消息会来到这个handle进行一个处理
* MessageHandler--->处理MQTT消息
* @author hengzi
* @date 2022/8/24
*/
@Component
public class MqttMessageHandle implements MessageHandler {
public static final Logger log = LoggerFactory.getLogger(MqttMessageHandle.class);
// 包含 @MqttService注解 的类(Component)
public static Map<String, Object> mqttServices;
@Autowired
private MqttPahoMessageDrivenChannelAdapter adapter;
/**
* 所有mqtt到达的消息都会在这里处理
* 要注意这个方法是在线程池里面运行的
* 这个方法调用getMqttTopicService(message)进行分配到对应主题
* @param message message
*/
@Override
public void handleMessage(Message<?> message) throws MessagingException {
getMqttTopicService(message);
}
public Map<String, Object> getMqttServices(){
if(mqttServices==null){
//获取所有带有MqttService注解的bean
mqttServices = SpringUtils.getBeansByAnnotation(MqttService.class);
}
return mqttServices;
}
public void getMqttTopicService(Message<?> message){
// 在这里 我们根据不同的 主题 分发不同的消息
String receivedTopic = message.getHeaders().get("mqtt_receivedTopic",String.class);
if(receivedTopic==null || "".equals(receivedTopic)){
return;
}
for(Map.Entry<String, Object> entry : getMqttServices().entrySet()){
// 把所有带有 @MqttService 的类遍历-->按照我们demo这获得的是-->MqttTopicHandle
Class<?> clazz = entry.getValue().getClass();
// 获取他所有方法
Method[] methods = clazz.getDeclaredMethods();
for ( Method method: methods ){
//MqttTopicHandle中的@MqttTopic的主题
if (method.isAnnotationPresent(MqttTopic.class)){
// 如果这个方法有 这个注解 到这里我们已经得到了所有主题
MqttTopic handleTopic = method.getAnnotation(MqttTopic.class);
//主题匹配
if(isMatch(receivedTopic,handleTopic.value())){
// 并且 这个 topic 匹配成功
try {
method.invoke(SpringUtils.getBean(clazz),message);
return;
} catch (IllegalAccessException e) {
e.printStackTrace();
log.error("代理炸了");
} catch (InvocationTargetException e) {
log.error("执行 {} 方法出现错误",handleTopic.value(),e);
}
}
}
}
}
}
/**
* mqtt 订阅的主题与我实际的主题是否匹配
* @param topic 是实际的主题
* @param pattern 是我订阅的主题 可以是通配符模式
* @return 是否匹配
*/
public static boolean isMatch(String topic, String pattern){
if((topic==null) || (pattern==null) ){
return false;
}
if(topic.equals(pattern)){
// 完全相等是肯定匹配的
return true;
}
if("#".equals(pattern)){
// # 号代表所有主题 肯定匹配的
return true;
}
String[] splitTopic = topic.split("/");
String[] splitPattern = pattern.split("/");
boolean match = true;
// 如果包含 # 则只需要判断 # 前面的
for (int i = 0; i < splitPattern.length; i++) {
if(!"#".equals(splitPattern[i])){
// 不是# 号 正常判断
if(i>=splitTopic.length){
// 此时长度不相等 不匹配
match = false;
break;
}
if(!splitTopic[i].equals(splitPattern[i]) && !"+".equals(splitPattern[i])){
// 不相等 且不等于 +
match = false;
break;
}
}
else {
// 是# 号 肯定匹配的
break;
}
}
return match;
}
@PostConstruct//实例化后调用该方法
public void autoSubscribeImpl(){
// 自动订阅系统
// 初始化的时候 去订阅主题
Set<String> topics = new HashSet<>(16);
Map<String, Object> theMqttServices = getMqttServices();
for(Map.Entry<String, Object> entry : theMqttServices.entrySet()){
Class<?> clazz = entry.getValue().getClass();
Method[] methods = clazz.getDeclaredMethods();
for ( Method method: methods ){
if (method.isAnnotationPresent(MqttTopic.class)){
MqttTopic handleTopic = method.getAnnotation(MqttTopic.class);
if(handleTopic.autoSubscribe()){
topics.add(handleTopic.value());
}
}
}
}
if(topics.size()>0){
topics.forEach(item->{
if (!isTopicSubscribed(item)){
log.info("自动订阅主题: {}",item);
adapter.addTopic(item);
}
});
}
}
// 判断特定主题是否已被订阅
public boolean isTopicSubscribed(String topic) {
String[] subscribedTopics = adapter.getTopic();
for (String subscribedTopic : subscribedTopics) {
if (topic.equals(subscribedTopic)) {
return true;
}
}
return false;
}
}
到现在为止,我们有了主题handle,有了消息通道,有了messageHandle处理订阅主题入站的消息,在主题中也进行了处理后的结果应答。
我们在topicHandle中
@MqttTopic(value = "xjydemo",autoSubscribe = true)
public void publishTopic(Message<?> message){
log.info("订阅xjydemo主题,新消息内容为:"+message.getPayload());
String ifSuccess= Math.random() < 0.5 ? "comsumer success" : "comsumer fail";
mqttGatewayPublish.publish("IfSuccessComsumer","消息"+message.getPayload()+":"+ ifSuccess);
}
这段代码模拟了一个处理业务的成功率,然后发送消息通知。
Controller测试
@Slf4j
@RestController
@RequestMapping("/mqtt")
public class mqttController {
@Resource
private MqttGatewayPublish mqttGatewayPublish;
@Resource
private MqttService mqttService;
@PostMapping("/publish")
public String publish(String content,String topic){
mqttGatewayPublish.publish(topic,content);
log.info("发送消息:"+content+";给主题:"+topic);
return "success";
}
@PostMapping("/addTopic")
public String addTopic(String topic){
mqttService.addTopic(topic);
log.info("订阅主题:"+topic);
return "success";
}
@PostMapping("/removeTopic")
public String removeTopic(String topic){
mqttService.removeTopic(topic);
log.info("取消订阅主题:"+topic);
return "success";
}
}
发送请求:
接收以及响应,我们可以看看控制台:
文章基于个人学习,参考创作,如果有代码写的不好的地方欢迎大佬指出!