爲什麼要自定義
爲什麼要自定義消息隊列組件 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,大家一起交流: