RocketMQ最佳實踐(三)開發spring-boot-starter-rocketmq實現與spring boot項目的整合

不要以爲這只是spring boot與RocketMQ的簡單整合,本篇文章還爲各位看官呈現以下知識點的最佳實踐:

  • 自定義一個spring boot 的starter
  • 使用spring的事件傳播機制實現bean與bean之間基於事件驅動的通信
  • 自定義註解、組合註解

先來撩點故事背景^_^

最近在使用spring boot/spring cloud搭建做微服務架構,發現spring boot官方提供的starter中居然沒有集成RocketMQ驚訝word天,頓時激發我的創作基情啊有木有大笑

上面這張截圖來自spring boot官方文檔,爲啥官方提供了JMS、AMQP和Kafka卻偏偏少了RocketMQ呢,我認爲是因爲目前RocketMQ在國外並不普及,而且才捐獻給apache不久,需要一段時間,那麼如此看來,寫一個spring-boot-starter-rocketmq還是比較有意義的。

but,本人水平畢竟有限,寫的東西自然沒法和spring相比,這個版本的starter參考了JMS的starter來封裝,雖然不夠盡善盡美,但還是極具實用價值的微笑

編寫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

pom.xml中加入上面的starter的依賴
        <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註解以及是否有意義?

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