第四十三章: 基於SpringBoot & RabbitMQ完成TopicExchange分散式訊息消費

恆宇少年發表於2018-01-04

我們在之前的兩個章節第四十一章: 基於SpringBoot & RabbitMQ完成DirectExchange分散式訊息消費第四十二章: 基於SpringBoot & RabbitMQ完成DirectExchange分散式訊息多消費者消費提高了RabbitMQ訊息佇列的DirectExchange交換型別的訊息消費,我們之前的章節提到了RabbitMQ比較常用的交換型別有三種,我們今天來看看TopicExchange主題交換型別。

本章目標

基於SpringBoot平臺完成RabbitMQTopicExchange訊息型別交換。

SpringBoot 企業級核心技術學習專題

專題 專題名稱 專題描述
001 Spring Boot 核心技術 講解SpringBoot一些企業級層面的核心元件
002 Spring Boot 核心技術章節原始碼 Spring Boot 核心技術簡書每一篇文章碼雲對應原始碼
003 Spring Cloud 核心技術 對Spring Cloud核心技術全面講解
004 Spring Cloud 核心技術章節原始碼 Spring Cloud 核心技術簡書每一篇文章對應原始碼
005 QueryDSL 核心技術 全面講解QueryDSL核心技術以及基於SpringBoot整合SpringDataJPA
006 SpringDataJPA 核心技術 全面講解SpringDataJPA核心技術

解決問題

之前少年也遇到了一個問題,分類了多模組後訊息佇列無法自動建立,說來也好笑,之前沒有時間去看這個問題,今天在編寫本章文章時發現原因竟然是SpringBoot沒有掃描到common模組內的配置類。讓我一陣的頭大~~~,我們在XxxApplication啟動類上新增@ComponentScan(value = "com.hengyu.rabbitmq")就可以自動建立佇列了!!!

構建專案

本章構建專案時同樣採用多模組的方式進行設計,可以很好的看到訊息處理的效果,因為是多模組專案,我們先來建立一個SpringBoot專案,pom.xml配置檔案依賴配置如下所示:

<dependencies>
		<!--rabbbitMQ相關依賴-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
		<!--web相關依賴-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!--lombok依賴-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<!--spring boot tester-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!--fast json依賴-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.40</version>
		</dependency>
	</dependencies>
複製程式碼

下面我們先來構建公共RabbitMQ模組,因為我們的消費者以及生產者都是需要RabbitMQ相關的配置資訊,這裡我們可以提取出來,使用時進行模組之間的引用。

rabbitmq-topic-common

建立子模組rabbitmq-topic-common,在resources下新增application.yml配置檔案並配置RabbitMQ相關的依賴配置,如下所示:

spring:
  #rabbitmq訊息佇列配置資訊
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    publisher-confirms: true
複製程式碼
定義交換配置資訊

我們跟之前的章節一張,獨立編寫一個列舉型別來配置訊息佇列的交換資訊,如下所示:

/**
 * rabbitmq交換配置列舉
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/26
 * Time:13:56
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Getter
public enum ExchangeEnum
{
    /**
     * 使用者註冊交換配置列舉
     */
    USER_REGISTER_TOPIC_EXCHANGE("register.topic.exchange")
    ;
    private String name;

    ExchangeEnum(String name) {
        this.name = name;
    }
}
複製程式碼
定義佇列配置資訊

同樣訊息佇列的基本資訊配置也同樣採用列舉的形式配置,如下所示:

/**
 * 佇列配置列舉
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/26
 * Time:14:05
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Getter
public enum QueueEnum
{
    /**
     * 使用者註冊
     * 建立賬戶訊息佇列
     */
    USER_REGISTER_CREATE_ACCOUNT("register.account","register.#"),
    /**
     * 使用者註冊
     * 傳送註冊成功郵件訊息佇列
     */
    USER_REGISTER_SEND_MAIL("register.mail","register.#")
    ;
    /**
     * 佇列名稱
     */
    private String name;
    /**
     * 佇列路由鍵
     */
    private String routingKey;

    QueueEnum(String name, String routingKey) {
        this.name = name;
        this.routingKey = routingKey;
    }
}
複製程式碼

訊息佇列列舉內新增了兩個屬性,分別對應了佇列名稱佇列路由,我們本章所講解的TopicExchange型別訊息佇列可以根據路徑資訊配置多個訊息消費者,而轉發的匹配規則資訊則是我們定義的佇列的路由資訊。

定義傳送訊息路由資訊

我們在傳送訊息到佇列時,需要我們傳遞一個路由相關的配置資訊,RabbitMQ會根據傳送時的訊息路由規則資訊與定義訊息佇列時的路由資訊進行匹配,如果可以匹配則呼叫該佇列的消費者完成訊息的消費,傳送訊息路由資訊配置如下所示:

/**
 * 訊息佇列topic交換路由key配置列舉
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/12/11
 * Time:21:58
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Getter
public enum TopicEnum {
    /**
     * 使用者註冊topic路由key配置
     */
    USER_REGISTER("register.user")
    ;

    private String topicRouteKey;

    TopicEnum(String topicRouteKey) {
        this.topicRouteKey = topicRouteKey;
    }
}
複製程式碼
路由特殊字元 #

我們在QueueEnum內配置的路由鍵時有個特殊的符號:#,在RabbitMQ訊息佇列內路由配置#時表示可以匹配零個或多個字元,我們TopicEnum列舉內定義的register.user,則是可以匹配QueueEnum列舉定義register.#佇列的路由規則。 當然傳送訊息時如果路由傳遞:register.user.account也是可以同樣匹配register.#的路由規則。

路由特殊字元 *

除此之外比較常用到的特殊字元還有一個*,在RabbitMQ訊息佇列內路由配置*時表示可以匹配一個字元,我們QueueEnum定義路由鍵如果修改成register.*時,傳送訊息時路由為register.user則是可以接受到訊息的。但如果傳送時的路由為register.user.account時,則是無法匹配該訊息。

訊息佇列配置

配置準備工作已經做好,接下來我們開始配置佇列相關的內容,跟之前一樣我們需要配置QueueExchangeBinding將訊息佇列與交換繫結。下面我們來看看配置跟之前的章節有什麼差異的地方,程式碼如下所示:

/**
 * 使用者註冊訊息佇列配置
 * ========================
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/26
 * Time:16:58
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Configuration
public class UserRegisterQueueConfiguration {

    private Logger logger = LoggerFactory.getLogger(UserRegisterQueueConfiguration.class);
    /**
     * 配置使用者註冊主題交換
     * @return
     */
    @Bean
    public TopicExchange userTopicExchange()
    {
        TopicExchange topicExchange = new TopicExchange(ExchangeEnum.USER_REGISTER_TOPIC_EXCHANGE.getName());
        logger.info("使用者註冊交換例項化成功。");
        return topicExchange;
    }

    /**
     * 配置使用者註冊
     * 傳送啟用郵件訊息佇列
     * 並設定持久化佇列
     * @return
     */
    @Bean
    public Queue sendRegisterMailQueue()
    {
        Queue queue = new Queue(QueueEnum.USER_REGISTER_SEND_MAIL.getName());
        logger.info("建立使用者註冊訊息佇列成功");
        return queue;
    }

    /**
     * 配置使用者註冊
     * 建立賬戶訊息佇列
     * 並設定持久化佇列
     * @return
     */
    @Bean
    public Queue createAccountQueue()
    {
        Queue queue = new Queue(QueueEnum.USER_REGISTER_CREATE_ACCOUNT.getName());
        logger.info("建立使用者註冊賬號佇列成功.");
        return queue;
    }

    /**
     * 繫結使用者傳送註冊啟用郵件佇列到使用者註冊主題交換配置
     * @return
     */
    @Bean
    public Binding sendMailBinding(TopicExchange userTopicExchange,Queue sendRegisterMailQueue)
    {
        Binding binding = BindingBuilder.bind(sendRegisterMailQueue).to(userTopicExchange).with(QueueEnum.USER_REGISTER_SEND_MAIL.getRoutingKey());
        logger.info("繫結傳送郵件到註冊交換成功");
        return binding;
    }

    /**
     * 繫結使用者建立賬戶到使用者註冊主題交換配置
     * @return
     */
    @Bean
    public Binding createAccountBinding(TopicExchange userTopicExchange,Queue createAccountQueue)
    {
        Binding binding = BindingBuilder.bind(createAccountQueue).to(userTopicExchange).with(QueueEnum.USER_REGISTER_CREATE_ACCOUNT.getRoutingKey());
        logger.info("繫結建立賬號到註冊交換成功。");
        return binding;
    }
}
複製程式碼

我們從上面開始分析。 第一步: 首先我們建立了TopicExchange訊息佇列物件,使用ExchangeEnum列舉內的USER_REGISTER_TOPIC_EXCHANGE型別作為交換名稱。

第二步:我們建立了傳送註冊郵件的佇列sendRegisterMailQueue,使用QueueEnum列舉內的型別USER_REGISTER_SEND_MAIL作為佇列名稱。

第三步:與傳送郵件佇列一致,使用者建立完成後需要初始化賬戶資訊,而createAccountQueue訊息佇列後續邏輯就是來完成該工作,使用QueueEnum列舉內的USER_REGISTER_CREATE_ACCOUNT列舉作為建立賬戶佇列名稱。

第四步:在上面步驟中已經將交換、佇列建立完成,下面就開始將佇列繫結到使用者註冊交換,從而實現註冊使用者訊息佇列訊息消費,sendMailBinding繫結了QueueEnum.USER_REGISTER_SEND_MAILRoutingKey配置資訊。

createAccountBinding繫結了QueueEnum.USER_REGISTER_CREATE_ACCOUNTRoutingKey配置資訊。

到目前為止我們完成了rabbitmq-topic-common模組的所有配置資訊,下面我們開始編寫使用者註冊訊息消費者模組。

rabbitmq-topic-consumer

我們首先來建立一個子模組命名為rabbitmq-topic-consumer,在pom.xml配置檔案內新增rabbitmq-topic-common模組的引用,如下所示:

....//
<dependencies>
        <!--公共模組依賴-->
        <dependency>
            <groupId>com.hengyu</groupId>
            <artifactId>rabbitmq-topic-common</artifactId>
            <version>${parent.version}</version>
        </dependency>
    </dependencies>
....//
複製程式碼
消費者程式入口

下面我們來建立程式啟動類RabbitMqTopicConsumerApplication,在這裡需要注意,手動配置下掃描路徑@ComponentScan,啟動類程式碼如下所示:

/**
 * 訊息消費者程式啟動入口
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/12/11
 * Time:21:48
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@SpringBootApplication
@ComponentScan(value = "com.hengyu.rabbitmq")
public class RabbitMqTopicConsumerApplication {

    /**
     * logback
     */
    private static Logger logger = LoggerFactory.getLogger(RabbitMqTopicConsumerApplication.class);

    /**
     * 程式入口
     * @param args
     */
    public static void main(String[] args)
    {
        SpringApplication.run(RabbitMqTopicConsumerApplication.class,args);

        logger.info("【【【【【Topic佇列訊息Consumer啟動成功】】】】】");
    }
}
複製程式碼

手動配置掃描路徑在文章的開始解釋過了,主要目的是為了掃描到RabbitMQConfiguration配置類內的資訊,讓RabbitAdmin自動建立配置資訊到server端。

傳送郵件消費者

傳送郵件訊息費監聽register.mail訊息佇列資訊,如下所示:

/**
 * 傳送使用者註冊成功郵件消費者
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/12/11
 * Time:22:07
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
@RabbitListener(queues = "register.mail")
public class SendMailConsumer
{

    /**
     * logback
     */
    Logger logger = LoggerFactory.getLogger(SendMailConsumer.class);

    /**
     * 處理訊息
     * 傳送使用者註冊成功郵件
     * @param userId 使用者編號
     */
    @RabbitHandler
    public void handler(String userId)
    {

        logger.info("使用者:{},註冊成功,自動傳送註冊成功郵件.",userId);

        //... 傳送註冊成功郵件邏輯
    }
}
複製程式碼

在這裡我只是完成了訊息的監聽,具體的業務邏輯可以根據需求進行處理。

建立賬戶消費者

建立使用者賬戶資訊消費者監聽佇列register.account,程式碼如下所示:

/**
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/12/11
 * Time:22:04
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
@RabbitListener(queues = "register.account")
public class CreateAccountConsumer {

    /**
     * logback
     */
    Logger logger = LoggerFactory.getLogger(CreateAccountConsumer.class);

    /**
     * 處理訊息
     * 建立使用者賬戶
     * @param userId 使用者編號
     */
    @RabbitHandler
    public void handler(String userId)
    {
        logger.info("使用者:{},註冊成功,自動建立賬戶資訊.",userId);

        //... 建立賬戶邏輯
    }
}
複製程式碼

建立賬戶,賬戶初始化邏輯都可以在handler方法進行處理,本章沒有做資料庫複雜的處理,所以沒有過多的邏輯處理在消費者業務內。

rabbitmq-topic-provider

接下來是我們的訊息提供者的模組編寫,我們依然先來建立程式入口類,並新增掃描配置@ComponentScan路徑,程式碼如下所示:

/**
 * 訊息生產者程式啟動入口
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/12/11
 * Time:21:48
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@SpringBootApplication
@ComponentScan(value = "com.hengyu.rabbitmq")
public class RabbitMqTopicProviderApplication {

    /**
     * logback
     */
    private static Logger logger = LoggerFactory.getLogger(RabbitMqTopicProviderApplication.class);

    /**
     * 程式入口
     * @param args
     */
    public static void main(String[] args)
    {
        SpringApplication.run(RabbitMqTopicProviderApplication.class,args);

        logger.info("【【【【【Topic佇列訊息Provider啟動成功】】】】】");
    }
}
複製程式碼
定義訊息傳送介面

建立QueueMessageService佇列訊息傳送介面並新增send方法,如下所示:

/**
 * 訊息佇列業務
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/26
 * Time:14:50
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
public interface QueueMessageService
{
    /**
     * 傳送訊息到rabbitmq訊息佇列
     * @param message 訊息內容
     * @param exchangeEnum 交換配置列舉
     * @param routingKey 路由key
     * @throws Exception
     */
    public void send(Object message, ExchangeEnum exchangeEnum, String routingKey) throws Exception;
}
複製程式碼

send方法內有三個引數,解析如下:

  • message:傳送訊息內容,可以為任意型別,當然本章內僅僅是java.lang.String。
  • exchangeEnum:我們自定義的交換列舉型別,方便傳送訊息到指定交換。
  • routingKey:傳送訊息時的路由鍵內容,該值採用TopicEnum列舉內的topicRouteKey作為引數值。

下面我們來看看該介面的實現類QueueMessageServiceSupportsend方法實現,如下所示:

/**
 * 訊息佇列業務邏輯實現
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/11/26
 * Time:14:52
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Component
public class QueueMessageServiceSupport
    implements QueueMessageService
{
    /**
     * 訊息佇列模板
     */
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Override
    public void send(Object message, ExchangeEnum exchangeEnum, String routingKey) throws Exception {
        //傳送訊息到訊息佇列
        rabbitTemplate.convertAndSend(exchangeEnum.getName(),routingKey,message);
    }
}
複製程式碼

我們通過RabbitTemplate例項的convertAndSend方法將物件型別轉換成JSON字串後傳送到訊息佇列服務端,RabbitMQ接受到訊息後根據註冊的消費者並且路由規則篩選後進行訊息轉發,並實現訊息的消費。

執行測試

為了方便測試我們建立一個名為UserService的實現類,如下所示:

/**
 * 使用者業務邏輯
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/12/11
 * Time:22:10
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@Service
public class UserService
{
    /**
     * 訊息佇列傳送業務邏輯
     */
    @Autowired
    private QueueMessageService queueMessageService;

    /**
     * 隨機建立使用者
     * 隨機生成使用者uuid編號,傳送到訊息佇列服務端
     * @return
     * @throws Exception
     */
    public String randomCreateUser() throws Exception
    {
        //使用者編號
        String userId = UUID.randomUUID().toString();
        //傳送訊息到rabbitmq服務端
        queueMessageService.send(userId, ExchangeEnum.USER_REGISTER_TOPIC_EXCHANGE, TopicEnum.USER_REGISTER.getTopicRouteKey());
        return userId;
    }
}
複製程式碼

該類內新增了一個名為randomCreateUser隨機建立使用者的方法,通過UUID隨機生成字串作為使用者的編號進行傳遞給使用者註冊訊息佇列,完成使用者的模擬建立。

編寫測試用例

接下來我們建立RabbitMqTester測試類來完成隨機使用者建立訊息傳送,測試用例完成簡單的UserService注入,並呼叫randomCreateUser方法,如下所示:

/**
 * ========================
 *
 * @author 恆宇少年
 * Created with IntelliJ IDEA.
 * Date:2017/12/11
 * Time:22:10
 * 碼雲:http://git.oschina.net/jnyqy
 * ========================
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitMqTopicProviderApplication.class)
public class RabbitMqTester
{
    /**
     * 使用者業務邏輯
     */
    @Autowired
   private UserService userService;

    /**
     * 模擬隨機建立使用者 & 傳送訊息到註冊使用者訊息佇列
     * @throws Exception
     */
    @Test
    public void testTopicMessage() throws Exception
    {
        userService.randomCreateUser();
    }
}
複製程式碼

到目前為止,我們的編碼已經完成,下面我們按照下面的步驟啟動測試:

  1. 啟動rabbitmq-topic-consumer訊息消費者模組,並檢視控制檯輸出內容是否正常
  2. 執行rabbitmq-topic-provider模組測試用例方法testTopicMessage
  3. 檢視rabbitmq-topic-consumer控制檯輸出內容

最終效果:


2017-12-30 18:39:16.819  INFO 2781 --- [           main] c.h.r.c.RabbitMqTopicConsumerApplication : 【【【【【Topic佇列訊息Consumer啟動成功】】】】】
2017-12-30 18:39:29.376  INFO 2781 --- [cTaskExecutor-1] c.h.r.consumer.CreateAccountConsumer     : 使用者:c6ef682d-da2e-4cac-a004-c244ff4c4503,註冊成功,自動建立賬戶資訊.
2017-12-30 18:39:29.376  INFO 2781 --- [cTaskExecutor-1] c.h.rabbitmq.consumer.SendMailConsumer   : 使用者:c6ef682d-da2e-4cac-a004-c244ff4c4503,註冊成功,自動傳送註冊成功郵件.
複製程式碼

總結

本章主要講解了TopicExchange交換型別如何消費佇列訊息,講解了常用到了的特殊字元#*如何匹配,解決了多模組下的佇列配置資訊無法自動建立問題。還有一點需要注意TopicExchange交換型別在訊息消費時不存在固定的先後順序!!!

本章原始碼已經上傳到碼雲: SpringBoot配套原始碼地址:gitee.com/hengboy/spr… SpringCloud配套原始碼地址:gitee.com/hengboy/spr… SpringBoot相關係列文章請訪問:目錄:SpringBoot學習目錄 QueryDSL相關係列文章請訪問:QueryDSL通用查詢框架學習目錄 SpringDataJPA相關係列文章請訪問:目錄:SpringDataJPA學習目錄,感謝閱讀! 歡迎加入QQ技術交流群,共同進步。

QQ技術交流群

相關文章