[30] RabbitMQ-工作模式

1. 工作模式#

1.1 Work Queue#

a. 说明#

生产者发消息,启动多个消费者实例来消费消息,每个消费者仅消费部分信息,可达到负载均衡的效果。

b. 案例#

生产者代码:

public class Producer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");

        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

        // 声明一个消息队列
        channel.queueDeclare("queue.wq", true, false, false, null);
        // 声明Direct交换器
        channel.exchangeDeclare("ex.wq", BuiltinExchangeType.DIRECT, true, false, null);
        // 将消息队列绑定到指定的交换器,并指定绑定键
        channel.queueBind("queue.wq", "ex.wq", "key.wq");

        for (int i = 0; i < 15; i++) {
            channel.basicPublish("ex.wq", "key.wq", null, ("Msg_" + i).getBytes("utf-8"));
        }

        channel.close();
        connection.close();
    }

}

消费者代码:

public class Consumer {

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");

        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

        channel.queueDeclare("queue.wq", true, false, false, null);

        channel.basicConsume("queue.wq",
                new DeliverCallback() {
                    @Override
                    public void handle(String consumerTag, Delivery message) throws IOException {
                        System.out.println("Receive: " + new String(message.getBody(), "utf-8"));
                    }
                },
                new CancelCallback() {
                    @Override
                    public void handle(String consumerTag) throws IOException {
                        System.out.println("Cancel: " + consumerTag);
                    }
                });
    }
}

Consumer 跑 3 个,即 3 个消费者:

然后将生产者启动起来。

随即消费者控制台打印:

[Consuemr1]
Receive: Msg_0
Receive: Msg_3
Receive: Msg_6
Receive: Msg_9
Receive: Msg_12

[Consuemr2]
Receive: Msg_1
Receive: Msg_4
Receive: Msg_7
Receive: Msg_10
Receive: Msg_13

[Consuemr3]
Receive: Msg_2
Receive: Msg_5
Receive: Msg_8
Receive: Msg_11
Receive: Msg_14

1.2 发布订阅模式#

a. 说明#

使用 Fanout 类型交换器,routingKey 忽略。每个消费者定义生成一个队列并绑定到同一个 Exchange,每个消费者都可以消费到完整的消息。

消息广播给所有订阅该消息的消费者。

在 RabbitMQ 中,生产者不是将消息直接发送给消息队列,实际上生产者根本不知道一个消息被发送到哪个队列。

生产者将消息发送给交换器。交换器非常简单,从生产者接收消息,将消息推送给消息队列。交换器必须清楚地知道要怎么处理接收到的消息。应该是追加到一个指定的队列,还是追加到多个队列,还是丢弃。规则就是「交换器类型」。

交换器的类型前面已经介绍过了: direct 、 topic 、 headers 和 fanout 四种类型。发布订阅使用 Fanout。Fanout 交换器很简单,从名字就可以看出来(用风扇吹出去),将所有收到的消息发送给它知道的所有的队列。

列出 RabbitMQ 的交换器,包括了 amq.* 的和默认的(未命名)的交换器。

$ rabbitmqctl list_exchanges --formatter pretty_table
Listing exchanges for vhost / ...
+--------------------+---------+
| name               | type    |
+--------------------+---------+
| amq.direct         | direct  |
+--------------------+---------+
| amq.topic          | topic   |
+--------------------+---------+
| amq.match          | headers |
+--------------------+---------+
| amq.rabbitmq.trace | topic   |
+--------------------+---------+
| ex.biz             | direct  |
+--------------------+---------+
| amq.fanout         | fanout  |
+--------------------+---------+
| amq.headers        | headers |
+--------------------+---------+
| ex.wq              | direct  |
+--------------------+---------+
|                    | direct  |
+--------------------+---------+

未命名交换器

在前面的那里中我们没有指定交换器,但是依然可以向队列发送消息。这是因为我们使用了默认的交换器。

channel.basicPublish("", "hello", null, message.getBytes());

第一个参数就是交换器名称,为空字符串。直接使用 routingKey 向队列发送消息,会发送到名为 “hello“ 的队列(如果该队列存在)。

临时队列

前面我们使用队列的名称,生产者和消费者都是用该名称来发送和接收该队列中的消息。

首先,我们无论何时连接 RabbitMQ 的时候,都需要一个新的、空的队列。我们可以使用随机的名字创建队列,也可以让服务器帮我们生成随机的消息队列名字。

其次,一旦我们断开到消费者的连接,该队列应该自动删除。

String queueName = channel.queueDeclare().getQueue();

上述代码我们声明了一个非持久化的、排他的、自动删除的队列,并且名字是服务器随机生成的。

queueName 一般的格式类似:amq.gen-JzTY20BRgKO-HjmUJj0wLg 。

绑定

在创建了消息队列和 Fanout 类型的交换器之后,我们需要将两者进行绑定,让交换器将消息发送给该队列。

// Fanout 类型的交换器绑定不需要 BINDING_KEY
channel.queueBind(queueName, "logs", "");

此时, logs 交换器会将接收到的消息追加到 queueName 队列中。

b. 案例#

生产者代码:

public class Producer {
  
  private static String FANOUT_EXCHANGE_NAME = "ex.my.fanout";
  private static String FANOUT_BINDING_KEY = ""; // FANOUT 类型的交换器不需要指定路由键(绑定键)

  public static void main(String[] args) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");
    final Connection connection = factory.newConnection();
    final Channel channel = connection.createChannel();

    // 声明 FANOUT 类型的交换器
    channel.exchangeDeclare(FANOUT_EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, null);

    for (int i = 0; i < 10; i++) {
      // FANOUT 类型的交换器不需要指定路由键
      channel.basicPublish(FANOUT_EXCHANGE_NAME, FANOUT_BINDING_KEY, null, ("FANOUT TEST:" + i).getBytes("utf-8"));
    }

    channel.close();
    connection.close();
  }
}

消费者代码:

public class Consumer {
  
  private static String FANOUT_EXCHANGE_NAME = "ex.my.fanout";
  private static String FANOUT_BINDING_KEY = "";

  public static void main(String[] args) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");

    final Connection connection = factory.newConnection();
    final Channel channel = connection.createChannel();

    // 声明临时队列,队列的名字由RabbitMQ自动生成
    final String queueName = channel.queueDeclare().getQueue();
    System.out.println("生成的临时队列的名字为:" + queueName);

    channel.exchangeDeclare(FANOUT_EXCHANGE_NAME, BuiltinExchangeType.FANOUT, true, false, null);

    // Fanout类型的交换器绑定不需要BingdingKey
    channel.queueBind(queueName, FANOUT_EXCHANGE_NAME, FANOUT_BINDING_KEY);

    channel.basicConsume(
        queueName,
        (consumerTag, message) -> System.out.println(String.format("Consumer[%s] receive: %s", 
                                                     args[0], new String(message.getBody(), "utf-8"))),
        consumerTag -> {}
    );

  }
}

生产者面向 Exchange,消费者面向 Queue。

先把消费者启动起来,然后通过命令 rabbitmqctl list_bindings 列出绑定关系:

[root@centos7 ~]# rabbitmqctl list_bindings --formatter pretty_table
Listing bindings for vhost /...
+--------------+-------------+--------------------------------+------------------+--------------------------------+-----------+
| source_name  | source_kind | destination_name               | destination_kind | routing_key                    | arguments |
+--------------+-------------+--------------------------------+------------------+--------------------------------+-----------+
| ex.my.fanout | exchange    | amq.gen-LLTroI77YkR9MjOLYe93Fw | queue            |                                |           |
+--------------+-------------+--------------------------------+------------------+--------------------------------+-----------+
| ex.my.fanout | exchange    | amq.gen-YKNXwWpmfZbMPLZlS_w1HQ | queue            |                                |           |
+--------------+-------------+--------------------------------+------------------+--------------------------------+-----------+
| ex.my.fanout | exchange    | amq.gen-rEly-8lAd_oHzqkvZegTzQ | queue            |                                |           |
+--------------+-------------+--------------------------------+------------------+--------------------------------+-----------+
| ex.wq        | exchange    | queue.wq                       | queue            | key.wq                         |           |
+--------------+-------------+--------------------------------+------------------+--------------------------------+-----------+

消息的推拉

实现 RabbitMQ 的消费者有两种模式,推模式(Push)和拉模式(Pull)。

实现推模式推荐的方式是继承 DefaultConsumer 基类,也可以使用 Spring AMQP 的 SimpleMessageListenerContainer。推模式是最常用的,但是有些情况下推模式并不适用的,比如说: 由于某些限制,消费者在某个条件成立时才能消费消息,需要批量拉取消息进行处理。实现拉模式 —— RabbitMQ 的 Channel 提供了 basicGet 方法用于拉取消息。

1.3 路由模式-Direct#

a. 说明#

使用 Direct 类型的 Exchange,发 N 条消费并使用不同的 routingKey ,消费者定义队列并将 Queue、 RoutingKey 、Exchange 绑定。此时使用 Direct 模式Exchange 必须要 RoutingKey 完全匹配的情况下消息才会转发到对应的队列中被消费。

上一个模式中,可以将消息广播到很多接收者。

// [Fanout] 交换机的使用方式,第三个参数 BingdingKey 不使用
channel.queueBind(queueName, EXCHANGE_NAME, "");

BindingKey 的作用与具体使用的交换器类型有关。对于 Fanout 类型的交换器,此参数设置无效,系统直接忽略。

场景如下:分布式系统中有很多应用,这些应用需要运维平台的监控,其中一个重要的信息就是服务器的日志记录。我们需要将不同日志级别的日志记录交给不同的应用处理。

如何解决?

使用 Direct 交换器。

如果要对不同的消息做不同的处理,此时不能使用 Fanout 类型的交换器,因为它只会盲目的广播消息。我们需要使用 Direct 类型的交换器。 Direct 交换器的路由算法很简单:只要消息的 RoutingKey 和队列的 BindingKey 对应,消息就可以推送给该队列。

上图中的交换器 X 是 Direct 类型的交换器,绑定的两个队列中,一个队列的 BindingKey 是 orange ,另一个队列的 BindingKey 是 black 和 green。如此,则 RoutingKey 是 orange 的消息发送给队列 Q1, RoutingKey 是 black 和 green 的消息发送给 Q2 队列,其他消息丢弃。

【多重绑定】

如图,虽然 Exchange 的绑定类型是 Direct,但是它绑定的多个队列的 BindingKey 如果都相同,在这种情况下虽然绑定类型是 Direct 但是它表现的就和 Fanout有点类似了,就跟广播差不多。

b. 案例#

在案例中,我们将日志级别作为 RoutingKey。

(1)ReceiveLogsDirect01

public class ReceiveLogsDirect01 {
    private static final String EXCHANGE_NAME = "direct_logs";
    private static final String QUEUE_NAME = "disk";
    private static final String BINDING_KEY = "error";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");
        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();
      
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, BINDING_KEY);
      
        System.out.println("等待接收消息...");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("ERROR>>> " + message);
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {});
    }
}

(2)ReceiveLogsDirect02

public class ReceiveLogsDirect02 {
    private static final String EXCHANGE_NAME = "direct_logs";
    private static final String QUEUE_NAME = "console";
    private static final String BINDING_KEY_INFO = "info";
    private static final String BINDING_KEY_WARNING = "warning";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");
        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();
      
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, BINDING_KEY_INFO);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, BINDING_KEY_WARNING);
      
        System.out.println("等待接收消息...");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(delivery.getEnvelope().getRoutingKey() + "> " + message);
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {});
    }
}

(3)EmitLogDirect

public class EmitLogDirect {
    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");
        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        // 创建多个BindingKey(绑定是'交换机'和'队列'之间的桥梁关系)
        Map<String, String> bindingKeyMap = new HashMap<>();
        bindingKeyMap.put("info", "普通 info 信息");
        bindingKeyMap.put("warning", "警告 warning 信息");
        bindingKeyMap.put("error", "错误 error 信息");
        bindingKeyMap.put("debug", "调试 debug 信息"); // 该消息默认会被丢弃

        for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
            String bindingKey = bindingKeyEntry.getKey();
            String message = bindingKeyEntry.getValue();
            channel.basicPublish(EXCHANGE_NAME, bindingKey, null, message.getBytes("UTF-8"));
            System.out.println("SendMsg: " + message);
        }
    }
}

多个消费者消费的消息量加起来才是生产者发出的消息量。

1.4 路由模式-Topic#

a. 说明#

使用 topic 类型的交换器,Queue 绑定到 Exchange、 BindingKey 时使用通配符,交换器将消息路由转发到具体队列时会根据消息 RoutingKey 模糊匹配,比较灵活。

上个模式中,我们通过 Direct 类型的交换器做到了根据日志级别的不同,将消息发送给了不同队列的。

这里有一个限制,加入现在我不仅想根据日志级别划分日志消息,还想根据日志来源划分日志,怎么做?

比如,我想监听 cron 服务发送的 error 消息,又想监听从 kern 服务发送的所有消息。

此时可以使用 RabbitMQ 的 Topic 模式。

要想 topic 类型的交换器, RoutingKey 就不能随便写了,它必须得是点分单词。

单词可以随便写,生产中一般使用消息的特征。如:“stock.usd.nyse”,“nyse.vmw”,“quick.orange.rabbit”等。该点分单词字符串最长 255 字节。

BindingKey 也必须是这种形式。 topic 类型的交换器背后原理跟 Direct 类型的类似:只要队列的 BindingKey 的值与消息的 RoutingKey 匹配,队列就可以收到该消息。但有两个不同:

  1. * 匹配 1 个单词
  2. # 匹配 0 到多个单词

【举例说明】我们发送描述动物的消息。消息发送的时候指定的 RoutingKey 包含了三个词,两个点。第一个单词表示动物的速度,第二个是颜色,第三个是物种:<speed>.<color>.<species>

绑定关系如下:

Q1 绑定的是:
* 中间带 orange 带 3 个单词的字符串(*.orange.*)
Q2 绑定的是:
* 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
* 第一个单词是 lazy 的多个单词(lazy.#)

上图是一个队列绑定关系图,我们来看看他们之间数据接收情况是怎么样的:

quick.orange.rabbit       被队列 Q1Q2 接收到
lazy.orange.elephant      被队列 Q1Q2 接收到
quick.orange.fox          被队列 Q1 接收到
lazy.brown.fox            被队列 Q2 接收到
lazy.pink.rabbit          虽然满足两个绑定但只被队列 Q2 接收一次
quick.brown.fox           不匹配任何绑定不会被任何队列接收到会被丢弃
quick.orange.male.rabbit  是四个单词不匹配任何绑定会被丢弃
lazy.orange.male.rabbit   是四个单词但匹配 Q2
  • 如果在 topic 类型的交换器中 BindingKey 使用 # ,则就是 Fanout 类型交换器的行为。
  • 如果在 topic 类型的交换器中 BindingKey 中不使用 *# ,则就是 Direct 类型交换器的行为。

b. 案例#

(1)消费者1

public class ReceiveLogsTopic01 {
    
    private static final String EXCHANGE_NAME = "topic_logs";
    private static final String QUEUE_NAME = "Q1";
    private static final String BINDING_KEY = "*.orange.*";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");
        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        // 声明 Q1 队列与绑定关系
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, BINDING_KEY);

        System.out.println("等待接收消息...");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("接收队列:" + QUEUE_NAME + ",绑定键:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message);
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {});
    }
}

(2)消费者2

public class ReceiveLogsTopic02 {

    private static final String EXCHANGE_NAME = "topic_logs";
    private static final String QUEUE_NAME = "Q2";
    private static final String BINDING_KEY_1 = "*.*.rabbit";
    private static final String BINDING_KEY_2 = "lazy.#";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");
        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 声明 Q2 队列与绑定关系
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, BINDING_KEY_1);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, BINDING_KEY_2);
        System.out.println("等待接收消息...");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("接收队列:" + QUEUE_NAME + ",绑定键:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message);
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
        });
    }
}

(3)生产者

public class EmitLogTopic {
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri("amqp://root:123456@192.168.6.160:5672/%2f");
        final Connection connection = factory.newConnection();
        final Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        /**
         * Q1 -> 绑定的是:
         * - 中间带 orange 带 3 个单词的字符串(*.orange.*)
         * Q2 -> 绑定的是
         * - 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
         * - 第一个单词是 lazy 的多个单词(lazy.#)
         */
        Map<String, String> bindingKeyMap = new HashMap<>();
        bindingKeyMap.put("quick.orange.rabbit", "被队列 Q1Q2 接收到");
        bindingKeyMap.put("lazy.orange.elephant", "被队列 Q1Q2 接收到");
        bindingKeyMap.put("quick.orange.fox", "被队列 Q1 接收到");
        bindingKeyMap.put("lazy.brown.fox", "被队列 Q2 接收到");
        bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 Q2 接收一次");
        bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
        bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
        bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2");
        for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
            String bindingKey = bindingKeyEntry.getKey();
            String message = bindingKeyEntry.getValue();
            channel.basicPublish(EXCHANGE_NAME, bindingKey, null, message.getBytes("UTF-8"));
            System.out.println("生产者发出消息:" + message);
        }
    }
}

2. 整合 Spring#

spring-amqp 是对 AMQP 的一些概念的一些抽象,spring-rabbit 是对 RabbitMQ 操作的封装实现。

主要有几个核心类 RabbitAdmin 、 RabbitTemplate 、 SimpleMessageListenerContainer 等。

  • RabbitAdmin 类完成对 Exchange、Queue、Binding 的操作,在容器中管理了 RabbitAdmin 类的时候,可以对 Exchange,Queue,Binding 进行自动声明。
  • RabbitTemplate 类是发送和接收消息的工具类。
  • SimpleMessageListenerContainer 是消费消息的容器。

目前比较新的一些项目都会选择基于注解方式,而比较老的一些项目可能还是基于配置文件的。

2.1 基于配置文件整合#

(1)创建 Maven 工程

(2)配置 pom.xml,添加 RabbitMQ 的 Spring 依赖

<dependencies>
  <dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
    <version>2.2.7.RELEASE</version>
  </dependency>
</dependencies>

(3)rabbit-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<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/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/rabbit
                            http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
    <rabbit:connection-factory id="connectionFactory"
                               host="node1"
                               virtual-host="/"
                               username="root"
                               password="123456"
                               port="5672"/>
    <!-- 创建一个rabbit的template对象(org.springframework.amqp.rabbit.core.RabbitTemplate)以便于访问broker -->
    <rabbit:template id="amqpTemplate" connection-factory="connectionFactory"/>
    <!-- 自动查找类型是Queue、Exchange、Binding的bean,并为用户向 RabbitMQ 声明 -->
    <!-- 因此,我们不需要显式地在java中声明 -->
    <rabbit:admin id="rabbitAdmin" connection-factory="connectionFactory"/>
    <!-- 为消费者创建一个队列,如果broker中存在,则使用同名存在的队列,否则创建一个新的。 -->
    <!-- 如果要发送消息,得使用交换器 -->
    <!-- 这里使用的是默认的交换器 -->
    <rabbit:queue name="myqueue"/>
    <rabbit:direct-exchange name="direct.biz.ex" auto-declare="true" auto-delete="false" durable="false">
        <rabbit:bindings>
            <!-- exchange:其他绑定到该交换器的交换器名称 -->
            <!-- queue:绑定到该交换器的queue的bean名称 -->
            <!-- bindingKey:显式声明的绑定key -->
            <rabbit:binding queue="myqueue" key="dir.ex"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>
</beans>

(4)Application.java

/**
 * 使用 Spring XML 配置的方式发送接接收消息
 */
public class App {
    public static void main(String[] args) {
        AbstractApplicationContext context = new GenericXmlApplicationContext("classpath:/rabbit-context.xml");
        AmqpTemplate template = context.getBean(AmqpTemplate.class);
        for (int i = 0; i < 1000; i++) {
            // 第一个参数是路由key,第二个参数是消息
            template.convertAndSend("dir.ex", "foo" + i);
        }
        // 主动从队列拉取消息
        String foo = (String) template.receiveAndConvert("myqueue");
        System.out.println(foo);
        context.close();
    }
}

启动 RabbitMQ 之后,直接运行即可。

2.2 基于注解整合#

(1)创建 Maven 项目

(2)配置 pom.xml,添加 RabbitMQ 的 Spring 依赖

(3)添加 ProducerApp 配置类 RabbitProducerConfig.java

@Configuration
public class RabbitProducerConfig {

    // 连接工厂
    @Bean
    public ConnectionFactory connectionFactory() {
        ConnectionFactory factory = new CachingConnectionFactory(URI.create("amqp://root:123456@node1:5672/%2f"));
        return factory;
    }

    // RabbitTemplate
    @Bean
    @Autowired
    public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(factory);
        return rabbitTemplate;
    }

    // RabbitAdmin
    @Bean
    @Autowired
    public RabbitAdmin rabbitAdmin(ConnectionFactory factory) {
        RabbitAdmin rabbitAdmin = new RabbitAdmin(factory);
        return rabbitAdmin;
    }

    // Queue
    @Bean
    public Queue queue() {
        final Queue queue = QueueBuilder.nonDurable("queue.anno").build();
        return queue;
    }

    // Exchange
    @Bean
    public Exchange exchange() {
        final FanoutExchange fanoutExchange = new FanoutExchange("ex.anno.fanout", false, false, null);
        return fanoutExchange;
    }

    // Binding
    @Bean
    @Autowired
    public Binding binding(Queue queue, Exchange exchange) {
        // 创建一个绑定,不指定绑定的参数
        final Binding binding = BindingBuilder.bind(queue).to(exchange).with("key.anno").noargs();
        return binding;
    }
}

(4)ProducerApp 启动类

public class ProducerApp {
    public static void main(String[] args) throws UnsupportedEncodingException {
      
        AbstractApplicationContext context = new AnnotationConfigApplicationContext(RabbitProducerConfig.class);
      
        final RabbitTemplate template = context.getBean(RabbitTemplate.class);
      
        final MessageProperties messageProperties = MessagePropertiesBuilder
                .newInstance()
                .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
                .setContentEncoding("gbk")
                .setHeader("myKey", "myValue")
                .build();

        final Message message = MessageBuilder
                    .withBody(("HelloWorld").getBytes("gbk"))
                    .andProperties(messageProperties)
                    .build();
        template.send("ex.anno.fanout", "key.anno", message);

        context.close();
    }
}

(5)另建一个 Maven 项目,添加 ConsumerApp 配置类 RabbitConsumerConfig.java

@Configuration
public class RabbitConsumerConfig {

    @Bean
    public ConnectionFactory connectionFactory() {
        return new CachingConnectionFactory(URI.create("amqp://root:123456@node1:5672/%2f"));
    }

    @Bean
    @Autowired
    public RabbitTemplate rabbitTemplate(ConnectionFactory factory) {
        return new RabbitTemplate(factory);
    }

    @Bean
    @Autowired
    public RabbitAdmin rabbitAdmin(ConnectionFactory factory) {
        return new RabbitAdmin(factory);
    }

    @Bean
    public Queue queue() {
        return QueueBuilder.nonDurable("queue.anno").build();
    }

}

(6)ConsumerApp 启动类

public class ConsumerApp {
    public static void main(String[] args) throws UnsupportedEncodingException {

        // 从指定类加载配置信息
        AbstractApplicationContext context = new AnnotationConfigApplicationContext(RabbitConsumerConfig.class);
        // 获取RabbitTemplate对象
        final RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);
        // 接收消息
        final Message message = rabbitTemplate.receive("queue.anno");
        // 打印消息
        System.out.println(new String(message.getBody(), message.getMessageProperties().getContentEncoding()));
        // 关闭Spring的上下文
        context.close();

    }
}

3. 整合 SpringBoot#

(1)添加 starter 依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

(2)application.properties 中添加连接信息

spring.application.name=springboot_rabbitmq
spring.rabbitmq.host=node1
spring.rabbitmq.virtual-host=/
spring.rabbitmq.username=root
spring.rabbitmq.password=123456
spring.rabbitmq.port=5672

(3)RabbitConfig 类

@Configuration
public class RabbitConfig {

    @Bean
    public Queue queue() {
        return new Queue("queue.boot", false, false, false, null);
    }

    @Bean
    public Exchange exchange() {
        return new TopicExchange("ex.boot", false, false, null);
    }

    @Bean
    public Binding binding() {
        return new Binding("queue.boot", Binding.DestinationType.QUEUE, "ex.boot", "key.boot", null);
    }

}

(4)使用 RestController 发送消息

@RestController
public class MessageController {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    @RequestMapping("/rabbit/{message}")
    public String receive(@PathVariable String message) throws UnsupportedEncodingException {

        final MessageProperties messageProperties = MessagePropertiesBuilder.newInstance()
                .setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN)
                .setContentEncoding("utf-8")
                .setHeader("hello", "world")
                .build();

        final Message msg = MessageBuilder
                .withBody(message.getBytes("utf-8"))
                .andProperties(messageProperties)
                .build();

        rabbitTemplate.send("ex.boot", "key.boot", msg);

        return "ok";
    }

}

(5)使用监听器,用于消费消息

@Component
public class MyMessageListener {

//    @RabbitListener(queues = "queue.boot")
//    public void getMyMessage(@Payload String message, @Header(name = "hello") String value, Channel channel) {
//        System.out.println(message);
//        System.out.println("hello = " + value);
//
//        // 确认消息
//        channel.basicAck();
//        // 拒收消息
//        channel.basicReject();
//    }

    private Integer index = 0;

    @RabbitListener(queues = "queue.boot")
    public void getMyMessage(Message message, Channel channel) throws IOException {
        String value = message.getMessageProperties().getHeader("hello");

        System.out.println(message);
        System.out.println("hello = " + value);

        final long deliveryTag = message.getMessageProperties().getDeliveryTag();

        if (index % 2 == 0) {
            // 确认消息
            channel.basicAck(deliveryTag, false);
        } else {
            // 拒收消息
            channel.basicReject(deliveryTag, false);
        }
        index++;
    }

}
posted @   tree6x7  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示
主题色彩