SpringBoot整合MQTT实现消息响应

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";
    }
}

在这里插入图片描述

发送请求:

在这里插入图片描述

接收以及响应,我们可以看看控制台:

在这里插入图片描述

文章基于个人学习,参考创作,如果有代码写的不好的地方欢迎大佬指出!

  • 31
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值