开源版 EMQX 代码访问和 WebHook 使用

我们使用上一篇博客搭建的 EMQX 开源版服务,编写一个 SpringBoot 示例程序通过代码演示消息的发送和接收操作。

EMQX 收发消息编写代码非常简单,不需要提前创建主题,直接编码向主题发送消息或订阅接收消息即可。在本篇博客最后会提供源代码的下载。

客户端 SDK 官网地址:https://docs.emqx.com/zh/emqx/v5.8/connect-emqx/introduction.html

Github 上 Java 版 SDK 源码地址:https://github.com/eclipse-paho/paho.mqtt.java

客户端使用 Java 编码示例官网地址:https://www.emqx.com/zh/blog/how-to-use-mqtt-in-java

Github 使用 Java SDK 示例地址:https://github.com/emqx/MQTT-Client-Examples/tree/master/mqtt-client-Java


一、SpringBoot 示例程序

搭建一个 SpringBoot 工程,代码结构如下图所示:

01

首先我们需要在 pom 文件中引入 Paho MQTT 依赖包,这里我使用的是 V3 版本,pom 文件内容如下:

<?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>com.jobs</groupId>
    <artifactId>springboot_emqx</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!--引入Paho MQTT v3依赖包 -->
        <dependency>
            <groupId>org.eclipse.paho</groupId>
            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
            <version>1.2.5</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>
</project>

在 application.yml 配置文件中自定义了常用的配置项,具体内容如下:

server:
  port: 8080
spring:
  application:
    name: emqx-app

emqx:
  # 服务器地址和端口
  host: tcp://192.168.136.128:1883
  # 客户端id(不能重复)
  # 如果运行多个实例,可以考虑在程序代码中增加随机字符串作为后缀
  clientId: emqx-client
  # 用户账号
  username: jobs
  # 用户密码
  password: jobsloveyou
  # 订阅主题表达式(如果为空,表示不订阅;如果订阅多个主题表达式,需要以英文逗号分隔)
  # 主题表达式可以使用通配符,+号是单层通配符,#号是多层通配符,使用方法参看以下官网链接:
  # https://docs.emqx.com/zh/emqx/v5.8/messaging/mqtt-wildcard-subscription.html
  # 订阅主题默认消息质量等级为 Qos1
  topics: jobs/subscribe

EmqxService 文件中编码读取配置项,初始化客户端连接,加载到 Spring 容器。客户端发送消息使用该类实例对象中的方法即可,具体内容如下:

package com.jobs.mqtt;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;

@Slf4j
@Component
public class EmqxService {

    @Value("${emqx.host}")
    private String host;

    @Value("${emqx.clientId}")
    private String clientId;

    @Value("${emqx.username}")
    private String username;

    @Value("${emqx.password}")
    private String password;

    @Value("${emqx.topics}")
    private String topics;

    //客户端连接收发消息的回调对象,本Demo中由Spring容器注入的是EmqxCallback对象实例
    @Autowired
    private MqttCallback mqttCallback;

    private MqttClient mqttClient;

    //当对象实例化后,所需要的其它依赖注入完成,并且即将初始化完成时,执行该方法
    @PostConstruct
    private void init() {
        try {
            //构建连接参数
            MqttConnectOptions options = new MqttConnectOptions();
            options.setAutomaticReconnect(true); //异常断开后自动重连
            options.setUserName(username); //用户账号
            options.setPassword(password.toCharArray()); //用户密码
            options.setCleanSession(true); //当前连接是临时会话,每次连接都创建新的会话
            //实例化连接对象
            mqttClient = new MqttClient(host, clientId, new MemoryPersistence());
            mqttClient.setCallback(mqttCallback);
            mqttClient.connect(options); //连接Emqx服务器
            //如果配置的主题表达式不为空,则进行订阅
            if (StringUtils.isNotEmpty(topics)) {
                //如果配置了多个主题表达式,则以英文逗号分隔
                String[] topicsArr = topics.split(",");
                mqttClient.subscribe(topicsArr); //系统默认采用的消息质量等级为Qos1
            }
        } catch (Exception e) {
            log.error("客户端连接Emqx服务器失败:{}", e.getMessage());
            throw new RuntimeException(e); //抛出异常
        }
    }

    /**
     * 发送消息(消息内容转换为字节数组,采用系统默认字符集)
     *
     * @param topic  主题
     * @param msg    消息字节数组
     * @param qos    消息质量等级
     * @param retain 是否是保留消息
     * @return 如果成功,返回空字符串,否则返回错误消息
     */
    public String publish(String topic, String msg, QosEnum qos, Boolean retain) {
        return publish(topic, msg.getBytes(), qos, retain);
    }

    /**
     * 发送消息
     *
     * @param topic   主题
     * @param msg     消息字节数组
     * @param qos     消息质量等级
     * @param retain  是否是保留消息
     * @param charSet 字符集,比如 UTF-8、ISO-8859-1
     * @return 如果成功,返回空字符串,否则返回错误消息
     */
    public String publish(String topic, String msg, QosEnum qos, Boolean retain, String charSet) {
        byte[] msgBytes;
        try {
            msgBytes = msg.getBytes(charSet);
        } catch (Exception e) {
            String errMsg = "消息按照指定字符集转换字节数组失败:" + e.getMessage();
            log.error(errMsg);
            return errMsg;
        }
        return publish(topic, msgBytes, qos, retain);
    }

    /**
     * 发送消息
     *
     * @param topic  主题
     * @param msg    消息字节数组
     * @param qos    消息质量等级
     * @param retain 是否是保留消息
     * @return 如果成功,返回空字符串,否则返回错误消息
     */
    public String publish(String topic, byte[] msg, QosEnum qos, Boolean retain) {
        try {
            //构建消息对象
            MqttMessage mqttMessage = new MqttMessage();
            mqttMessage.setPayload(msg);
            mqttMessage.setQos(qos.getValue());
            mqttMessage.setRetained(retain);
            //发送消息
            mqttClient.publish(topic, mqttMessage);
            return "";
        } catch (Exception e) {
            String errMsg = "发送消息失败:error:" + e.getMessage();
            log.error(errMsg);
            return errMsg;
        }
    }

    /**
     * 订阅一个主题表达式
     *
     * @param topicFilter 主题表达式,可以使用通配符
     * @param qos         消息质量等级
     * @return 如果成功,返回空字符串,否则返回错误消息
     */
    public String subscribe(String topicFilter, QosEnum qos) {
        try {
            mqttClient.subscribe(topicFilter, qos.getValue());
            return "";
        } catch (Exception e) {
            String errMsg = "订阅主题(" + topicFilter + ")失败:" + e.getMessage();
            log.error(errMsg);
            return errMsg;
        }
    }

    /**
     * 订阅多个主题表达式(传入的列表中,Qos的顺序需要与主题表达式的顺序保持一致)
     *
     * @param topicFilterlist 主题表达式列表
     * @param qoslist         消息质量等级列表
     * @return 如果成功,返回空字符串,否则返回错误消息
     */
    public String subscribe(List<String> topicFilterlist, List<QosEnum> qoslist) {
        try {
            String[] topicArr = topicFilterlist.toArray(new String[0]);
            int[] qosArr = qoslist.stream().mapToInt(QosEnum::getValue).toArray();
            mqttClient.subscribe(topicArr, qosArr);
            return "";
        } catch (Exception e) {
            String errMsg = "订阅多个主题失败:" + e.getMessage();
            log.error(errMsg);
            return errMsg;
        }
    }

    /**
     * 取消订阅主题
     *
     * @param topicFilter 主题表达式
     * @return 如果成功,返回空字符串,否则返回错误消息
     */
    public String unsubscribe(String topicFilter) {
        try {
            mqttClient.unsubscribe(topicFilter);
            return "";
        } catch (Exception e) {
            String errMsg = "取消订阅主题失败:" + e.getMessage();
            log.error(errMsg);
            return errMsg;
        }
    }

    /**
     * 取消订阅多个主题
     *
     * @param topicFilterlist 主题表达式列表
     * @return 如果成功,返回空字符串,否则返回错误消息
     */
    public String unsubscribe(List<String> topicFilterlist) {
        try {
            String[] topicArr = topicFilterlist.toArray(new String[0]);
            mqttClient.unsubscribe(topicArr);
            return "";
        } catch (Exception e) {
            String errMsg = "取消订阅多个主题失败:" + e.getMessage();
            log.error(errMsg);
            return errMsg;
        }
    }
}

上面代码中客户端连接初始化过程中,设置了 callback 回调实例对象,也就是加载到 Spring 容器中的 EmqxCallback 对象,V3 版本的 SDK 提供了 3 个回调方法,其中 messageArrived 是客户端作为消费者接收发过来的消息,deliveryComplete 是客户端作为生产者发送消息成功后的回调方法。

package com.jobs.mqtt;

import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class EmqxCallback implements MqttCallback {

    /**
     * 当客户端意外断开连接时触发回调
     *
     * @param throwable
     */
    @Override
    public void connectionLost(Throwable throwable) {
        //由于我们在初始化客户端连接对象时,已经设置了自动重连,所以这里只记录日志
        log.error("客户端工具意外断开连接:" + throwable.getMessage());
    }

    /**
     * 当客户端作为消息接收者,收到消息时触发回调
     * 需要注意的是:该方法由客户端同步调用,在此方法未正确执行前,不会给Emqx服务器发送Ack确认消息
     * 一旦该方法抛出异常,客户端就会断开连接;
     * 当客户端重连时,所有Qos1、Qos2且客户端未进行Ack确认的消息,都将由Emqx服务器再次发送给客户端
     *
     * @param topic       主题
     * @param mqttMessage 接收到的消息对象
     * @throws Exception
     */
    @Override
    public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
        //为了防止此方法抛出异常,导致客户端断开连接,这里使用 try catch 进行保障
        try {
            StringBuilder sb = new StringBuilder();
            sb.append("消息主题:").append(topic).append(System.lineSeparator());
            sb.append("消息Id:").append(mqttMessage.getId()).append(System.lineSeparator());
            //这里假设消息发送者,发送的消息采用的是系统默认字符集编码将字符串转换成字节数组的
            sb.append("消息内容:").append(new String(mqttMessage.getPayload())).append(System.lineSeparator());
            sb.append("消息质量等级Qos:").append(mqttMessage.getQos()).append(System.lineSeparator());
            //是否是保留消息(当发送者发送消息时,接收客户端处于离线状态。然后接收客户端再上线,才会收到保留消息)
            sb.append("是否是保留消息:").append(mqttMessage.isRetained()).append(System.lineSeparator());
            sb.append("是否是重发的消息:").append(mqttMessage.isDuplicate());

            log.info(sb.toString());
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }

    /**
     * 当前客户端作为消息发送者,如果发送成功后会触发回调
     *
     * @param token
     */
    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
        int msgId = token.getMessageId();
        String[] topicArr = token.getTopics();
        log.info("向主题({})发送消息(id为{})成功", String.join(",", topicArr), msgId);
    }
}

发送消息和订阅主题时,为了规范消息质量等级值的传入,这里专门编写了一个枚举类:

package com.jobs.mqtt;

/**
 * 消息质量等级
 */
public enum QosEnum {

    Qos0(0),
    Qos1(1),
    Qos2(2);

    private final Integer value;

    QosEnum(Integer value) {
        this.value = value;
    }

    public Integer getValue() {
        return value;
    }
}

本 Demo 代码提供了 2 种方法测试发送消息,第一种测试发送消息的方法是通过 EmqxTest 测试类,内容如下:

package com.jobs;

import com.jobs.mqtt.EmqxService;
import com.jobs.mqtt.QosEnum;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class EmqxTest {

    @Autowired
    private EmqxService emqxService;

    /**
     * 发消息测试,可以使用【MQTT客户端工具】验证接收情况
     */
    @Test
    public void publishMsg() {
        String topic = "jobs/test";
        String msg = "hello jobs,未来可期!";
        String result = emqxService.publish(topic, msg, QosEnum.Qos1, false);
        if (StringUtils.isNotEmpty(result)) {
            //如果发送失败,打印 error 信息
            System.out.printf(result);
        }
    }
}

我们使用 MQTT 客户端工具,订阅 jobs/test 主题即可进行测试。第二种方法就是通过 web 接口,代码如下:

package com.jobs.controller;

import com.jobs.entity.AjaxResult;
import com.jobs.entity.MsgEntity;
import com.jobs.mqtt.EmqxService;
import com.jobs.mqtt.QosEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {

    @Autowired
    private EmqxService emqxService;

    /**
     * 发送消息
     */
    @PostMapping("/publish")
    public AjaxResult publishMsg(@RequestBody MsgEntity me) {
        String result = emqxService.publish(me.getTopic(), me.getMsg(), QosEnum.values()[me.getQos()], me.getRetain());
        if (StringUtils.isNotEmpty(result)) {
            return AjaxResult.error(result);
        } else {
            return AjaxResult.success("发送成功");
        }
    }
}

为了接收参数,编写了一个实体类 MsgEntity 内容如下:

package com.jobs.entity;

import lombok.Data;

@Data
public class MsgEntity {

    /**
     * 主题
     */
    public String topic;

    /**
     * 消息内容
     */
    private String msg;

    /**
     * 质量等级
     */
    private Integer qos;

    /**
     * 是否是保留消息
     */
    private Boolean retain;
}

返回的参数使用 AjaxResult 具体内容如下:

package com.jobs.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AjaxResult {

    /**
     * 状态码
     */
    private Integer code;

    /**
     * 状态消息
     */
    private String msg;

    /**
     * 返回数据
     */
    private Object data;

    //---------返回成功的相关方法

    public static AjaxResult success() {
        return AjaxResult.success("success");
    }

    public static AjaxResult success(Object data) {
        return AjaxResult.success("success", data);
    }

    public static AjaxResult success(String msg) {
        return AjaxResult.success(msg, null);
    }

    public static AjaxResult success(String msg, Object data) {
        return new AjaxResult(200, msg, data);
    }

    //---------返回错误的相关方法

    public static AjaxResult error() {
        return AjaxResult.error("error");
    }

    public static AjaxResult error(String msg) {
        return AjaxResult.error(msg, null);
    }

    public static AjaxResult error(String msg, Object data) {
        return new AjaxResult(500, msg, data);
    }

    public static AjaxResult error(int code, String msg) {
        return new AjaxResult(code, msg, null);
    }
}

以上代码都经过实际测试,没有问题,可以借助 MQTT 客户端工具进行收发消息,测试验证程序代码。


二、WebHook 配置使用

官方 WebHook 功能介绍:https://docs.emqx.com/zh/emqx/v5.8/data-integration/webhook.html

如果你只是接收消息的话,可以使用 WebHook,它可以调用你提供的 Web 接口发送数据,这是比较常见的应用场景。

通过 Emqx 开源版 Web 后台管理工具,新建一个 WebHook 配置,界面如下:

02

触发器选择【消息发布】,请求方式选择【POST】,填写好你提供的 URL 地址即可,本 Demo 提供的接口代码如下:

package com.jobs.controller;

import com.jobs.entity.AjaxResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@Slf4j
@RequestMapping("/test")
@RestController
public class TestController {
    /**
     * 接收 Emqx 的 webhook 的调用,传送 webhook 订阅主题中接收的消息数据
     */
    @PostMapping("/webhook")
    public AjaxResult webHookMsg(@RequestBody Map<String, Object> params) {
        log.info("接收到 webhook 调用发来的消息:{}", params);
        return AjaxResult.success("接收消息成功");
    }
}

/**
上面的接口在控制台上打印的结果如下:
接收到 webhook 调用发来的消息:{publish_received_at=1771242437998, pub_props={User-Property={}}, qos=1, client_attrs={}, clientid=mqttx_a72e6d56, 
peerhost=192.168.136.1, topic=jobs/webhook, payload=测试webhook消息, username=jobs, event=message.publish, metadata={rule_id=webhooktest_WH_D}, 
peername=192.168.136.1:9415, timestamp=1771242437998, node=emqx@docker.jobs.com, id=00064AEF8453F73D599000004CFD0002, flags={retain=false, dup=false}}
*/

根据接口调用后在控制台上打印出的内容,我们可以通过具体的 key 值拿到数据,比如通过 payload 拿到接收的消息内容,通过 topic 获取到具体的主题等。


本篇博客的源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springboot_emqx.zip

posted @ 2026-02-16 19:59  乔京飞  阅读(1183)  评论(0)    收藏  举报