面試官:小夥子,你給我簡單說一下RocketMQ 整合 Spring Boot吧

前程有光發表於2020-12-12

前言

在使用SpringBoot的starter整合包時,要特別注意版本。因為SpringBoot整合RocketMQ的starter依賴是由Spring社群提供的,目前正在快速迭代的過程當中,不同版本之間的差距非常大,甚至基礎的底層物件都會經常有改動。例如如果使用rocketmq-spring-boot-starter:2.0.4版本開發的程式碼,升級到目前最新的rocketmq-spring-boot-starter:2.1.1後,基本就用不了了

應用結構

TestController: 測試入口, 有基本訊息測試和事務訊息測試
TopicListener: 是監聽"topic"這個主題的普通訊息監聽器
TopicTransactionListener: 是監聽"topic"這個主題的事務訊息監聽器, 和TopicTransactionRocketMQTemplate繫結(一一對應關係)
Customer: 是測試訊息體的一個entity物件
TopicTransactionRocketMQTemplate: 是擴充套件自RocketMQTemplate的另一個RocketMQTemplate, 專門用來處理某一個業務流程, 和TopicTransactionListener繫結(一一對應關係)

pom.xml

org.apache.rocketmq:rocketmq-spring-boot-starter:2.1.1, 引用的springboot版本是2.0.5.RELEASE

<?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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.mrathena.middle.ware</groupId>
    <artifactId>rocket.mq.springboot</artifactId>
    <version>1.0.0</version>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.4.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.1.1</version>
            <!-- 遮蔽舊版本的springboot, 引用的springboot版本是2.0.5.RELEASE -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-webmvc</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-aop</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-context</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-messaging</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.fasterxml.jackson.core</groupId>
                    <artifactId>jackson-databind</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-messaging</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

application.yml

server:
  servlet:
    context-path:
  port: 80
rocketmq:
  name-server: 116.62.162.48:9876
  producer:
    group: producer

Customer

package com.mrathena.rocket.mq.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
	private String username;
	private String nickname;
}

生產者 TestController

package com.mrathena.rocket.mq.controller;

import com.mrathena.rocket.mq.configuration.TopicTransactionRocketMQTemplate;
import com.mrathena.rocket.mq.entity.Customer;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.core.MessagePostProcessor;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping("test")
public class TestController {

	private static final String TOPIC = "topic";

	@Autowired
	private RocketMQTemplate rocketMQTemplate;
	@Autowired
	private TopicTransactionRocketMQTemplate topicTransactionRocketMQTemplate;

	@GetMapping("base")
	public Object base() {
		// destination: topic/topic:tag, topic或者是topic拼接tag的整合體
		// payload: 荷載即訊息體
		// message: org.springframework.messaging.Message, 是Spring自己封裝的類, 和RocketMQ的Message不是一個類, 裡面沒有tags/keys等內容
		rocketMQTemplate.send(TOPIC, MessageBuilder.withPayload("你好").setHeader("你是誰", "你猜").build());
		// tags null
		rocketMQTemplate.convertAndSend(TOPIC, "tag null");
		// tags empty, 證明 tag 要麼有值要麼null, 不存在 empty 的 tag
		rocketMQTemplate.convertAndSend(TOPIC + ":", "tag empty ?");
		// 只有 tag 沒有 key
		rocketMQTemplate.convertAndSend(TOPIC + ":a", "tag a");
		rocketMQTemplate.convertAndSend(TOPIC + ":b", "tag b");
		// 有 property, 即 RocketMQ 基礎 API 裡面, Message(String topic, String tags, String keys, byte[] body) 裡面的 key
		// rocketmq-spring-boot-starter 把 userProperty 和其他的一些屬性都糅合在 headers 裡面可, 具體可以參考 org.apache.rocketmq.spring.support.RocketMQUtil.addUserProperties
		// 獲取某個自定義的屬性的時候, 直接 headers.get("自定義屬性key") 就可以了
		Map<String, Object> properties = new HashMap<>();
		properties.put("property", 1);
		properties.put("another-property", "你好");
		rocketMQTemplate.convertAndSend(TOPIC, "property 1", properties);
		rocketMQTemplate.convertAndSend(TOPIC + ":a", "tag a property 1", properties);
		rocketMQTemplate.convertAndSend(TOPIC + ":b", "tag b property 1", properties);
		properties.put("property", 5);
		rocketMQTemplate.convertAndSend(TOPIC, "property 5", properties);
		rocketMQTemplate.convertAndSend(TOPIC + ":a", "tag a property 5", properties);
		rocketMQTemplate.convertAndSend(TOPIC + ":c", "tag c property 5", properties);

		// 訊息後置處理器, 可以在傳送前對訊息體和headers再做一波操作
		rocketMQTemplate.convertAndSend(TOPIC, "訊息後置處理器", new MessagePostProcessor() {
			/**
			 * org.springframework.messaging.Message
			 */
			@Override
			public Message<?> postProcessMessage(Message<?> message) {
				Object payload = message.getPayload();
				MessageHeaders messageHeaders = message.getHeaders();
				return message;
			}
		});

		// convertAndSend 底層其實也是 syncSend
		// syncSend
		log.info("{}", rocketMQTemplate.syncSend(TOPIC, "sync send"));
		// asyncSend
		rocketMQTemplate.asyncSend(TOPIC, "async send", new SendCallback() {
			@Override
			public void onSuccess(SendResult sendResult) {
				log.info("onSuccess");
			}

			@Override
			public void onException(Throwable e) {
				log.info("onException");
			}
		});
		// sendOneWay
		rocketMQTemplate.sendOneWay(TOPIC, "send one way");

		// 這個我還是不太清楚是幹嘛的? 跑的時候會報錯!!!
//		Object receive = rocketMQTemplate.sendAndReceive(TOPIC, "你好", String.class);
//		log.info("{}", receive);

		return "success";
	}

	@GetMapping("transaction")
	public Object transaction() {
		Message<Customer> message = MessageBuilder.withPayload(new Customer("mrathena", "你是誰")).build();
		// 這裡使用的是通過 @ExtRocketMQTemplateConfiguration(group = "anotherProducer") 擴充套件出來的另一個 RocketMQTemplate
		log.info("{}", topicTransactionRocketMQTemplate.sendMessageInTransaction(TOPIC, message, null));
		log.info("{}", topicTransactionRocketMQTemplate.sendMessageInTransaction(TOPIC + ":tag-a", message, null));
		return "success";
	}

}

配置 TopicTransactionRocketMQTemplate

package com.mrathena.rocket.mq.configuration;

import org.apache.rocketmq.spring.annotation.ExtRocketMQTemplateConfiguration;
import org.apache.rocketmq.spring.core.RocketMQTemplate;

/**
 * 一個事務流程和一個RocketMQTemplate需要一一對應
 * 可以通過 @ExtRocketMQTemplateConfiguration(注意該註解有@Component註解) 來擴充套件多個 RocketMQTemplate
 * 注意: 不同事務流程的RocketMQTemplate的producerGroup不能相同
 * 因為MQBroker會反向呼叫同一個producerGroup下的某個checkLocalTransactionState方法, 不同流程使用相同的producerGroup的話, 方法可能會呼叫錯
 */
@ExtRocketMQTemplateConfiguration(group = "anotherProducer")
public class TopicTransactionRocketMQTemplate extends RocketMQTemplate {}

消費者 TopicListener

package com.mrathena.rocket.mq.listener;

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

/**
 * 最簡單的消費者例子
 * topic: 主題
 * consumerGroup: 消費者組
 * selectorType: 過濾方式, TAG:標籤過濾,僅支援標籤, SQL92:SQL過濾,支援標籤和屬性
 * selectorExpression: 過濾表示式, 根據selectorType定, TAG時, 寫標籤如 "a || b", SQL92時, 寫SQL表示式
 * consumeMode: CONCURRENTLY:併發消費, ORDERLY:順序消費
 * messageModel: CLUSTERING:叢集競爭消費, BROADCASTING:廣播消費
 */
@Slf4j
@Component
@RocketMQMessageListener(topic = "topic",
		// 只過濾tag, 不管headers中的key和value
//		selectorType = SelectorType.TAG,
		// 必須指定selectorExpression, 可以過濾tag和headers中的key和value
//		selectorType = SelectorType.SQL92,
		// 不限tag
//		selectorExpression = "*",
		// 不限tag, 和 * 一致
//		selectorExpression = "",
		// 只要tag為a的訊息
//		selectorExpression = "a",
		// 要tag為a或b的訊息
//		selectorExpression = "a || b",
		// SelectorType.SQL92時, 可以跳過tag, 直接用headers裡面的key和value來判斷
//		selectorExpression = "property = 1",
		// tag不為null
//		selectorExpression = "TAGS is not null",
		// tag為empty, 證明tag不會是empty, 要麼有值要麼null
//		selectorExpression = "TAGS = ''",
		// SelectorType.SQL92時, 即過濾tag, 又過濾headers裡面的key和value
//		selectorExpression = "(TAGS is not null and TAGS = 'a') and (property is not null and property between 4 and 6)",
		// 併發消費
		consumeMode = ConsumeMode.CONCURRENTLY,
		// 順序消費
//		consumeMode = ConsumeMode.ORDERLY,
		// 叢集消費
		messageModel = MessageModel.CLUSTERING,
		// 廣播消費
//		messageModel = MessageModel.BROADCASTING,
		consumerGroup = "consumer"
)
public class TopicListener implements RocketMQListener<String> {
	public void onMessage(String s) {
		log.info("{}", s);
	}
}

消費者 TopicTransactionListener

package com.mrathena.rocket.mq.listener;

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RocketMQTransactionListener(rocketMQTemplateBeanName = "topicTransactionRocketMQTemplate")
public class TopicTransactionListener implements RocketMQLocalTransactionListener {

	@Override
	public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
		// message: org.springframework.messaging.Message, 是Spring自己封裝的類, 和RocketMQ的Message不是一個類, 裡面沒有tags/keys等內容
		// 一般來說, 並不會在這裡處理tags/keys等內容, 而是根據訊息體中的某些欄位做不同的操作, 第二個引數也可以用來傳遞一些資料到這裡
		log.info("executeLocalTransaction message:{}, object:{}", message, o);
		log.info("payload: {}", new String((byte[]) message.getPayload()));
		MessageHeaders headers = message.getHeaders();
		log.info("tags: {}", headers.get(RocketMQHeaders.PREFIX + RocketMQHeaders.TAGS));
		log.info("rocketmq_TOPIC: {}", headers.get("rocketmq_TOPIC"));
		log.info("rocketmq_QUEUE_ID: {}", headers.get("rocketmq_QUEUE_ID"));
		log.info("rocketmq_MESSAGE_ID: {}", headers.get("rocketmq_MESSAGE_ID"));
		log.info("rocketmq_TRANSACTION_ID: {}", headers.get("rocketmq_TRANSACTION_ID"));
		log.info("TRANSACTION_CHECK_TIMES: {}", headers.get("TRANSACTION_CHECK_TIMES"));
		log.info("id: {}", headers.get("id"));
		return RocketMQLocalTransactionState.UNKNOWN;
	}

	@Override
	public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
		log.info("checkLocalTransaction message:{}", message);
		// 在呼叫了checkLocalTransaction後, 另一個常規訊息監聽器才能收到訊息
		return RocketMQLocalTransactionState.COMMIT;
	}
}

最後

歡迎關注公眾號:前程有光,領取一線大廠Java面試題總結+各知識點學習思維導+一份300頁pdf文件的Java核心知識點總結!

相關文章