当前位置:   article > 正文

SpringBoot整合RabbitMq实战(一)_exchangetypes源码

exchangetypes源码
1 Spring AMQP 简介

Spring AMQP项目是一个引入Spring核心概念用于基于高级消息队列AMQP)的解决方案的开发,它提供了一个模板用于发送和接受消息的高级抽象。它对基于消息驱动并带有一个监听容器的pojo对象提供支持,这个库促进AMQP资源的管理,同时也促进Spring AMQP的依赖注入和声明式配置。在所有的案例中,你可以看到类似于JMS对Spring框架的支持。

整个Spring AMQP项目包含两部分,即spring-amqpspring-rabbit,前者是RabbitMq的基础抽象,后者是RabbitMq的实现。

目前Spring官网发布的最新稳定版本Spring AMQP是2.2.9版本,它具有以下新特性:

  • 支持异步处理入站消息的监听器容器;
  • RabbitTemplate模板类用于发送和接收消息;
  • RabbitAdmin类用于自动声明队列、交换机和绑定
2 引入依赖和声明配置
2.1 引入依赖

在maven构建的spring项目中可以在pom.xml文件中通过下面这种引入spring-rabbitmq的依赖

<dependency>
  <groupId>org.springframework.amqp</groupId>
  <artifactId>spring-rabbit</artifactId>
  <version>2.2.9.RELEASE</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5

而在spring-boot项目中则通过springboot对应的rabbitmq起步依赖项引入

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <version>2.2.9.RELEASE</version>
 </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
2.2 声明配置
  1. 通过xml的方式配置

applicationContext.xml

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/rabbit
           https://www.springframework.org/schema/rabbit/spring-rabbit.xsd
           http://www.springframework.org/schema/beans
           https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--rabbit连接工厂-->
    <rabbit:connection-factory id="connectionFactory"/>
    <!--RabbitTemplate-->
    <rabbit:template id="amqpTemplate" connection-factory="connectionFactory"/>
    <!--RabbitAdmnin-->
    <rabbit:admin connection-factory="connectionFactory"/>
    <!--Queue-->
    <rabbit:queue name="myqueue"/>

</beans>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 通过java config方式配置
@Configuration
public class RabbitConfiguration {

    @Bean
    public ConnectionFactory connectionFactory() {
        return new CachingConnectionFactory("localhost");
    }

    @Bean
    public AmqpAdmin amqpAdmin() {
        return new RabbitAdmin(connectionFactory());
    }

    @Bean
    public RabbitTemplate rabbitTemplate() {
        return new RabbitTemplate(connectionFactory());
    }

    @Bean
    public Queue myQueue() {
       return new Queue("myqueue");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
3AMQP抽象概念

AMQP抽象概念是使用Spring-AMQP模块必须要掌握的重要概念,开发过程中也经常要使用到这些接口和类,主要包括MessageExchangeQueueBinding,它们都是org.springframework.amqp.core包下的接口或类

3.1 Message

Spring AMQPMessage类定义为更通用的AMQP域模型表示的一部分,Message类的目的是将主体和属性封装在单个实例中,从而使API更简单。Message类的定义如下,发送消息的时候可直接将消息封装成Message类

public class Message implements Serializable{
    //消息属性类,具体可查看MessageProperties类源码
    private final MessageProperties messageProperties;
    //消息的主题部分,类型为byte数组
    private final byte[] body;

    public Message(byte[] body, MessageProperties messageProperties) {
        this.body = body;
        this.messageProperties = messageProperties;
    }

    public byte[] getBody() {
        return this.body;
    }

    public MessageProperties getMessageProperties() {
        return this.messageProperties;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
3.2 Exchange

Exchange接口代表一个AMQP交换机,它是消息生产者投送到的地方,代理的虚拟主机中的每个交换器都有一个惟一的名称和一些其他属性。Exchange接口的源码如下:

public interface Exchange extends Declarable {
    String getName();

    String getType();

    boolean isDurable();

    boolean isAutoDelete();

    Map<String, Object> getArguments();

    boolean isDelayed();

    boolean isInternal();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

从上面的源码可以看出Exchange的实现类中都有一个type属性来决定属于什么类型的交换机,这些类型限制在ExchangeTypes常量中,主要有direct、topic、fanout和headers4种,每种类型的交换机都可以在org.springframework.amqp.core下找到其对应的实现类。

在处理绑定到队列的方式方面,这些交换类型的行为各不相同。

  • direct交换只允许队列被固定的路由键(通常是队列的名称)绑定;
  • topic交换机支持带有路由模式的绑定,这些模式可能分别包含“*”和“#”通配符,用于“确定的一个”和“0或多个”;
  • Fanout exchange发布到绑定到它的所有队列,而不考虑任何路由密钥
3.3 Queue

Queue类表示消息使用者从其中接收消息的组件。与各种Exchange类一样,我们的实现是这个核心AMQP类型的抽象表示。下面的清单显示了Queue类的主体的核心源码:

public class Queue extends AbstractDeclarable {

    private final String name;

    private volatile boolean durable;

    private volatile boolean exclusive;

    private volatile boolean autoDelete;

    private volatile Map<String, Object> arguments;

    /**
     * 默认的消息队列是持久化, 非独立和非自动删除的.
     * @param name 消息队列的命名.
     */
    public Queue(String name) {
        this(name, true, false, false);
    }

    // Getters and Setters omitted for brevity

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

请注意,构造函数接受队列名称。根据实现的不同,管理模板可能提供用于生成唯一命名队列的方法。这样的队列可以用作“回复”地址或其他临时情况。因此,自动生成队列的exclusiveautoDelete属性都将被设置为true

3.4 Binding

消息传递连接生产者和消费者至关重要, 在Spring AMQP中,我们定义了一个Binding类来表示这些连接;

构造Binding实例的2种方式:

1) 通过关键字new构造

Queue directBinding = new Binding(queueName, directExchange, "foo.bar");

Queue topicBinding = new Binding(queueName, topicExchange, "foo.*");

Queue fanoutBinding = new Binding(queueName, fanoutExchange);
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 通过 BindingBuilder类构造
Binding binding = BindingBuilder.bind(someQueue).to(someTopicExchange).with("foo.*");
  • 1
3 实战演练

由于rabbitmq消息队列一般用于系统间异步通信,如电商项目中处理高峰期(双十一抢购)的订单时一般先把订单数据投递到消息队列中,之后再通过异步处理减轻服务器DB和IO压力。

3.1 构建聚合项目

本实战中笔者使用IDEA构建了一个聚合模块spring-boot项目message-practices,该项目包含common、message-producer和message-consumer三个子模块项目,common项目中放一些公共的通用类;message-producer项目模拟发送消息;message-consumer项目用于模拟消费消息

聚合项目的结构如下:

messagepractices
|---common
|---message-consumer
|---message-producer
  • 1
  • 2
  • 3
  • 4

各个项目的 pom.xml中引入依赖和坐标

  1. messagepractices项目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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath />
    </parent>
    <groupId>com.hsf.rabbitmq</groupId>
    <artifactId>message-practices</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>message-producer</module>
        <module>message-consumer</module>
        <module>common</module>
    </modules>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!--阿里fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>
    </dependencies>   
</project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

messagepractices项目`中无配置项和业务逻辑代码

2)common项目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">
    <parent>
        <artifactId>message-practices</artifactId>
        <groupId>com.hsf.rabbitmq</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>common</artifactId>
</project>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

common项目中src/main/java目录下新建模拟订单的ProductOrder实体类

注意:发送消息的实体类和消费消息的实体类必须具有相同的全限定类名,否则消费消息反序列化时会报找不到那个实体类,因此消息的实体类必须是一个公共类

package com.hsf.rabbitmq.common.pojo;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;

public class ProductOrder implements Serializable {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd:HH:mm:ss");
    //订单ID,默认为UUID
    private String orderId;
    //商品ID
    private String productId;
    //商品名称
    private String productName;
    //商品类目ID
    private String categoryId;
    //商品单价
    private Double price = 0.0;
    //商品数量
    private Integer count = 0;
    //下单时间戳,日期字符串格式
    private String timestamp;

    public String getOrderId() {
         if(orderId==null || "".equals(orderId)){
            orderId = UUID.randomUUID().toString();
        }
        return orderId;
    }

    public String getTimestamp() {
        if(timestamp==null || "".equals(timestamp)){
            timestamp = sdf.format(new Date());
        }
        return timestamp;
    }
    //省略其他setter和getter方法
   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

common项目编辑完后需要在common项目的根目录下通过IDEA的Terminal或者git bash或者cmd命令窗口执行

mvn install命令将common项目以jar包的形式上传到本地maven仓库,方便依赖它的message-producermessage-consumer项目引用它,common项目打包成功并上传到本地仓库路后可以看到在本地Maven仓库中

看到其对应的jar包和pom文件,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-33Ix885F-1595749932669)(D:\markdown撰写文档\images\common_jar.png)]

3)message-producer项目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">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>message-producer</artifactId>
    <parent>
        <groupId>com.hsf.rabbitmq</groupId>
        <artifactId>message-practices</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>com.hsf.rabbitmq</groupId>
            <artifactId>common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--引入spring mvc的起步依赖-->
        <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>
            <scope>test</scope>
        </dependency>
        <!--rabbitmq依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <version>2.2.9.RELEASE</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!--打包插件-->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.1.5.RELEASE</version>
            </plugin>
            <!--编译和打包时跳过测试-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  1. message-consumer项目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">
   <parent>
        <groupId>com.hsf.rabbitmq</groupId>
        <artifactId>message-practices</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>message-consumer</artifactId>

    <dependencies>
            <dependency>
                <groupId>com.hsf.rabbitmq</groupId>
                <artifactId>common</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        <!--引入spring mvc的起步依赖-->
        <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>
            <scope>test</scope>
        </dependency>
        <!--rabbitmq依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <version>2.2.9.RELEASE</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.1.5.RELEASE</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

2.1.5版本的spring-boot-maven-plugin打包插件用的阿里云仓库的,因为笔者将项目改为聚合项目后从maven中央仓库一直拉不下来

注: 笔者的maven仓库配置配置了阿里云的maven仓库镜像地址以及本地仓库

IDEA使用的maven对应的conf/setting.xml配置镜像和代理仓库

<mirrors>
    <!-- mirror
     | Specifies a repository mirror site to use instead of a given repository. The repository that
     | this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
     | for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
     | -->
    <mirror>
      <id>aliyunPublic</id>
      <mirrorOf>public</mirrorOf>
      <name>阿里云公共仓库</name>
      <url>https://maven.aliyun.com/repository/public</url>
    </mirror>
    <mirror>
      <id>aliyunCentral</id>
      <mirrorOf>central</mirrorOf>
      <name>阿里云中央仓库</name>
      <url>https://maven.aliyun.com/repository/central</url>
    </mirror>
    <mirror>
      <id>aliyunSprinPlugin</id>
      <mirrorOf>spring-plugin</mirrorOf>
      <name>阿里云spring-plugin仓库</name>
      <url>https://maven.aliyun.com/repository/spring-plugin</url>
    </mirror>
    <mirror>
      <id>central</id>
      <name>Maven Repository Switchboard</name>
      <url>https://repo1.maven.org/maven2/</url>
      <mirrorOf>central</mirrorOf>
    </mirror>
    <mirror>
      <id>repo2</id>
      <mirrorOf>central</mirrorOf>
      <name>Human Readable Name for this Mirror.</name>
      <url>https://repo1.maven.org/maven2/maven/</url>
    </mirror>
  </mirrors>

<repositories>
       <!--本地仓库1-->
        <repository>
          <id>local1</id>
          <url>file:///C:/Users/HP/.m2/repository</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>true</enabled>
          </snapshots>
        </repository>
        <repository>
        <!--本地仓库2-->    
          <id>local2</id>
          <url>file:///D:/mavenRepository/.m2</url>m
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>true</enabled>
          </snapshots>
        </repository>
        <!-阿里云的spring代理仓库-->
        <repository>
          <id>spring</id>
          <url>https://maven.aliyun.com/repository/spring</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>true</enabled>
          </snapshots>
        </repository>
   </repositories>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

配置好后执行拉取命令:

mvn install

  • 1
  • 2
3.2 配置启动类、rabbitmq连接、创建交换机和队列
3.2.1 message-producer项目配置文件与bean
  1. application.yaml配置和项目启动类

application.yaml

为节约时间起见,这里没有配置不同环境下的application.yaml

server:
  port: 8081
  servlet:
    context-path: /messge-producer

  • 1
  • 2
  • 3
  • 4
  • 5

MessageProducerApplication.java

@SpringBootApplication
public class MessageProducerApplication {

    public  static void  main(String[] args){

        SpringApplication.run(MessageProducerApplication.class,args);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2)连接配置与交换机和队列bean实例的配置

package com.hsf.rabbitmq.message.producer.configuration;


import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMqConfig {

    @Bean
    //注意这个ConnectionFactory类是org.springframework.amqp.rabbit包下的类,而不是com.rabbit.client包下的类
   public ConnectionFactory connectionFactory() {
        CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin123");
        connectionFactory.setVirtualHost("/");
       //设置通道缓存最大值
        connectionFactory.setChannelCacheSize(50);
       //设置缓存模式
        connectionFactory.setCacheMode(CachingConnectionFactory.CacheMode.CHANNEL);
        //设置最大连接数
        connectionFactory.setConnectionLimit(50);
        return  connectionFactory;
   }
   //配置RabbitTemplate模板操作bean
   @Bean
   public RabbitTemplate rabbitTemplate(){

        return new RabbitTemplate(connectionFactory());
   }
   //配置消息队列bean
   @Bean
   public Queue myQueue(){

        return new Queue("myQueue");
   }

   @Bean
   public Queue topicQueue(){

        return new Queue("topicQueue");
   }
    
   @Bean
   public Queue testQueue(){

        return new Queue("testQueue");
   }

   @Bean
   public Queue fanoutQueue(){
        return new Queue("fanoutQueue");
   }
   //配置direct类型交换机
   @Bean
   public DirectExchange directExchange(){

        return new DirectExchange("directExchange",true,false);
   }
    //配置topic类型交换机
   @Bean
   public TopicExchange topicExchange(){

        return new TopicExchange("topicExchange",true,false);
    }
    //配置fanout型交换机
    @Bean
    public FanoutExchange fanoutExchange(){

        return new FanoutExchange("fanoutExchange",true,false);
    }
    //配置direct类型交换机的绑定
    @Bean
    public Binding directBinding(){

        return BindingBuilder.bind(myQueue()).to(directExchange()).with("direct.key");
    }
    //配置第二个direct类型交换机的绑定,用于测试不按固定啊绑定路由键发送消息时的场景
    @Bean
    public Binding testBinding(){

        return BindingBuilder.bind(testQueue()).to(directExchange()).with("test.key");
    }
    //配置topic类型交换机的绑定
    @Bean
    public Binding topicBinding(){
       
        return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.*");
    }
    //配置fanout类型交换机的绑定
     @Bean
    public Binding fanoutBinding(){

        return BindingBuilder.bind(fanoutQueue()).to(fanoutExchange());
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
3.2.2 message-consumer项目配置文件与bean

application.yaml配置与启动类:

在消息消费项目中我们无需配置交换机、消息队列,只需要配置rabbitmq的客户端连接即可

application.yaml

server:
  port: 8082
  servlet:
    context-path: /messge-consumer
spring:
  rabbitmq:
    virtual-host: /
    host: localhost
    user: guest
    password: guest
    port: 5672
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

spring-boot-starter-amqp模块中的条件配置类会根据spring.rabbitmq的前缀自动配置连接工厂和RabbitTemplate的bean实例

启动类

MessageConsumerApplication.java

@SpringBootApplication
public class MessageConsumerApplication {

    public static void main(String[] args){

        SpringApplication.run(MessageConsumerApplication.class,args);
    }
    
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
3.3 完成生产消息与消费消息业务代码
3.3.1 完成生产消息逻辑

message-producer项目下完成使用接口投递消息的逻辑

@RestController
@RequestMapping("/rabbitmq")
public class RabbitController {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    //向direct交换机投递消息
    @GetMapping("sendDirect")
    public Map<String,Object> testDirectMessage(@RequestParam("message") String message){
        rabbitTemplate.convertAndSend("directExchange","direct.key",message);
        Map<String,Object> resMap = new HashMap<>();
        resMap.put("status",200);
        resMap.put("message","ok");
        resMap.put("data","hello "+message);
        return resMap;
    }
    //向第二个direct交换机投递消息,投送时路由键不与交换机绑定的路由键一致
    @GetMapping("sendDirect1")
    public Map<String,Object> testSendDirectMessage1(@RequestParam("message") String message){
        //测试投递的路由键不是direct型交换机中绑定的路由场景
        rabbitTemplate.convertAndSend("directExchange","test.queue",message);
        Map<String,Object> resMap = new HashMap<>();
        resMap.put("status",200);
        resMap.put("message","ok");
        resMap.put("data","hello "+message);
        return resMap;
    }
    //向topic型交换机投递消息
    @PostMapping("sendTopic")
    public Map<String,Object> testSendTopicObjectMessage(@RequestBody ProductOrder message){

        rabbitTemplate.convertAndSend("topicExchange","topic.order",message);
        Map<String,Object> resMap = new HashMap<>();
        resMap.put("status",200);
        resMap.put("message","ok");
        resMap.put("data",message);
        return resMap;
    }
    //headers交换机使用的不多,这里就不放测试demo了
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

上面的代码中使用接口的方式模拟生产和投递消息,这里要注意调用RabbitTemplate#convertAndSend方法时最好使用convertAndSend(String exchange, String routingKey, final Object object)方法,第一个参数为要投递的交换机名,第二个参数为路由键,第三个参数为任意序列化对象类型的消息;

如果调用的是convertAndSend(String routingKey, final Object object)方法,很可能会导致消息无法消费,作者亲自踩过坑。

3.3.2 消费消息逻辑

message-consumer项目下完成消费消息的逻辑

  1. 测试消费direct型交换机转发到myQueue消息队列中的消息

DirectConsumer.java

@RabbitListener(queues = {"myQueue"})
@Component
public class DirectConsumer {

    private static Logger logger = LoggerFactory.getLogger(DirectConsumer.class);
    @RabbitHandler
    public void consumeDirectMessage(String message)throws Exception{

        logger.info("myQueue收到消息:"+message);

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  1. 测试消费direct型交换机转发到testQueue消息队列中的消息

TestQueueConsumer.java

@Component
@RabbitListener(queues = {"testQueue"})
public class TestQueueConsumer {

    private static Logger logger = LoggerFactory.getLogger(TestQueueConsumer.class);

    @RabbitHandler
    public void consumeMessage(String message)throws Exception{

        logger.info("testQueue收到消息:"+message);

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  1. 测试消费topic型交换机转发到topicQueue

TopicConsumer.java

@Component
@RabbitListener(queues = {"topicQueue"})
public class TopicConsumer {

    private Logger logger = LoggerFactory.getLogger(TopicConsumer.class);

    @RabbitHandler
    public void consumeTopicMessage(ProductOrder order)throws Exception{
        //在这里可以根据订单的信息进行订单数据持久化以及查库存的逻辑处理
        logger.info("topicQueue接收到消息:order={}", JSON.toJSON(order));

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  1. 测试消费fanout型交换机转发到fanoutQueue

FanoutQueueConsumer.java

@Component
@RabbitListener(queues = {"fanoutQueue"})
public class FanoutQueueConsumer {

    Logger logger = LoggerFactory.getLogger(FanoutQueueConsumer.class);
    @RabbitHandler
    public void consumeMessage(String message)throws Exception{

        logger.info("fanoutQueue收到消息:"+message);

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
4 测试生产消息与消费消息

在IDEA中以debug模式依次运行message-producermessage-consumer两个项目下的启动类中的main`函数

启动两个项目

4.1 测试投递到direct和fanout类型交换机中的String类型信息消费情况

依次在浏览器地址栏中输入

http://localhost:8081/messge-producer/rabbitmq/sendDirect?message=rabbitmq

http://localhost:8081/messge-producer/rabbitmq/sendDirect1?message=rabbitmq

http://localhost:8081/messge-producer/rabbitmq/sendFanout?message=hellow-Fanout

可以看到message-consumer项目的控制台中输出如下信息

INFO 20360 --- [ntContainer#0-1] c.h.r.m.c.rabbitmq.DirectConsumer        : myQueue收到消息:rabbitmq
INFO 24004 --- [ntContainer#1-1] c.h.r.m.c.rabbitmq.FanoutQueueConsumer   : fanoutQueue收到消息:hellow-Fanout
  • 1
  • 2

调用第二个接口生产消息并投递到direct型交换机中的消息因为与绑定的路由键 不一致,没有投递到testQueue消息队列中去,因而没有被它对对于的消费者TestQueueConsumer消费,因而没有输出相应的日志信息

4.2 测试投递到topic类型交换机中的对ProductOrder类型消息消费情况

postman中调用Post类型接口

http://localhost:8081/messge-producer/rabbitmq/sendTopic
//入参请求体,row类型application/json 格式
{
  "productId": "huawei1001",
  "productName": "华为P30手机",
  "categoryId": "hauweiPhone",
  "price": 2950.0,
  "count": 1
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

可看到消息消费端控制台输出如下日志信息:

INFO 24004 --- [ntContainer#3-1] c.h.r.m.consumer.rabbitmq.TopicConsumer  : topicQueue接收到消息:order={"productId":"huawei1001","orderId":"5fb3190d-ee82-4960-8fa4-d751653f3f9d","price":2950.0,"count":1,"categoryId":"hauweiPhone","productName":"华为P30手机","timestamp":"2020-07-26:11:48:33"}
  • 1

再到rabbitmq服务的管理页面查看交换机和消息队列信息

发现自定义的交换机都出现在了exchange管理页面,其中amq.direct、amq.fanout、amq.headers、amq.match、qmq.rabbit.trace、amq.topic为系统默认的交换机,右边的type字段表示交换机的类型,如果启动消息生产者项目后发现交换机中没有配置文件中定义的交换机,则需要在rabbitmq管理页面手动创建,点击下面的Add new exchange按钮输入交换机的名称和类型即可

消息生产者中自定义的消息队列同意出现在了Queues管理页面,同样如果启动消息生产者项目后配置文件中定义的消息队列没有出现在该页面,则需要手动创建,点击下面的Add new Queue按钮输入Name选项值即可完成创建新的消息队列。

5 采坑总结
5.1 客户端不必要配置项在消息消费异常导致的踩坑

在运行本文的测试demo时作者踩了大半天的坑,其中一个坑就当消费者消费消息异常时,消费端控制台不停抛出ListenerExecutionFailedException这个异常,说明程序在不停地消费异常消息。异常信息如下:

org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException: Listener method 'no match' threw exception
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:198) ~[spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
caused by: org.springframework.amqp.AmqpException: No method found for class com.hsf.rabbitmq.message.ProducerOrder
  • 1
  • 2
  • 3

网上查了资料说时下面这个原因:

根据官方文档说明,是 consumer 在消息消费时发生了异常, 默认情况下,该消息会被 reject, 并重新回到队列中, 但如果异常发生在到达用户代码之前的异常,此消息将会一直投递。
  • 1

最让作者头疼的是最开始ProductOrder实体类在消息生产者和消息消费者两个项目中均有定义,造成两个项目中的该实体类全限定类名不一致导致消费消息报错,而后即使把这个投递ProductOrder类型的接口注释,把对于的交换机和队列从rabbitmq管理页面删除还是会继续报这个异常,后来发现是因为在消息消费者项目的配置中加入了下面两行配置,导致消息消费失败的话即使项目重启还是会一直报消费异常,找不到对应的消息消费方法

spring:
  rabbitmq:
    publisher-confirms: true
    publisher-returns: true
  • 1
  • 2
  • 3
  • 4

后面把上面两行配置代码注释,并依次在消息生产者和消息消费者项目的根目录下执行mvn clean install重新打包编译后重启项目后这一问题才算解决

5.2 消息对象实体类全限定名不一致导致不停的消费异常消息的坑

这个坑要是同时在消息生产者和消息消费者中定义了ProducerOrder实体类,造成消费消息反序列化时全限定名与投递过来的消息全限定名不一致导致的,解决的办法是把消息实体类抽出到一个公共的模块中,然后再消息生产者和消费者项目的依赖性中引用公共模块的依赖。

5.4 一个交换机绑定多个消息队列的导致的坑

一个交换机绑定多个消息队列后会造成只有第一个绑定该交换机的消息队列能被投递消息,其他绑定的消息队列都不会投递消息,也是也就造成无法从其他消息队列中消费消息的问题。解决的办法是给每个需要绑定的消息队列配置一个单独唯一的Exchange

5.5 在消费方法中使用Message类和byte[]接受消息产生的坑
@Component
@RabbitListener(queues = {"testQueue"})
public class TestQueueConsumer {

    private static Logger logger = LoggerFactory.getLogger(TestQueueConsumer.class);

    @RabbitHandler
    public void consumeMessage(Message message)throws Exception{

        String messageBody = new String(message.getBody(),"UTF-8");
        logger.info("testQueue收到消息:"+messageBody);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
 @RabbitHandler
    public void consumeMessage(byte[] message)throws Exception{

        String messageBody = new String(message,0,message.length,"UTF-8");
        logger.info("testQueue收到消息:"+messageBody);

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

例如使用上面两种方式中的任何接收消息会导致消息消费端出现下面这种消息消费异常:

org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException: Listener method 'no match' threw exception
	at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:198) ~[spring-rabbit-2.1.5.RELEASE.jar:2.1.5.RELEASE]
Caused by: org.springframework.amqp.AmqpException: No method found for class java.lang.String
  • 1
  • 2
  • 3

解决上面这种异常的办法是将接受消息改为发送消息时对应的类,一般是String类型;同时可能还需要再rabbitmq管理页面同时删掉消费异常对应的ExchangeQueue,然后手动新建与原来相同的ExchangeQueue`,最后再重启项目

上面这些坑一个很重要的原因就是我们才刚入门Spring-AMQP,没有配置死信队列和配置重复消费次数以及异常消费消息时的处理方法,等涩会给你如学习了Spring-AMQP项目之后,我们会发现很多问题自然迎刃而解,而且还能弄懂产生消费异常的具体深层原因,从根本是防止消费消息异常的发生。

点个再看,持续关注作者,后面的文章会发布深入学习SpringBoot整合AMQP和实战Demo的系列文章

6 小结

本文系统了讲解了rabbitmq整合springboot项目,结合图文详细演示了一个集合消息生产者和消息消费者很公共模块的聚合项目的搭建,演示了使用direct、topic和fanout三种交换机从生产者投递消息到消费端消费消息的详细过程,并结合作者踩过的坑给出了具体的解决办法,让读者在整合AMQP开发需求时少走很多弯路!

参考资料

[1] Spring AMQP参考文档

[2] 黄朝兵的达人课04整合常用技术框架之 MongoDB 和 RabbitMQ](https://gitbook.cn/gitchat/column/5b4fd439bf8ece6c81e44cfb/topic/5b50254103f7e37c51456ee9/ “[2] 黄朝兵的达人课04整合常用技术框架之 MongoDB 和 RabbitMQ”)

[3] 王松著《SpringBoot+Vue全栈开发实战》第12章消息服务

[4]等等!这两个 Spring-RabbitMQ 的坑我们已经替你踩了

原创不易,首次阅读作者文章的读者如果觉得文章对你有帮助欢迎扫描下方二位二维码关注作者的微信公众号,作者的微信公众号将第一时间不断输出技术干货。
公众号二维码
你的关注和点赞是作者持续创作的最大动力!

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号