不要以爲這只是spring boot與RocketMQ的簡單整合,本篇文章還爲各位看官呈現以下知識點的最佳實踐:
- 自定義一個spring boot 的starter
- 使用spring的事件傳播機制實現bean與bean之間基於事件驅動的通信
- 自定義註解、組合註解
先來撩點故事背景^_^
編寫spring-boot-starter-rocketmq
創建一個Maven項目名字就叫spring-boot-starter-rocketmq,其pom.xml文件內容如下:
<?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">
<groupId>com.bqjr</groupId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-starter-rocketmq</name>
<description>Starter for using RocketMQ</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<rocketmq.version>4.0.0-incubating</rocketmq.version>
</properties>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-starter-rocketmq</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- RocketMq客戶端相關依賴 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>${rocketmq.version}</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-common</artifactId>
<version>${rocketmq.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version><!--$NO-MVN-MAN-VER$-->
</dependency>
</dependencies>
</project>
編寫配置類RocketmqProperties,這個類的屬性對應application.properties文件中的配置項,目前只提供核心的一些配置支持,其他性能優化方面的配置參數可自行擴展
/**
* @author jiangjb
*/
@Data
@ConfigurationProperties(PREFIX)
public class RocketmqProperties {
public static final String PREFIX = "spring.extend.rocketmq";
private String namesrvAddr;
private String instanceName;
private String clientIP;
private ProducerConfig producer;
private ConsumerConfig consumer;
}
編寫配置解析類RocketmqAutoConfiguration,這個類主要初始化了三個Bean:defaultProducer用來發送普通消息、transactionProducer用來發送事務消息以及pushConsumer用來接收訂閱的所有topic下的消息,並派發給不同的tag的消費者。/**
* @author jiangjb
*/
@Configuration
@EnableConfigurationProperties(RocketmqProperties.class)
@ConditionalOnProperty(prefix = PREFIX, value = "namesrvAddr")
public class RocketmqAutoConfiguration {
@Autowired
private RocketmqProperties properties;
@Value("${spring.application.name}")
private String producerGroupName;
@Value("${spring.application.name}")
private String consumerGroupName;
@Autowired
private ApplicationEventPublisher publisher;
/**
* 初始化向rocketmq發送普通消息的生產者
*/
@Bean
@ConditionalOnProperty(prefix = PREFIX, value = "producer.instanceName")
public DefaultMQProducer defaultProducer() throws MQClientException{
/**
* 一個應用創建一個Producer,由應用來維護此對象,可以設置爲全局對象或者單例<br>
* 注意:ProducerGroupName需要由應用來保證唯一<br>
* ProducerGroup這個概念發送普通的消息時,作用不大,但是發送分佈式事務消息時,比較關鍵,
* 因爲服務器會回查這個Group下的任意一個Producer
*/
DefaultMQProducer producer = new DefaultMQProducer(producerGroupName);
producer.setNamesrvAddr(properties.getNamesrvAddr());
producer.setInstanceName(properties.getProducer().getInstanceName());
producer.setVipChannelEnabled(false);
/**
* Producer對象在使用之前必須要調用start初始化,初始化一次即可<br>
* 注意:切記不可以在每次發送消息時,都調用start方法
*/
producer.start();
System.out.println("RocketMq defaultProducer Started.");
return producer;
}
/**
* 初始化向rocketmq發送事務消息的生產者
*/
@Bean
@ConditionalOnProperty(prefix = PREFIX, value = "producer.tranInstanceName")
public TransactionMQProducer transactionProducer() throws MQClientException{
/**
* 一個應用創建一個Producer,由應用來維護此對象,可以設置爲全局對象或者單例<br>
* 注意:ProducerGroupName需要由應用來保證唯一<br>
* ProducerGroup這個概念發送普通的消息時,作用不大,但是發送分佈式事務消息時,比較關鍵,
* 因爲服務器會回查這個Group下的任意一個Producer
*/
TransactionMQProducer producer = new TransactionMQProducer("TransactionProducerGroupName");
producer.setNamesrvAddr(properties.getNamesrvAddr());
producer.setInstanceName(properties.getProducer().getTranInstanceName());
// 事務回查最小併發數
producer.setCheckThreadPoolMinSize(2);
// 事務回查最大併發數
producer.setCheckThreadPoolMaxSize(2);
// 隊列數
producer.setCheckRequestHoldMax(2000);
//TODO 由於社區版本的服務器閹割調了消息回查的功能,所以這個地方沒有意義
//TransactionCheckListener transactionCheckListener = new TransactionCheckListenerImpl();
//producer.setTransactionCheckListener(transactionCheckListener);
/**
* Producer對象在使用之前必須要調用start初始化,初始化一次即可<br>
* 注意:切記不可以在每次發送消息時,都調用start方法
*/
producer.start();
System.out.println("RocketMq TransactionMQProducer Started.");
return producer;
}
/**
* 初始化rocketmq消息監聽方式的消費者
*/
@Bean
@ConditionalOnProperty(prefix = PREFIX, value = "consumer.instanceName")
public DefaultMQPushConsumer pushConsumer() throws MQClientException{
/**
* 一個應用創建一個Consumer,由應用來維護此對象,可以設置爲全局對象或者單例<br>
* 注意:ConsumerGroupName需要由應用來保證唯一
*/
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(consumerGroupName);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
consumer.setNamesrvAddr(properties.getNamesrvAddr());
consumer.setInstanceName(properties.getConsumer().getInstanceName());
consumer.setConsumeMessageBatchMaxSize(1);//設置批量消費,以提升消費吞吐量,默認是1
/**
* 訂閱指定topic下tags
*/
List<String> subscribeList = properties.getConsumer().getSubscribe();
for (String sunscribe : subscribeList) {
consumer.subscribe(sunscribe.split(":")[0], sunscribe.split(":")[1]);
}
consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
MessageExt msg = msgs.get(0);
try {
//默認msgs裏只有一條消息,可以通過設置consumeMessageBatchMaxSize參數來批量接收消息
System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs.size());
//發佈消息到達的事件,以便分發到每個tag的監聽方法
this.publisher.publishEvent(new RocketmqEvent(msg,consumer));
System.out.println("消息到達事件已經發布成功!");
} catch (Exception e) {
e.printStackTrace();
if(msg.getReconsumeTimes()<=3){//重複消費3次
//TODO 進行日誌記錄
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
} else {
//TODO 消息消費失敗,進行日誌記錄
}
}
//如果沒有return success,consumer會重複消費此信息,直到success。
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);//延遲5秒再啓動,主要是等待spring事件監聽相關程序初始化完成,否則,回出現對RocketMQ的消息進行消費後立即發佈消息到達的事件,然而此事件的監聽程序還未初始化,從而造成消息的丟失
/**
* Consumer對象在使用之前必須要調用start初始化,初始化一次即可<br>
*/
try {
consumer.start();
} catch (Exception e) {
System.out.println("RocketMq pushConsumer Start failure!!!.");
e.printStackTrace();
}
System.out.println("RocketMq pushConsumer Started.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
return consumer;
}
}
編寫基於spring事件傳播機制的事件類RocketmqEvent,用來定義上面的consumer接收到消息後的發佈的事件。
/**
*
* @author jiangjb
*
*/
@Data
@EqualsAndHashCode(callSuper=false)
public class RocketmqEvent extends ApplicationEvent{
private static final long serialVersionUID = -4468405250074063206L;
private DefaultMQPushConsumer consumer;
private MessageExt messageExt;
private String topic;
private String tag;
private byte[] body;
public RocketmqEvent(MessageExt msg,DefaultMQPushConsumer consumer) throws Exception {
super(msg);
this.topic = msg.getTopic();
this.tag = msg.getTags();
this.body = msg.getBody();
this.consumer = consumer;
this.messageExt = msg;
}
public String getMsg() {
try {
return new String(this.body,"utf-8");
} catch (UnsupportedEncodingException e) {
return null;
}
}
public String getMsg(String code) {
try {
return new String(this.body,code);
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
然後運行maven的編譯、打包編寫測試項目rocketmq-starter-test
<dependency>
<groupId>com.bqjr</groupId>
<artifactId>spring-boot-starter-rocketmq</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
發送消息測試類producerDemo/**
*
* @author jiangjb
*
*/
@RestController
public class producerDemo {
@Autowired
private DefaultMQProducer defaultProducer;
@Autowired
private TransactionMQProducer transactionProducer;
@Value("${spring.extend.rocketmq.producer.topic}")
private String producerTopic;
@RequestMapping(value = "/sendMsg", method = RequestMethod.GET)
public void sendMsg() {
Message msg = new Message(producerTopic,// topic
"TagA",// tag
"OrderID001",// key
("Hello jyqlove333").getBytes());// body
try {
defaultProducer.send(msg,new SendCallback(){
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
//TODO 發送成功處理
}
@Override
public void onException(Throwable e) {
System.out.println(e);
//TODO 發送失敗處理
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
@RequestMapping(value = "/sendTransactionMsg", method = RequestMethod.GET)
public String sendTransactionMsg() {
SendResult sendResult = null;
try {
//構造消息
Message msg = new Message(producerTopic,// topic
"TagA",// tag
"OrderID001",// key
("Hello jyqlove333").getBytes());// body
//發送事務消息,LocalTransactionExecute的executeLocalTransactionBranch方法中執行本地邏輯
sendResult = transactionProducer.sendMessageInTransaction(msg, (Message msg1,Object arg) -> {
int value = 1;
//TODO 執行本地事務,改變value的值
//===================================================
System.out.println("執行本地事務。。。完成");
if(arg instanceof Integer){
value = (Integer)arg;
}
//===================================================
if (value == 0) {
throw new RuntimeException("Could not find db");
} else if ((value % 5) == 0) {
return LocalTransactionState.ROLLBACK_MESSAGE;
} else if ((value % 4) == 0) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}, 4);
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
}
return sendResult.toString();
}
}
消費消息測試類consumerDemo
/**
*
* @author jiangjb
*
*/
@Component
public class consumerDemo {
@EventListener(condition = "#event.topic=='TopicTest1' && #event.tag=='TagA'")
public void rocketmqMsgListen(RocketmqEvent event) {
DefaultMQPushConsumer consumer = event.getConsumer();
try {
System.out.println("com.bqjr.consumerDemo監聽到一個消息達到:" + event.getMsg("gbk"));
//TODO 進行業務處理
} catch (Exception e) {
if(event.getMessageExt().getReconsumeTimes()<=3){//重複消費3次
try {
consumer.sendMessageBack(event.getMessageExt(), 2);
} catch (Exception e1) {
//TODO 消息消費失敗,進行日誌記錄
}
} else {
//TODO 消息消費失敗,進行日誌記錄
}
}
}
}
來,測試一把
在瀏覽器中訪問:http://10.89.0.144:12306/sendMsg,控制檯輸出如下:再測試一下消費者,在RocketMQ控制檯(RocketMQ控制檯的介紹放到下一篇吧)發送一條消息
補充說明:
本來想自定義一個叫RocketmqListener的註解來實現消息的監聽的,花了大量時間去閱讀和研究了spring關於EventListener註解和JmsListener註解的實現,發現目前我並不能很好的理解和掌控其設計思路,想以瓢畫葫最終也沒能實現,迫於五一節來臨,只能使用EventListener註解代替,不過發現其實也不錯。
同時,也希望各位猿友能給出指導性意見和建議:如何實現RocketmqListener註解以及是否有意義?