自定義消息隊列組件 mq-spring-boot-starter

爲什麼要自定義

爲什麼要自定義消息隊列組件 mq-spring-boot-starter ?

考慮到公司同一套系統能支持阿里雲上部署和客戶內網部署,業務代碼不修改的情況下,只修改yml文件配置屬性即可遷移。

本文主要針對RocketMq 進行構建,後期增加 RabbitMq的支持。

本組件源碼:
https://github.com/dwhgygzt/mq-spring-boot-starter

準備工作

首先請確保你已經掌握了SpringBoot 如何自定義一個starter,還沒有明白的請查看博文:
https://blog.csdn.net/gzt19881123/article/details/106456362

架構構思

想要做到業務代碼不修改,那麼必須創建一套自己的消息服務接口和屬性對象,封裝具體的消息隊列產品業務邏輯。
充分比對和分析各消息隊列產品的相同業務邏輯,抽取出公共接口。

RocketMq還有半消息機制需要考慮進去。

工程整體目錄結構如下:
在這裏插入圖片描述

消息對象抽取

這裏目前只針對 主題類消息(Topic)進行架構,創建出自己的 消息對象。
主要參考 ApacheRocketMq 的源碼

首先創建 消息體類的公共接口:

package com.middol.starter.mq.pojo;

import java.io.Serializable;

/**
 * 公共抽象消息體 Message
 *
 * @author <a href="mailto:[email protected]">guzhongtao</a>
 */
public interface Message extends Serializable {

    /**
     * 獲得消息體ID
     * @return String
     */
    String getMessageId();


    /**
     * 設置消息體ID
     *
     * @param messageId 消息唯一id
     */
    void setMessageId(String messageId);

    /**
     * 獲得消息體
     *
     * @return byte[]
     */
    byte[] getMessageBody();

    /**
     * 設置消息體
     *
     * @param messageBody 消息體 byte[]
     */
    void setMessageBody(byte[] messageBody);


}

然後創建 主題類消息體對象


package com.middol.starter.mq.pojo;

import java.util.Properties;

/**
 * 公共Topic消息體 Message
 *
 * @author <a href="mailto:[email protected]">guzhongtao</a>
 */
public class TopicMessage implements Message {
    private static final long serialVersionUID = 1L;

    /**
     * 用戶其他屬性
     */
    private Properties userProperties;

    /**
     * <p>
     *   消息唯一主鍵.
     * </p>
     *
     * <p>
     *   <strong>由具體mq產品生成</strong>
     * </p>
     */
    private String messageId;

    /**
     * <p>
     *   消息主題名稱, 最長不超過255個字符; 由a-z, A-Z, 0-9, 以及中劃線"-"和下劃線"_"構成.
     * </p>
     *
     * <p>
     *   <strong>一條合法消息本成員變量不能爲空</strong>
     * </p>
     */
    private String topicName;

    /**
     * <p>
     *   消息標籤, 合法標識符, 儘量簡短且見名知意.
     * </p>
     *
     * <p>
     *   建議傳遞該值
     * </p>
     */
    private String tags;

    /**
     * <p>
     *   業務主鍵,例如商戶訂單號.
     * </p>
     *
     * <p>
     *   建議傳遞該值
     * </p>
     */
    private String bussinessKey;

    /**
     * <p>
     *    消息體, 消息體長度默認不超過4M, 具體請參閱集羣部署文檔描述.
     * </p>
     *
     * <p>
     *    <strong>一條合法消息本成員變量不能爲空</strong>
     * </p>
     */
    private byte[] messageBody;

    /**
     * 默認構造函數; 必要屬性後續通過Set方法設置.
     */
    public TopicMessage() {
        this(null, null, "", null);
    }

    /**
     * 有參構造函數.
     * @param topicName 消息主題
     * @param tags 消息標籤
     * @param bussinessKey 業務主鍵
     * @param messageBody 消息體
     */
    public TopicMessage(String topicName, String tags, String bussinessKey, byte[] messageBody) {
        this.topicName = topicName;
        this.tags = tags;
        this.bussinessKey = bussinessKey;
        this.messageBody = messageBody;
    }

    /**
     * 有參構造函數.
     * @param messageId 唯一主鍵
     * @param topicName 消息主題
     * @param tags 消息標籤
     * @param bussinessKey 業務主鍵
     * @param messageBody 消息體
     */
    public TopicMessage(String messageId,String topicName, String tags, String bussinessKey, byte[] messageBody) {
        this.messageId = messageId;
        this.topicName = topicName;
        this.tags = tags;
        this.bussinessKey = bussinessKey;
        this.messageBody = messageBody;
    }

    @Override
    public String toString() {
        return "TopicMessage [topicName=" + topicName + ", tags=" + tags + ", messageBody=" + (messageBody != null ? messageBody.length : 0) + "]";
    }

    public String getTopicName() {
        return topicName;
    }

    public void setTopicName(String topicName) {
        this.topicName = topicName;
    }

    public String getTags() {
        return tags;
    }

    public void setTags(String tags) {
        this.tags = tags;
    }

    public String getBussinessKey() {
        return bussinessKey;
    }

    public void setBussinessKey(String bussinessKey) {
        this.bussinessKey = bussinessKey;
    }

    @Override
    public String getMessageId() {
        return messageId;
    }

    @Override
    public void setMessageId(String messageId) {
        this.messageId = messageId;
    }

    @Override
    public byte[] getMessageBody() {
        return messageBody;
    }

    @Override
    public void setMessageBody(byte[] messageBody) {
        this.messageBody = messageBody;
    }

    public Properties getUserProperties() {
        return userProperties;
    }

    public void setUserProperties(Properties userProperties) {
        this.userProperties = userProperties;
    }

    /**
     * 添加用戶自定義屬性鍵值對; 該鍵值對在消費消費時可被獲取.
     * @param key 自定義鍵
     * @param value 對應值
     */
    public void putUserProperties(final String key, final String value) {
        if (null == this.userProperties) {
            this.userProperties = new Properties();
        }

        if (key != null && value != null) {
            this.userProperties.put(key, value);
        }
    }

    /**
     * 獲取用戶自定義鍵的值
     * @param key 自定義鍵
     * @return 用戶自定義鍵值
     */
    public String getUserProperties(final String key) {
        if (null != this.userProperties) {
            return (String) this.userProperties.get(key);
        }

        return null;
    }
}

當然還包括 消息處理的狀態對象:

package com.middol.starter.mq.pojo;

/**
 * 消費消息的返回結果
 *
 * @author <a href="mailto:[email protected]">guzhongtao</a>
 */
public enum MessageStatus {
    /**
     * 消費成功,繼續消費下一條消息
     */
    CommitMessage,
    /**
     * 消費失敗,告知服務器稍後再投遞這條消息,繼續消費其他消息
     */
    ReconsumeLater,
}

消息訂閱和發佈者抽取

訂閱者

package com.middol.starter.mq.service;

import com.middol.starter.mq.pojo.MessageStatus;
import com.middol.starter.mq.pojo.TopicMessage;

/**
 * MQ對應的監聽者,實現具體的消費業務
 * 訂閱(subscribe)模式.
 * 訂閱關係一致 https://help.aliyun.com/document_detail/43523.html?spm=a2c4g.11186623.6.734.60b94c07Uwhsky
 * 1.訂閱的 Topic 必須一致
 * 2.訂閱的 Topic 中的 Tag 必須一致
 *
 * @author <a href="mailto:[email protected]">guzhongtao</a>
 */
public interface TopicListener {

    /**
     * 對應的消費者,例如 aliyunrocketmq 中的groupId
     *
     * @return SubscriberBeanName
     */
    String getSubscriberBeanName();

    /**
     * 訂閱的topic
     *
     * @return topic
     */
    String getTopicName();

    /**
     * 訂閱的 tag
     *
     * @return 訂閱過濾表達式字符串,ONS服務器依據此表達式進行過濾。只支持或運算<br>
     * eg: "tag1 || tag2 || tag3"<br>
     * 如果subExpression等於null或者*,則表示全部訂閱
     */
    String getTagExpression();

    /**
     * 消息訂閱
     *
     * @param topicMessage 從消息服務器獲得的訂閱消息
     * @return 執行完本地業務邏輯反饋消息服務器是否消費完畢 MessageStatus
     */
    MessageStatus subscribe(TopicMessage topicMessage);

}

發佈者

package com.middol.starter.mq.service;

import com.middol.starter.mq.pojo.TopicMessage;
import com.middol.starter.mq.pojo.TopicMessageSendResult;

/**
 * 推送消息到MQ服務端
 * 發佈(pub)模式
 *
 * @author <a href="mailto:[email protected]">guzhongtao</a>
 */
public interface TopicPublisher extends Admin {

    /**
     * 同步推送消息
     *
     * @param topicMessage 消息對象
     * @return TopicMessageSendResult
     */
    TopicMessageSendResult publish(TopicMessage topicMessage);

    /**
     * 異步推送消息
     *
     * @param topicMessage      消息對象
     * @param topicSendCallback 異步結果處理
     */
    void publishAsync(TopicMessage topicMessage, TopicSendCallback topicSendCallback);

}


接口具體實現,需要分 阿里雲的 和 apache 的
在這裏插入圖片描述
具體實現舉一個例子查看, 具體源碼請參考最後的 githug地址。


package com.middol.starter.mq.service.impl.aliyun;

import com.aliyun.openservices.ons.api.*;
import com.middol.starter.mq.exception.TopicMqException;
import com.middol.starter.mq.pojo.TopicMessage;
import com.middol.starter.mq.pojo.TopicMessageSendResult;
import com.middol.starter.mq.service.TopicPublisher;
import com.middol.starter.mq.service.TopicSendCallback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 阿里雲推送消息到MQ服務端
 * 發佈(pub)模式
 *
 * @author <a href="mailto:[email protected]">guzhongtao</a>
 */
public class AliyunSimpleRocketMqPublisher implements TopicPublisher {

    private Logger logger = LoggerFactory.getLogger(this.getClass());
    /**
     * 阿里雲 rocketmq producer
     */
    Producer producer;

    String beanName;

    public AliyunSimpleRocketMqPublisher(Producer producer,String beanName) {
        this.producer = producer;
        this.beanName = beanName;
    }

    public AliyunSimpleRocketMqPublisher() {
    }

    @Override
    public TopicMessageSendResult publish(TopicMessage topicMessage) {
        Message message = new Message();
        message.setUserProperties(topicMessage.getUserProperties());
        message.setKey(topicMessage.getBussinessKey());
        message.setBody(topicMessage.getMessageBody());
        message.setTag(topicMessage.getTags());
        message.setTopic(topicMessage.getTopicName());
        SendResult sendResult = producer.send(message);
        TopicMessageSendResult topicMessageSendResult = new TopicMessageSendResult();
        topicMessageSendResult.setMessageId(sendResult.getMessageId());
        topicMessageSendResult.setTopicName(sendResult.getTopic());
        return topicMessageSendResult;
    }

    @Override
    public void publishAsync(TopicMessage topicMessage, TopicSendCallback topicSendCallback) {
        Message message = new Message();
        message.setUserProperties(topicMessage.getUserProperties());
        message.setKey(topicMessage.getBussinessKey());
        message.setBody(topicMessage.getMessageBody());
        message.setTag(topicMessage.getTags());
        message.setTopic(topicMessage.getTopicName());
        producer.sendAsync(message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                TopicMessageSendResult topicMessageSendResult = new TopicMessageSendResult();
                topicMessageSendResult.setTopicName(sendResult.getTopic());
                topicMessageSendResult.setMessageId(sendResult.getMessageId());
                topicSendCallback.onSuccess(topicMessageSendResult);
            }

            @Override
            public void onException(OnExceptionContext context) {
                TopicMqException topicMqException = new TopicMqException(context.getException());
                topicMqException.setTopicName(context.getTopic());
                topicMqException.setMessageId(context.getMessageId());
                topicSendCallback.onFail(topicMqException);
            }
        });
    }

    @Override
    public boolean isStarted() {
        return producer.isStarted();
    }

    @Override
    public boolean isClosed() {
        return producer.isClosed();
    }

    @Override
    public void start() {
        logger.info("【MQ】AliyunSimpleRocketMqPublisher["+beanName+"] start...");
        producer.start();
    }

    @Override
    public void close() {
        logger.info("【MQ】AliyunSimpleRocketMqPublisher[" + beanName + "] close...");
        producer.shutdown();
    }

    public String getBeanName() {
        return beanName;
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }

    public Producer getProducer() {
        return producer;
    }

    public void setProducer(Producer producer) {
        this.producer = producer;
    }

}


package com.middol.starter.mq.service.impl.aliyun;

import com.aliyun.openservices.ons.api.Action;
import com.aliyun.openservices.ons.api.Consumer;
import com.middol.starter.mq.pojo.MessageStatus;
import com.middol.starter.mq.pojo.TopicMessage;
import com.middol.starter.mq.service.TopicListener;
import com.middol.starter.mq.service.TopicSubscriber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 阿里雲訂閱消息
 *
 * @author <a href="mailto:[email protected]">guzhongtao</a>
 */
public class AliyunSimpleRocketMqSubscriber implements TopicSubscriber {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 阿里雲 rocketMq消費服務
     */
    Consumer consumer;

    String beanName;

    public AliyunSimpleRocketMqSubscriber(Consumer consumer, String beanName) {
        this.consumer = consumer;
        this.beanName = beanName;
    }

    public AliyunSimpleRocketMqSubscriber() {
    }

    @Override
    public void subscribe(String topic, String tagExpression, TopicListener listener) {
        consumer.subscribe(topic, tagExpression, (message, context) -> {
            TopicMessage topicMessage = new TopicMessage();
            topicMessage.setUserProperties(message.getUserProperties());
            topicMessage.setBussinessKey(message.getKey());
            topicMessage.setMessageBody(message.getBody());
            topicMessage.setTags(message.getTag());
            topicMessage.setTopicName(message.getTopic());

            MessageStatus messageStatus = listener.subscribe(topicMessage);
            if (messageStatus.equals(MessageStatus.CommitMessage)) {
                return Action.CommitMessage;
            } else {
                return Action.ReconsumeLater;
            }
        });
    }

    @Override
    public void unsubscribe(String topicName) {
        consumer.unsubscribe(topicName);
    }

    @Override
    public boolean isStarted() {
        return consumer.isStarted();
    }

    @Override
    public boolean isClosed() {
        return consumer.isClosed();
    }

    @Override
    public void start() {
        logger.info("【MQ】AliyunSimpleRocketMqSubscriber[" + beanName + "] start...");
        consumer.start();
    }

    @Override
    public void close() {
        logger.info("【MQ】AliyunSimpleRocketMqSubscriber[" + beanName + "] close...");
        consumer.shutdown();
    }

    public Consumer getConsumer() {
        return consumer;
    }

    public void setConsumer(Consumer consumer) {
        this.consumer = consumer;
    }

    public String getBeanName() {
        return beanName;
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }
}

這裏的架構思想很簡單,其實就是在阿里雲 或 Apahce rocketmq SDK 的基礎上在套一層自己接口的殼子就行了。

如何使用

用戶在具體自己項目中使用該消息組件時,可根據自己依賴的消息隊列產品配置即可:
如果使用阿里雲的rocketmq 可在yml文件中配置如下:

mq:
    aliyun:
      rocketmq:
        enable: true
        access-key-id: xxxxxx
        access-key-secret: bbbbb
        name-server-addr: http://MQ_INST_1666017288766448_BcidZfUM.mq-internet-access.mq-internet.aliyuncs.com:80
        publishers:
          - {beanName: publishService1, groupId: GID_SAAS_DEV, sendMsgTimeoutMillis: 5000}
          - {beanName: xaPublishService1, groupId: GID_XA_SAAS_DEV, sendMsgTimeoutMillis: 5000, messageType: TRANSACTION, checkImmunityTimeInSeconds: 5}
        subscribers:
          - {beanName: subscriberService1, groupId: GID_SAAS_DEV}
          - {beanName: xaSubscriberService1, groupId: GID_XA_SAAS_DEV}

如果你要使用Apache的 RocketMq 則將配置修改如下:

    apache:
      rocketmq:
        enable: true
        access-key-id: xxxxx
        access-key-secret: bbbb
        name-server-addr: localhost:9876
        publishers:
          - {beanName: publishService1, groupId: GID_SAAS_DEV, sendMsgTimeout: 5000}
          - {beanName: xaPublishService1, groupId: GID_XA_SAAS_DEV, sendMsgTimeout: 5000, messageType: TRANSACTION}
        subscribers:
          - {beanName: subscriberService1, groupId: GID_SAAS_DEV}
          - {beanName: xaSubscriberService1, groupId: GID_XA_SAAS_DEV}

在 java代碼中, 消息發佈如下:

package com.middol.mytest.controller;

import cn.hutool.core.util.StrUtil;
import com.middol.mytest.config.mq.localtransactionexecuter.MyXaTopicLocalTransactionExecuter;
import com.middol.starter.mq.pojo.TopicMessage;
import com.middol.starter.mq.pojo.XaTopicMessage;
import com.middol.starter.mq.service.TopicPublisher;
import com.middol.starter.mq.service.XaTopicPublisher;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;

/**
 * 測試 mq
 *
 * @author <a href="mailto:[email protected]">guzhongtao</a>
 */
@RestController
@RequestMapping("/api/mq")
public class MqTestController {

    /**
     * 普通類型的消息發送服務端 ResourceName 對應yml文件中配置
     */
    @Lazy
    @Resource(name = "publishService1")
    private TopicPublisher topicPublisher;

    /**
     * 事務類型的消息發送服務端 ResourceName 對應yml文件中配置
     */
    @Lazy
    @Resource(name = "xaPublishService1")
    private XaTopicPublisher xaTopicPublisher;

    /**
     * 普通消息發送測試.
     *
     * @param message 消息體
     * @return 發送成功
     */
    @PostMapping("singlePush")
    public String singlePush(String message) {
        if (StrUtil.isEmpty(message)) {
            return "消息體不能爲空";
        }

        TopicMessage msg = new TopicMessage();
        msg.setTopicName("SAAS-PT");
        msg.setTags("TAG1");
        msg.setBussinessKey(System.currentTimeMillis() + "");
        msg.setMessageBody(message.getBytes(StandardCharsets.UTF_8));
        topicPublisher.publish(msg);

        return "發送成功";
    }

    /**
     * 半消息事務類型消息發送
     *
     * @param message  消息體
     * @param isCommit 是否要提交測試 true 提交  false 不提交,回滾
     * @return 發送成功
     */
    @PostMapping("xaPush")
    public String xaPush(String message, Boolean isCommit) {
        if (StrUtil.isEmpty(message)) {
            return "消息體不能爲空";
        }

        XaTopicMessage msg = new XaTopicMessage();
        msg.setLocalTransactionExecuterId(MyXaTopicLocalTransactionExecuter.EXECUTER_ID);
        msg.setTopicName("XA-SAAS");
        msg.setTags("TAG1");
        msg.setBussinessKey(System.currentTimeMillis() + "");
        msg.setMessageBody(message.getBytes(StandardCharsets.UTF_8));
        xaTopicPublisher.publishInTransaction(msg, isCommit);

        return "發送成功";
    }

}

消息訂閱如下:


package com.middol.mytest.config.mq.listener;

import com.middol.starter.mq.pojo.MessageStatus;
import com.middol.starter.mq.pojo.TopicMessage;
import com.middol.starter.mq.service.TopicListener;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;

/**
 * 消息監聽者
 *
 * @author guzt
 */
@Component
public class MyMessageListenerService implements TopicListener {
    @Override
    public String getSubscriberBeanName() {
        return "subscriberService1";
    }

    @Override
    public String getTopicName() {
        return "SAAS-PT";
    }

    @Override
    public String getTagExpression() {
        return "*";
    }

    @Override
    public MessageStatus subscribe(TopicMessage topicMessage) {

        System.out.println("消費消息 message body = " + new String(topicMessage.getMessageBody(), StandardCharsets.UTF_8));

        return MessageStatus.CommitMessage;
    }
}

上面只是簡單說明了一下大致思路,代碼只貼出一點,具體源碼請移步github,大家一起交流:

https://github.com/dwhgygzt/mq-spring-boot-starter

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章