SpringBoot整合MQTT多租户

需要引入的JAR包

    <dependencies>

        <dependency>
            <groupId>org.springframework.integration</groupId>
            <artifactId>spring-integration-mqtt</artifactId>
        </dependency>

    </dependencies>

需要一个注解标注消费者处理器

/**
 * mqtt注解
 */
@Target(value = { ElementType.FIELD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Component
@Scope("prototype")
public @interface Mqtt {

  /**
   * 主题
   */
  String[] topics();

  /**
   * 消息质量
   */
  int qos() default 0;
}

其次就需要一个通用的消费处理器

public abstract class AbstractConsumer<T> implements IMqttMessageListener {

  protected final Logger logger = LoggerUtils.logger(getClass());

  @Override
  public void messageArrived(String topic, MqttMessage message) throws Exception {
    String json          = new String(message.getPayload(), StandardCharsets.UTF_8);
    try {
      if(checkMessageKey(topic,message)){
        throw new BaseException(MqErrorCode.MESSAGE_REPEAT_CONSUMPTION);
      }
      MqttModel mqttModel = JackJsonUtils.conversionClass(json, MqttModel.class);
      HeardHolder.setTenantCode(mqttModel.getTenantCode());
      logger.info("线程名:{},租户编码为:{},topic主题:{},AbstractConsumer:消费者消息: {}",mqttModel.getTenantCode(),Thread.currentThread().getName(),topic, json);
      // 消费者消费消息
      this.handleMessage((T) mqttModel.getBody());
    } catch (Throwable e){
      logger.info("AbstractConsumer:消费报错,消息为:{}, 异常为:",json, e);
      saveFailMessage(topic,message,e);
    }
  }

  /**
   * 消费方法
   * @param body 请求数据
   */
  public abstract void handleMessage(T body) throws Exception;

  /**
   * 保存消费失败的消息
   *
   * @param message mq所包含的信息
   * @param e 异常
   */
  public void saveFailMessage(String topic,MqttMessage message, Throwable e){

  }

  /**
   * 判断是否重复消费
   * @return true 重复消费 false 不重复消费
   */
  public boolean checkMessageKey(String topic, MqttMessage message){
    return false;
  }
}

然后就是发送mq消息的通用实体类

public class BaseMq<T> implements Serializable {

    /**
     * 消费者数据
     */
    private              T body;

    public BaseMq(){

    }

    public BaseMq(T body){
        this.body = body;
    }

    public T getBody() {
        return body;
    }

    public void setBody(T body) {
        this.body = body;
    }
}
/**
 * 基础发送MQ基类
 */
public class MqttModel<T> extends BaseMq<T> {

  private String topic;

  private String tenantCode;

  public String getTopic() {
    return topic;
  }

  public void setTopic(String topic) {
    this.topic = topic;
  }

  public String getTenantCode() {
    return tenantCode;
  }

  public void setTenantCode(String tenantCode) {
    this.tenantCode = tenantCode;
  }

  public MqttModel(String tenantCode) {
    super();
    this.tenantCode = tenantCode;
  }

  public MqttModel(String tenantCode,String topic,T body) {
    super(body);
    this.topic = topic;
    this.tenantCode = tenantCode;
  }

}

然后就是配置文件

public class MqttProfile {

  /**
   * 用户名
   */
  private String userName;

  /**
   * 密码
   */
  private String password;

  /**
   * 连接
   */
  private String url;

  /**
   * 客户端的标识(不可重复,为空时侯用uuid)
   */
  private String clientId;

  /**
   * 连接超时
   */
  private int completionTimeout = 30;

  /**
   * 是否自动重连
   */
  private boolean automaticReconnect = false;

  /**
   * 客户端掉线后,是否自动清除session
   */
  private boolean cleanSession = true;

  /**
   * 心跳时间
   */
  private int keepAliveInterval = 60;
  /**
   * 遗嘱消息
   */
  private MqttWill will;

  public static class MqttWill implements Serializable {

    /**
     * 遗嘱主题
     */
    private String topic;
    /**
     * 遗嘱消息
     */
    private String message;
    /**
     * 遗嘱消息质量
     */
    private int qos;

    /**
     * 是否保留消息
     */
    private boolean retained;

    public boolean getRetained() {
      return retained;
    }

    public void setRetained(boolean retained) {
      this.retained = retained;
    }

    public String getTopic() {
      return topic;
    }

    public void setTopic(String topic) {
      this.topic = topic;
    }

    public String getMessage() {
      return message;
    }

    public void setMessage(String message) {
      this.message = message;
    }

    public int getQos() {
      return qos;
    }

    public void setQos(int qos) {
      this.qos = qos;
    }
  }

  public MqttWill getWill() {
    return will;
  }

  public void setWill(MqttWill will) {
    this.will = will;
  }

  public int getKeepAliveInterval() {
    return keepAliveInterval;
  }

  public void setKeepAliveInterval(int keepAliveInterval) {
    this.keepAliveInterval = keepAliveInterval;
  }

  public boolean getCleanSession() {
    return cleanSession;
  }

  public void setCleanSession(boolean cleanSession) {
    this.cleanSession = cleanSession;
  }

  public boolean getAutomaticReconnect() {
    return automaticReconnect;
  }

  public void setAutomaticReconnect(boolean automaticReconnect) {
    this.automaticReconnect = automaticReconnect;
  }

  public String getUserName() {
    return userName;
  }

  public void setUserName(String userName) {
    this.userName = userName;
  }

  public String getPassword() {
    return password;
  }

  public void setPassword(String password) {
    this.password = password;
  }

  public String getUrl() {
    return url;
  }

  public void setUrl(String url) {
    this.url = url;
  }

  public String getClientId() {
    return clientId;
  }

  public void setClientId(String clientId) {
    this.clientId = clientId;
  }

  public int getCompletionTimeout() {
    return completionTimeout;
  }

  public void setCompletionTimeout(int completionTimeout) {
    this.completionTimeout = completionTimeout;
  }


}

这个是多租户的配置

@ConfigurationProperties("mqtt")
public class TenantMqttProfile {

  public Map<String,MqttProfile> tenant;

  public Map<String, MqttProfile> getTenant() {
    return tenant;
  }

  public void setTenant(Map<String, MqttProfile> tenant) {
    this.tenant = tenant;
  }
}

然后就是工具类

public interface SendService<T extends BaseMq> {

  /**
   * 发送消息
   */
  void send(T data) throws Exception;

  /**
   * 发送延缓信息
   */
  void sendDelay(T data, long delay) throws Exception;

  /**
   * 发送消息,并设置消息过期时间
   */
  void sendExpiration(T data, long expiration) throws Exception;
}
@Configuration
public class MqttUtils implements SendService<MqttModel> {

  private Logger logger = LoggerUtils.logger(MqttUtils.class);

  private Map<String,MqttClient> clientMap = new HashMap<>();

  private Map<String,MqttConnectOptions> optionsMap = new HashMap<>();

  public Map<String, MqttConnectOptions> getOptionsMap() {
    return optionsMap;
  }

  public void putOptionsMap(String tenantCode, MqttConnectOptions options) {
    this.optionsMap.put(tenantCode,options);
  }

  public Map<String, MqttClient> getClientMap() {
    return clientMap;
  }

  public void putClient(String tenantCode,MqttClient client) {
    this.clientMap.put(tenantCode, client);
  }

  /**
   *   发送消息
   *   @param data 消息内容
   */
  @Override
  public void send(MqttModel data) throws Exception {
    // 获取客户端实例
    ObjectMapper mapper = new ObjectMapper();
    try {
      // 转换消息为json字符串
      String json = mapper.writeValueAsString(data);
      getClientMap().get(data.getTenantCode()).getTopic(data.getTopic()).publish(new MqttMessage(json.getBytes(StandardCharsets.UTF_8)));
    } catch (JsonProcessingException e) {
      logger.error(String.format("MQTT: 主题[%s]发送消息转换json失败", data.getTopic()));
    } catch (MqttException e) {
      logger.error(String.format("MQTT: 主题[%s]发送消息失败", data.getTopic()));
    }
  }

  @Override
  public void sendDelay(MqttModel data, long delay) throws Exception {
    send(data);
  }

  @Override
  public void sendExpiration(MqttModel data, long expiration) throws Exception {
    send(data);
  }

  /**
   * 订阅消息
   * @param tenantCode 租户编码
   * @param topic 主题
   * @param qos 消息质量
   * @param consumer 消费者
   */
  public void subscribe(String tenantCode,String topic,int qos, IMqttMessageListener consumer) throws MqttException {
    if(ValidateUtils.isEmpty(topic)){
      return;
    }
    getClientMap().get(tenantCode).subscribe(topic, qos,consumer);
  }

  /**
   * 订阅消息
   * @param client mqtt连接
   */
  public void subscribe(MqttClient client) throws MqttException {
    List<Object> clazzList = new ArrayList<>(SpringUtil.getBeansWithAnnotation(Mqtt.class).values());
    if (ValidateUtils.isNotEmpty(clazzList)) {
      for (Object abstractConsumer : clazzList) {
        Mqtt mqtt = AnnotationUtils.findAnnotation(abstractConsumer.getClass(), Mqtt.class);
        if (ValidateUtils.isEmpty(mqtt)) {
          continue;
        }
        for (String topic : mqtt.topics()) {
          client.subscribe(topic, mqtt.qos(), (IMqttMessageListener) BeanUtil.copyProperties(abstractConsumer,abstractConsumer.getClass(), (String) null));
        }
      }
    }
  }

  /**
   * 取消订阅
   * @param topic 主题
   */
  public void unsubscribe(String tenantCode,String[] topic) throws MqttException {
    if(ValidateUtils.isEmpty(topic)){
      return;
    }
    getClientMap().get(tenantCode).unsubscribe(topic);
  }

  /**
   * 关闭连接
   */
  public void disconnect(String tenantCode) throws MqttException {
    getClientMap().get(tenantCode).disconnect();
  }

  /**
   * 重新连接
   */
  public void reconnect(String tenantCode) throws MqttException {
    MqttClient client = getClientMap().get(tenantCode);
    if(!client.isConnected()){
      client.connect(getOptionsMap().get(tenantCode));
      subscribe(client);
    }
  }

  public MqttConnectOptions initMqttConnectOptions(MqttProfile mqttProfile) {
    MqttConnectOptions mqttConnectOptions = new MqttConnectOptions();
    mqttConnectOptions.setUserName(mqttProfile.getUserName());
    mqttConnectOptions.setPassword(mqttProfile.getPassword().toCharArray());
    mqttConnectOptions.setServerURIs(new String[]{mqttProfile.getUrl()});
    //设置同一时间可以发送的最大未确认消息数量
    mqttConnectOptions.setMaxInflight(mqttProfile.getMaxInflight());
    //设置超时时间
    mqttConnectOptions.setConnectionTimeout(mqttProfile.getCompletionTimeout());
    //设置自动重连
    mqttConnectOptions.setAutomaticReconnect(mqttProfile.getAutomaticReconnect());
    //cleanSession 设为 true;当客户端掉线时;服务器端会清除 客户端session;重连后 客户端会有一个新的session,cleanSession
    // 设为false,客户端掉线后 服务器端不会清除session,当重连后可以接收之前订阅主题的消息。当客户端上线后会接受到它离线的这段时间的消息
    mqttConnectOptions.setCleanSession(mqttProfile.getCleanSession());
    // 设置会话心跳时间 单位为秒   设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送心跳判断客户端是否在线,但这个方法并没有重连的机制
    mqttConnectOptions.setKeepAliveInterval(mqttProfile.getKeepAliveInterval());
    // 设置重新连接之间等待的最长时间
    mqttConnectOptions.setMaxReconnectDelay(mqttProfile.getMaxReconnectDelay());
    // 设置连接超时值,该值以秒为单位 0 禁用超时处理,这意味着客户端将等待,直到网络连接成功或失败.
    mqttConnectOptions.setConnectionTimeout(mqttProfile.getConnectionTimeout());
    // 设置执行器服务应等待的时间(以秒为单位)在强制终止之前终止.不建议更改除非您绝对确定需要,否则该值.
    mqttConnectOptions.setExecutorServiceTimeout(mqttProfile.getExecutorServiceTimeout());
    //设置遗嘱消息
    if (ValidateUtils.isNotEmpty(mqttProfile.getWill())) {
      MqttWill will = mqttProfile.getWill();
      mqttConnectOptions.setWill(will.getTopic(), will.getMessage().getBytes(), will.getQos(), will.getRetained());
    }
    return mqttConnectOptions;
  }
}

最后一步就是需要一个初始化配置的代码了

@Configuration
@IntegrationComponentScan
@EnableConfigurationProperties(value={TenantMqttProfile.class,})
@Import(value = {MqttUtils.class})
public class MqttConfig {

  private final TenantMqttProfile profile;

  private final MqttUtils mqttUtils;

  public MqttConfig(TenantMqttProfile profile, ApplicationContext applicationContext, MqttUtils mqttUtils) {
    this.profile = profile;
    this.mqttUtils = mqttUtils;
    SpringUtil.setContext(applicationContext);
  }

  @PostConstruct
  public void afterPropertiesSet() throws Exception {
    Map<String,MqttProfile> tenantProfileMap = profile.getTenant();
    if(ValidateUtils.isEmpty(tenantProfileMap)){
      return ;
    }
    Map<String, MqttInitService> abstractMQMap = SpringUtil.getBeansOfType(MqttInitService.class);
    MqttInitService mqttInitService = ValidateUtils.isNotEmpty(abstractMQMap) ? abstractMQMap.values().stream().findFirst().get() : new DefaultMqttInitService(mqttUtils);
    for(Entry<String,MqttProfile> entry: tenantProfileMap.entrySet()){
      mqttInitService.initMqttClient(entry.getKey(),entry.getValue());
    }
  }

  @Bean
  @ConditionalOnMissingBean(MessageChannel.class)
  public MessageChannel mqttInputChannel() {
    return new DirectChannel();
  }

然后配置一个初始化接口,如果使用者没有实现,则使用默认的初始化方法,默认初始化方法会在重连成功时候,重新订阅主题

public interface MqttInitService {

    void initMqttClient(String tenantCode, MqttProfile mqttProfile) throws Exception;
}
public class DefaultMqttInitService implements MqttInitService {

    private final Logger logger = LoggerUtils.logger(DefaultMqttInitService.class);

    private final MqttUtils utils;

    public DefaultMqttInitService(MqttUtils utils) {
        this.utils = utils;
    }

    @Override
    public void initMqttClient(String tenantCode, MqttProfile mqttProfile) throws Exception {
        MqttClient mqttClient = new MqttClient(mqttProfile.getUrl(), ValidateUtils.getOrDefault(mqttProfile.getClientId(), UUID.randomUUID().toString()));
        MqttConnectOptions options = utils.initMqttConnectOptions(mqttProfile);
        mqttClient.connect(options);
        utils.putOptionsMap(tenantCode,options);
        // 订阅主题
        utils.subscribe(mqttClient);

        //配置callback
        mqttClient.setCallback(new MqttCallbackExtended() {
            @Override
            public void connectComplete(boolean reconnect, String serverURI) {
                logger.info("租户:{} 重连{}",tenantCode,reconnect ? "成功" : "失败");
                if(reconnect){
                    List<Object> clazzList = new ArrayList<>(SpringUtil.getBeansWithAnnotation(Mqtt.class).values());
                    for (Object abstractConsumer : clazzList) {
                        Mqtt mqtt = AnnotationUtils.findAnnotation(abstractConsumer.getClass(), Mqtt.class);
                        if (ValidateUtils.isNotEmpty(mqtt)) {
                            try {
                                for(String topic : mqtt.topics()){
                                    logger.info("租户:{} 重新订阅[{}]主题",tenantCode,topic);
                                    mqttClient.subscribe(topic, mqtt.qos(), (IMqttMessageListener) BeanUtil.copyProperties(abstractConsumer,abstractConsumer.getClass(), (String) null));
                                }
                            } catch (MqttException e) {
                                logger.error("重连重新订阅主题失败,异常为:",e);
                            }
                        }
                    }
                }
            }

            @Override
            public void connectionLost(Throwable cause) {
                logger.info("租户:{} 断开连接,异常为:",tenantCode,cause);
            }

            @Override
            public void messageArrived(String topic, MqttMessage message) throws Exception {

            }

            @Override
            public void deliveryComplete(IMqttDeliveryToken token) {

            }
        });
        //保存client
        utils.putClient(tenantCode,mqttClient);
    }
}

基本上的整合流程就是这样子的了

如果需要看到使用配置可以参考如下

mqtt:
  tenant:
    default:               #租户编码
      user-name:           #用户名
      password:            #密码
      url:                 #连接
      client-id:           #客户端的标识(不可重复,为空时侯用uuid)
      completion-timeout:  #连接超时
      automatic-reconnect: #是否自动重连
      clean-session:       #客户端掉线后,是否自动清除session
      keep-alive-interval: #心跳时间
      will:                #遗嘱消息
        topic:             #遗嘱主题
        message:           #遗嘱消息
        qos:               #遗嘱消息质量
        retained:          #遗嘱是否保留消息

消费者实例

@Mqtt(qos = 2,topics = "test")
@Scope("prototype")
public class TestHandler extends AbstractConsumer {


  @Override
  public void handleMessage(String body) throws Exception {
     //这里处理业务逻辑       
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值