Bootstrap

【EMQX实践】0到1实现EMQX客户端组件

在当今快速发展的物联网(IoT)时代,设备间的即时通信变得至关重要。Spring Boot以其简化配置和快速部署的特性,成为构建企业级应用的首选框架。Vert.x作为一个响应式应用平台,以其轻量级和高性能的特点,为构建高并发的分布式系统提供了强大支持。结合EMQ X Broker,我们能够实现一个基于Spring Boot和Vert.x的MQTT客户端,它将为微服务架构下的设备通信提供高效、可靠的解决方案。通过本实现指南,我们将一步步引导开发者如何从零开始构建这一组件,探索其在现代应用开发中的无限可能。

技术框架

为什么选择SpringBoot及 Vert.x 作为组件主要技术框架?
我想当我们聊到Java项目实践时候,往往离不开SpringSpringBoot 框架:

  • Spring Boot简化了基于Spring的应用开发过程,自动配置和微服务支持使其成为快速开发和部署微服务的理想选择。
  • Spring Boot拥有庞大的社区和丰富的生态系统,提供了大量的库和工具,方便开发者快速解决问题和扩展功能。
  • Spring Boot的应用结构清晰,易于理解和维护。
  • Spring Boot可以轻松地扩展应用,无论是水平扩展还是垂直扩展。

Vert.x它的核心是一个轻量级的、多语言的、响应式的应用程序运行时,专为构建响应式、高性能、可伸缩的应用程序而设计。

  • Vert.x提供了一个事件驱动的非阻塞编程模型,非常适合处理大量并发连接,这对于需要高吞吐量和低延迟的MQTT通信至关重要。
  • Vert.x都拥有庞大的社区和丰富的生态系统,提供了大量的库和工具,方便开发者快速解决问题和扩展功能。
  • Vert.x的模块化设计也有助于保持代码的整洁和可维护性。
  • Vert.x可以轻松地扩展应用,无论是水平扩展还是垂直扩展。

Spring Boot遇上Vert.x,它们的结合带来了更多的可能性:

  • Spring Boot和Vert.x可以很好地集成,Spring Boot提供了Vert.x的集成支持,使得在Spring Boot应用中使用Vert.x变得简单。
  • Vert.x的高性能特性,结合Spring Boot的轻量级特性,可以构建出既快速又高效的应用。

创建项目

源码地址Gitee
工具:idea、maven
环境:jdk17

创建项目步骤

  1. 选择创建新项目: 在启动界面,点击“Create New Project”(创建新项目)。
  2. 选择 Maven: 在左侧的项目类型列表中,选择“Maven”。
  3. 配置 JDK: 确保 Maven 项目使用的 JDK 版本与你的 Maven 配置匹配。你可以通过点击右侧的“JDK”下拉菜单来选择。
  4. 添加 Maven 架构: 如果你需要特定的 Maven 架构,可以在右侧的“Add Archetype”(添加架构)部分搜索并选择一个架构,例如 maven-archetype-quickstart
  5. 填写项目信息: 在右侧的表单中,填写项目的基本设置,包括:
    • Group Id(组 ID):通常是一个反向域名形式的标识符,如 com.gitee.xmhzzz.
    • Artifact Id(构件 ID):项目的唯一名称,如 emqx-practice.
    • Version(版本):项目的版本号,如 1.0.0-SNAPSHOT.
    • Package(包):项目的根包名,通常是组 ID 的全小写形式。

image.png

创建模块

  1. 选择创建新项目: 在启动界面,点击“New Module”(创建项目的模块)。
  2. 选择 Maven: 在左侧的项目类型列表中,选择“Maven”。
  3. 配置 JDK: “JDK”选择JDK17以上。
  4. 添加 Maven 架构: 如果你需要特定的 Maven 架构,可以在右侧的“Add Archetype”(添加架构)部分搜索并选择一个架构,例如 maven-archetype-quickstart
  5. 添加Parent: emqx-practice.
  6. 填写项目信息: 在右侧的表单中,填写项目的基本设置,包括:
    • Group Id(组 ID):通常是一个反向域名形式的标识符,如 com.gitee.xmhzzz.
    • Artifact Id(构件 ID):项目的唯一名称,如 emqx-client.
    • Version(版本):项目的版本号,如 1.0.0-SNAPSHOT.
    • Package(包):项目的根包名,通常是组 ID 的全小写形式。

创建三个模块emqx-client、emqx-client-starter、emqx-example-service
image.png

maven依赖

parent项目emqx-practice

maven依赖

<dependencyManagement>
  <dependencies>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>${spring-boot.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>

    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-stack-depchain</artifactId>
      <version>${vertx.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>

    <dependency>
      <groupId>com.alibaba.fastjson2</groupId>
      <artifactId>fastjson2</artifactId>
      <version>${fastjson2.version}</version>
    </dependency>

    <dependency>
      <groupId>com.squareup.okhttp3</groupId>
      <artifactId>okhttp</artifactId>
      <version>${okhttp.version}</version>
    </dependency>

    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>${guava.version}</version>
    </dependency>

    <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-all</artifactId>
      <version>${hutool.version}</version>
    </dependency>

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>${commons-lang3.version}</version>
    </dependency>

  </dependencies>
</dependencyManagement>

版本管理

<properties>
  <maven.compiler.source>17</maven.compiler.source>
  <maven.compiler.target>17</maven.compiler.target>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <spring-boot.version>3.0.2</spring-boot.version>
  <vertx.version>4.2.3</vertx.version>
  <fastjson2.version>2.0.33</fastjson2.version>
  <okhttp.version>3.14.9</okhttp.version>
  <guava.version>32.0.1-jre</guava.version>
  <hutool.version>5.8.20</hutool.version>
  <commons-lang3.version>3.12.0</commons-lang3.version>
</properties>

Module模块emqx-client

<dependencies>

  <dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
  </dependency>

  <dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-mqtt</artifactId>
  </dependency>

  <dependency>
    <groupId>io.vertx</groupId>
    <artifactId>vertx-core</artifactId>
  </dependency>

  <dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
  </dependency>

  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
  </dependency>

  <dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
  </dependency>

  <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
  </dependency>

  <dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
  </dependency>

</dependencies>

Module模块emqx-client-starter

<dependencies>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>

  <dependency>
    <groupId>com.gitee.xmhzzz</groupId>
    <artifactId>emqx-client</artifactId>
    <version>${project.version}</version>
  </dependency>

</dependencies>

Module模块emqx-example-service

<dependencies>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <!-- mqtt client -->
  <dependency>
    <groupId>com.gitee.xmhzzz</groupId>
    <artifactId>emqx-client-starter</artifactId>
    <version>${project.version}</version>
  </dependency>

</dependencies>

emqx-client组件实现

emqx-client主要划分四部分

MQTT客户端上下文、MQTT配置信息、MQTT消息监听接口、MQTT消息推送
image.png

EMQX客户端上下文

● 连接管理:管理MQTT连接及断开连接功能。
● 订阅列表:客户端订阅的所有主题及其对应的QoS(服务质量)级别。
● 推送消息:发送消息到EMQX中。

接口定义
/**
  * @ClassName
  * @Description emqx-client 上下文
  * @Author wzq
  * @Date 2024/1/22 14:07
  * @Version 1.0
  */
public interface IMqttClient {

    /**
      * Description: EMQX 建立连接
      * @Author: xmh
      * @Create: 2024/7/13-14:49
      * @Version: v1.0
      **/
    boolean connect();

    /**
      * Description: EMQX 关闭连接
      * @Author: xmh
      * @Create: 2024/7/13-14:50
      * @Version: v1.0
      **/
    boolean disconnect();

    /**
      * Description: EMQX-CLIENT 是否在线
      * @Author: xmh
      * @Create: 2024/7/13-14:49
      * @Version: v1.0
      **/
    boolean isConnect();

    /**
      * Description: EMQX-CLIENT 推送消息
      * @Author: xmh
      * @Create: 2024/7/13-14:49
      * @Version: v1.0
      **/
    IMqttResp publish(IMqttReq request);

    /**
      * Description: 关闭 EMQX-CLIENT 
      * @Author: xmh
      * @Create: 2024/7/13-14:49
      * @Version: v1.0
      **/
    void mqttStop();

}
EMQX-CLIENT上下文接口实现

EMQX-CLIENT技术框架选择Vert.x.
创建MqttClientContext类并继承Vert.x框架AbstractVerticle和实现接口IMqttClient

/**
 * @ClassName MqttClientContext emqx-client 上下文
 * @Description
 * @Author wzq
 * @Date 2024/7/9 17:16
 * @Version 1.0
 */
@Slf4j
public class MqttClientContext extends AbstractVerticle implements IMqttClient{

    public static final Long RECONNECT_INTERVAL = 3600L;
    private static final String allWildcard = "#";
    private static final String singleWildcard = "+";
    private final static String SMART_UP_TOPIC_FORMAT = "$share/%s/%s";
    private final static List<String> topicFormatList = Lists.newArrayList(
            SMART_UP_TOPIC_FORMAT);
    private final IMqttConfig mqttConfig;
    private final List<IMqttListener> IMqttListenerList;
    private CountDownLatch countDownLatch;
    private MqttClient mqttClient;
    private String subscribeGroup;

    public MqttClientContext(IMqttConfig mqttConfig, List<IMqttListener> IMqttListenerList, String subscribeGroup) {
        this.mqttConfig = mqttConfig;
        this.IMqttListenerList = IMqttListenerList;
        this.subscribeGroup = subscribeGroup;
    }
    
    /**
     * Description:在 Vert.x 中,start 方法是 Verticle 类的一个生命周期方法,它在 verticle 被部署并准备好开始执行时被调用。如果你的 verticle 在启动时需要执行一些简单的同步操作,你可以重写这个方法来放置你的启动代码。
     * @Author: xmh
     **/
    @Override
    public void start() {
          if (Objects.isNull(this.mqttClient)) {
            this.mqttClient = MqttClient.create(vertx, buildMqttClientOptions());
        }
        //接收服务端消息处理handler
        mqttClient.publishHandler(pub -> {
            Buffer buffer = pub.payload();
            String topicName = pub.topicName();
            String[] split = topicName.split("/");
            String string = buffer.toString(StandardCharsets.UTF_8);
            CommonMessage message = new CommonMessage();
            HashMap<String, Object> headers = Maps.newHashMap();
            headers.put("topic",topicName);
            headers.put("qos",pub.qosLevel().value());
            message.setHeaders(headers);
            message.setMessageContent(string);
            IMqttListenerList.forEach(f -> {
                String topic = f.getTopic();
                if (topicRute(topic, split)){
                    f.onMessage(message);
                }
            });

        });
        mqttClient.closeHandler(unused -> getVertx().setTimer(RECONNECT_INTERVAL, h -> start()));
        mqttClient.connect(mqttConfig.getTcpPort(), mqttConfig.getHost(),
                s -> {
                    if (s.succeeded()) {
                        log.info("IMqttClient connect success.");
                        subscribe();
                        countDownLatch.countDown();
                    } else {
                        log.error("IMqttClient connect fail: ", s.cause());
                        if (s.cause() != null) {
                            vertx.setTimer(RECONNECT_INTERVAL, handler -> this.start());
                        }
                    }
                });
    }

     /**
     * Description: 在 Vert.x 中,如果你的 verticle 需要在停止时执行一些简单的同步清理任务,你可以重写 stop 方法来放置你的清理代码。这个方法在 Vert.x 准备停止 verticle 时调用,通常用于关闭资源,如数据库连接、网络连接、文件句柄等。
     * @Author: xmh
     **/
    @Override
    public void stop() {
         mqttClient.closeHandler(null);
        mqttClient.disconnect((handler) -> {
            if (handler.succeeded()) {
                vertx.close();
                log.info("IMqttClient disConnect success.");
            } else {
                log.error("IMqttClient disConnect fail: ", handler.cause());
            }
        }).exceptionHandler(event -> {
            log.error("IMqttClient fail: ", event.getCause());
        });
    }
    

    /**
      * Description: EMQX 建立连接
      * @Author: xmh
      **/
    @Override
    public boolean connect() {
        log.info("emqx connect ing ......");
        countDownLatch = new CountDownLatch(1);
        Vertx.vertx().deployVerticle(this);
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return true;
    }

     /**
      * Description: EMQX 关闭连接
      * @Author: xmh
      **/
    @Override
    public boolean disconnect() {
          if (!isConnect()) {
            log.warn("IMqttClient no connect");
            return false;
        }
        vertx.undeploy(deploymentID(), (handler) -> {
            if (handler.succeeded()) {
                log.info("undeploy success");
            } else {
                log.warn("undeploy fail, {}. ", handler.cause().getMessage(), handler.cause());
            }
        });
        return true;
    }

    /**
      * Description: EMQX-CLIENT 是否在线
      * @Author: xmh
      **/
    @Override
    public boolean isConnect() {
        return Optional.ofNullable(mqttClient).map(MqttClient::isConnected).orElse(false);
    }

     /**
      * Description: EMQX-CLIENT 推送消息
      * @Author: xmh
      **/
    @Override
    public IMqttResp publish(IMqttReq request) {
         MqttResp response = new MqttResp();
        Buffer payload = Buffer.buffer(request.getMessageContent());
        mqttClient.publish(request.getTopic(), payload, MqttQoS.valueOf(request.getQos()), false, false, s -> {
            if (s.succeeded()) {
                log.info("===>IMqttClient publish success[{}]", s.result());
            } else {
                log.error("===>IMqttClient publish fail.", s.cause());
            }
        });
        response.setCode(200);
        return response;
    }

      /**
      * Description: 关闭 EMQX-CLIENT 
      * @Author: xmh
      **/
    @Override
    public void mqttStop() {
         this.stop();
    }

    
     private MqttClientOptions buildMqttClientOptions() {
        MqttClientOptions options = new MqttClientOptions();
        options.setClientId("micro-client-" + RandomStringUtils.randomAlphanumeric(17));
        options.setMaxMessageSize(100_000_000);
        options.setKeepAliveInterval(60);
        options.setPassword(mqttConfig.getPassword());
        options.setUsername(mqttConfig.getUsername());
        options.setSsl(mqttConfig.getSsl());
        options.setReconnectInterval(RECONNECT_INTERVAL);
        options.setReconnectAttempts(Integer.MAX_VALUE);
        return options;
    }

    private boolean topicRute(String topic, String[] split) {
        String[] listenerTopic = topic.split("/");
        boolean flag = true;
        for (int i = 0; i < split.length; i++) {
            if (allWildcard.equals(listenerTopic[i])) {
                break;
            }
            if (singleWildcard.equals(listenerTopic[i])) {
                continue;
            }
            if (!split[i].equals(listenerTopic[i])) {
                flag = false;
                break;
            }
        }
        return flag;
    }

    private void subscribe() {
        List<String> subscribeTopicSuffix = mqttConfig.getSysSubscribeTopics();
        if (subscribeTopicSuffix == null || subscribeTopicSuffix.isEmpty()) {
            throw new IllegalArgumentException("subscribe topic is empty");
        }
        List<String> subscribeTopic = subscribeTopicSuffix.stream()
                .flatMap(topicSuffix -> topicFormatList.stream().map(t -> String.format(t,subscribeGroup, topicSuffix)))
                .toList();
        subscribeTopic.forEach(topic -> mqttClient.subscribe(topic, 0, s -> {
            if (s.succeeded()) {
                log.info("===>IMqttClient subscribe success.  result[{}] topic[{}]", s.result(), topic);
            } else {
                log.error("===>IMqttClient subscribe fail. topic[{}] ", topic, s.cause());
            }
        }));
    }

} 

EMQX配置信息

接口定义
public interface IMqttConfig {

    
    String getHost();

    Integer getTcpPort();

    Integer getHttpPort();

    String getUsername();

    String getPassword();

    Boolean getSsl();

    //订阅的系统topic
    List<String> getSysSubscribeTopics();

    String getAppId();

    String getAppSecret();
}
Config接口实现
/**
 * @ClassName MqttConfig
 * @Description
 * @Author wzq
 * @Date 2024/7/9 9:36
 * @Version 1.0
 */
@Data
public class MqttConfig implements IMqttConfig{

    /**
      * EMQX 地址
      */
    private String host;

    /**
      * EMQX tcp端口
      */
    private Integer tcpPort;

    /**
     * EMQX Http端口
     */
    private Integer httpPort;

    /**
      * EMQX 登录名称
      */
    private String username;

    /**
     * EMQX 登录名称
     */
    private String password;

    private String appId;

    private String appSecret;

    private Boolean ssl = false;

    //订阅的系统topic
    private List<String> sysSubscribeTopics;


}

EMQX-CLIENT消息推送

接口定义
/**
 * @InterfaceName IMqttClientApi
 * @Description
 * @Author wzq
 * @Date 2024/7/8 17:04
 * @Version 1.0
 */
public interface IMqttClientApi {

    /**
      * @Description Http 方式推送消息
      */
    IMqttResp httpPublish(IMqttReq request);


    /**
      * @Description TCP 方式推送消息
      */
    IMqttResp tcpPublish(IMqttReq request);

}
model定义
public interface IMqttReq {

    String getTopic();

    Integer getQos();

    String getMessageContent();

}

@Data
@Accessors(chain = true)
public class MqttReq implements IMqttReq{

    private String topic;

    private Integer qos;

    private String messageContent;
}

public interface IMqttResp {
     Integer getCode();

     String getMessage();

     Map getData();
}

@Data
@Accessors(chain = true)
public class MqttResp implements IMqttResp{

    private Integer code;

    private String message;

    private Map data;

}
api接口实现
/**
 * @ClassName MqttApiImpl
 * @Description
 * @Author wzq
 * @Date 2024/7/9 19:23
 * @Version 1.0
 */
@Slf4j
public class MqttApiImpl implements IMqttClientApi{

    private final IMqttConfig mqttConfig;

    private final IMqttClient mqttClient;

    public MqttApiImpl(IMqttConfig mqttConfig, IMqttClient mqttClient) {
        this.mqttConfig = mqttConfig;
        this.mqttClient = mqttClient;
    }
    
    private static final MediaType HTTP_MEDIA_TYPE_JSON_UTF8 = MediaType.parse("application/json; charset=utf-8");

    @Override
    public IMqttResp httpPublish(IMqttReq request) {
        return BeanUtil.toBeanIgnoreError(this.callHttp(request), MqttResp.class);
    }

    @Override
    public IMqttResp tcpPublish(IMqttReq request) {
        return mqttClient.publish(request);
    }

    private Map<String, ?> callHttp(IMqttReq params) {
        String path = "";
        String url = mqttConfig.getHost() + path;
        log.debug("http url[{}] requestBodyStr[{}]", url, params.getMessageContent());

        Dict dict = Dict.create();
        dict.set("topic", params.getTopic());              //订阅主题
        dict.set("payload", params.getMessageContent());   //内容
        dict.set("qos", 0);                                //质量
        dict.set("retain",false);                          //是否保存
        String requestBodyStr = JSON.toJSONString(dict);

        RequestBody requestBody = RequestBody.create(HTTP_MEDIA_TYPE_JSON_UTF8, requestBodyStr);
        Request request = new Request.Builder()
        .url(url)
        .post(requestBody)
        .header("Content-Type", "application/json")
        .header("Authorization", Credentials.basic(mqttConfig.getAppId(), mqttConfig.getAppSecret()))
        .build();

        try (Response response = getHttpClientInstance().newCall(request).execute()) {
            log.debug("Call http success. url[{}] response[{}]", url, response);

            if (response.code() == 404) {
                return ImmutableMap.of("code", 404, "Message", "404 Not Found");
            } else if (!response.isSuccessful()) {
                return ImmutableMap.of("code", response.code(), "Message", "Server Error");
            }

            // 输出响应内容
            assert response.body() != null;
            String string = response.body().string();
            return JSON.parseObject(string);
        } catch (IOException e) {
            log.warn("Call http failed, {}. url[{}] requestBodyStr[{}]", e.getMessage(), url, requestBodyStr);
        }

        return Collections.emptyMap();
    }

    private OkHttpClient getHttpClientInstance() {
        return HttpClientHolder.HTTP_CLIENT_INSTANCE;
    }

    private static class HttpClientHolder {
        private static final OkHttpClient HTTP_CLIENT_INSTANCE = new OkHttpClient.Builder()
        .connectTimeout(Duration.ofSeconds(3))
        .readTimeout(Duration.ofSeconds(3))
        .writeTimeout(Duration.ofSeconds(3))
        .dispatcher(new Dispatcher(Executors.newFixedThreadPool(8)))
                .build();

    }
}

EMQX-CLIENT消息监听接口

接口定义
public interface IMqttListener {

    /**
     * Description: 路由topic
     **/
    String getTopic();

    /**
     * Description: 消费类型 广播、集群模式
     **/
    String getType();

    /**
     * Description: 消息处理
     **/
    void onMessage(IMessage message);
}
model定义
public interface IMessage {

    String getDeviceId();

    String getProductId();

    Map<String, Object> getHeaders();

    String getMessageContent();

}


@Data
@Accessors(chain = true)
public class CommonMessage implements IMessage{

    private String deviceId;

    private String productId;

    private String messageContent;

    protected volatile Map<String, Object> headers = Maps.newConcurrentMap();

}

emqx-client-starter模块实现

emqx-client-starter 是一个用于将emqx-client集成到 Spring Boot 应用中的自动配置启动器(starter)。在 Spring Boot 中,一个 starter 是一个包含特定库依赖和自动配置类的 POM 文件,它简化了应用程序的依赖管理和配置。
image.png

自动加载类EmqxClientAutoConfiguration

  • 定义自动加载类条件
  • 自动加载emqx-client模块的配置类MqttConfig
  • 在没有IMqttListener类型的类加载则默认实现IMqttListener接口
  • 自动加载emqx-client模块的配置类MqttClientContext
  • 自动加载emqx-client模块的配置类IMqttClientApi
@Slf4j
@Configuration
@ConditionalOnProperty(value = "emqx.sdk.enable", havingValue = "true")
public class EmqxClientAutoConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "emqx.sdk")
    public IMqttConfig mqttConfig() {
        return new MqttConfig();
    }

    @Bean
    @ConditionalOnMissingBean
    public IMqttListener mqttListener() {
        return new IMqttListener() {
            @Override
            public String getTopic() {
                return "#";
            }

            @Override
            public String getType(){
                return "";
            }
            @Override
            public void onMessage(IMessage message) {
                LoggerFactory.getLogger(IMqttListener.class)
                .info("[默认消息监听器]接收到消息. Message[{}]", JSON.toJSONString(message));
            }
        };
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean({IMqttConfig.class, IMqttListener.class})
    public IMqttClient mqttClient(MqttConfig emqXConfig, List<IMqttListener> IMqttListenerList, Environment environment) {
        String serviceName = environment.getProperty("spring.application.name");
        String env = environment.getProperty("spring.profiles.active");
        IMqttClient mqttClient = new MqttClientContext(emqXConfig, IMqttListenerList,serviceName+"-"+env);
        Executors.newSingleThreadExecutor().execute(mqttClient::connect);
        Runtime.getRuntime().addShutdownHook(new Thread(mqttClient::mqttStop));
        return mqttClient;
    }

    @Bean("mqttExecutorService")
    @ConditionalOnBean({IMqttClient.class, IMqttListener.class})
    public ExecutorService mqttExecutorService() {
        ExecutorService executorService = new ThreadPoolExecutor(
            1, 8,
            1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(1024),
            new ThreadFactoryBuilder().setNameFormat("mqttlogin-task-%d").build(),
            (r, executor) -> log.warn("MQTT  task executor rejectedExecution , Runnable Class : {}",
                                      r.getClass().getSimpleName()));
        return executorService;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean({IMqttConfig.class, IMqttClient.class})
    public IMqttClientApi mqttApi(IMqttClient client,IMqttConfig config) {
        return new MqttApiImpl(config,client);
    }

}

自定义META-INF

Sping Boot提供了一种方便的方式来将自定义的配置文件放置在 META.NF目录下,并将其添加到classpath中。
在SpringBoot2.7.x版本之后,慢慢不支持META-INF/spring.factories文件了,需要导入的自动配置类可以放在/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中

com.gitee.xmhzzz.emqx.strater.EmqxClientAutoConfiguration

emqx-example-service模块代码实现

emqx-example-service模块是Spring Boot Web 项目,主要实现 EMQX-CLIENT生命周期管理及消息的发送和消费。
image.png

添加web项目启动类

@SpringBootApplication
public class EmqxExampleApplication {

    private static final Logger logger = LoggerFactory.getLogger(EmqxExampleApplication.class);
    public static void main(String[] args) {

        SpringApplication.run(EmqxExampleApplication.class, args);
        logger.info("==================== emqx-example 启动完成 ====================");
    }

}

业务代码

controller层消息推送
@RequestMapping("/web/emqx")
@RestController
public class EmqxController {

    @Autowired
    private EmqxPublishService emqxPublishService;

    @PostMapping("/tcp/publish")
    public Result<Void> tcpPublish(@RequestBody MqttReq req){
        emqxPublishService.tcpPublish(req);
        return Result.ok();
    }

    @PostMapping("/http/publish")
    public Result<Void> httpPublish(@RequestBody MqttReq req){
        emqxPublishService.httpPublish(req);
        return Result.ok();
    }
}
service层消息推送
@Service
public class EmqxPublishService {

    @Autowired(required = false)
    private IMqttClientApi mqttClientApi;


   public IMqttResp httpPublish(IMqttReq request){
        IMqttResp iMqttResp = mqttClientApi.httpPublish(request);
        return iMqttResp;
    }

    public IMqttResp tcpPublish(IMqttReq request){
        IMqttResp iMqttResp = mqttClientApi.tcpPublish(request);
        return iMqttResp;
    }

}
HTTP公共返回类
@Data
public class Result<T> {

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

    /**
     * 状态描述
     */
    private String message;

    /**
     * 数据
     */
    private T body;

    public Result() {
        this(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg());
    }

    public Result(Integer code, String message) {
        this(code, message, null);
    }

    public Result(Integer code, String message, T body) {
        this.code = code;
        this.message = message;
        this.body = body;
    }

    /**
      * 返回成功消息
      */
    public static <T> Result<T> ok() {
        return ok(null);
    }

    /**
     * 返回成功消息
     *
     * @param body 数据
     * @return 成功消息
     */
    public static <T> Result<T> ok(T body) {
        return ok(ResultCode.SUCCESS.getMsg(), body);
    }

    /**
     * 返回成功消息
     *
     * @param message 状态描述
     * @param body    数据
     * @return 成功消息
     */
    public static <T> Result<T> ok(String message, T body) {
        return new Result<>(ResultCode.SUCCESS.getCode(), message, body);
    }

    /**
     * 返回失败消息
     *
     * @param message 状态描述
     * @return 失败消息
     */
    public static <T> Result<T> fail(String message) {
        return fail(ResultCode.FAIL.getCode(), message, null);
    }

    /**
     * 返回失败消息
     *
     * @param message 状态描述
     * @param body    数据
     * @return 失败消息
     */
    public static <T> Result<T> fail(String message, T body) {
        return fail(ResultCode.FAIL.getCode(), message, body);
    }

    /**
     * 返回失败消息
     *
     * @param code    状态码
     * @param message 状态描述
     * @return 失败消息
     */
    public static <T> Result<T> fail(Integer code, String message) {
        return fail(code, message, null);
    }

    /**
     * 返回失败消息
     *
     * @param resultCode 错误码枚举
     * @return
     */
    public static <T> Result<T> fail(ResultCode resultCode) {
        return fail(resultCode.getCode(), resultCode.getMsg());
    }

    /**
     * 返回失败消息
     *
     * @param resultCode 错误码枚举
     * @param body           数据
     * @return
     */
    public static <T> Result<T> fail(ResultCode resultCode, T body) {
        return fail(resultCode.getCode(), resultCode.getMsg(), body);
    }

    /**
     * 返回失败消息
     *
     * @param code    状态码
     * @param message 状态描述
     * @param body    数据
     * @return
     */
    public static <T> Result<T> fail(Integer code, String message, T body) {
        return new Result<>(code, message, body);
    }

    /**
     * 是否成功
     *
     * @return 结果
     */
    public boolean isSuccess() {
        return Objects.equals(this.code, ResultCode.SUCCESS.getCode());
    }
}


@Getter
@AllArgsConstructor
public enum ResultCode
{

    /**
     *
     */
    SUCCESS(200, "成功"),
    FAIL(101, "失败"),
    SYSTEM_BUSY(1000, "系统繁忙"),
    UNKNOWN(9999999, "未知错误");

    /**
     * 状态码
     */
    private final Integer code;
    /**
     * 状态描述
     */
    private final String msg;


    public static ResultCode of(Integer code) {
        return Arrays.stream(ResultCode.values()).filter(values -> values.code.equals(code)).findFirst().orElse(UNKNOWN);
    }

    public static ResultCode ofNull(Integer code) {
        return Arrays.stream(ResultCode.values()).filter(values -> values.code.equals(code)).findFirst().orElse(null);
    }

}

配置信息

application.yml
spring:
  application:
    name: emqx-example-service
server:
  port: 8080
  servlet:
    context-path: /emqx-example-service

application-dev.yml
emqx:
  sdk:
    enable: true
    host: 127.0.0.1
    tcpPort: 1883
    httpPort: 18083
    username: emqx-client
    password: a123456
    appId: 0c94e0423b409483
    appSecret: e379f64e96d383ced430804512c3e7f1
    ssl: false
    sysSubscribeTopics:
      - sys/+/+/thing/event/property/post

其它

源码地址Gitee
关注公众号【 java程序猿技术】获取EMQX实践系列文章

;