Java 中间件之 RabbitMQ 快速入门: SpringAMQP 的 DirectExchange 路由模式

概述

Fanout 交换机会把消息无差别地复制给所有绑定队列,一旦你需要"这条消息只给告警服务,不给归档服务",Fanout 就不够用了。DirectExchange 用 routingKey 与 bindingKey 的精确匹配解决这件事:发送方带上 key,交换机只把消息投给 bindingKey 完全相等的队列。

纲要

  • Direct 与 Fanout 的差异
    • Fanout:广播,忽略 key,所有绑定队列各拿一份
    • Direct:定向,routingKey 与 bindingKey 全等才投递
    • 多个队列绑定相同 key 时,Direct 可退化成广播
  • 路由原理与投递结果
    • routingKey / bindingKey 的对暗号模型
    • 一组消息(red / blue / yellow / green)的实际落点
    • 无匹配 bindingKey 时消息被静默丢弃
  • 动手:基于 @RabbitListener 注解声明交换机、队列、绑定
    • @QueueBinding / @Queue / @Exchange 三个注解
    • 与 @Bean 声明式写法的对比
  • 两种写法对比
    • @Bean + BindingBuilder(FanoutConfig 那一套)
    • 注解式(本文重点,代码量少一半)
  • 典型落地场景:日志分级
    • error 进告警队列,info/warn 只进归档队列
  • 实战坑
    • key 写错导致消息被丢且无报错
    • 一个队列绑多个 key 的两种写法
    • Direct 是精确匹配,order.* 不会被当通配符

Direct 与 Fanout 的差异

FanoutExchange 是"订阅即收",交换机完全不看消息内容,把消息复制 N 份发给 N 个绑定队列。DirectExchange 是"对暗号":

  • 每个队列与交换机绑定时都要声明一个 bindingKey,一个队列可以声明多个
  • 生产者发送消息时必须带上 routingKey
  • 交换机逐个比对,routingKey.equals(bindingKey) 成立才投递

#mermaid-svg-wTXDCBnXpGUuC5Go{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wTXDCBnXpGUuC5Go .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wTXDCBnXpGUuC5Go .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wTXDCBnXpGUuC5Go .error-icon{fill:#552222;}#mermaid-svg-wTXDCBnXpGUuC5Go .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wTXDCBnXpGUuC5Go .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wTXDCBnXpGUuC5Go .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wTXDCBnXpGUuC5Go .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wTXDCBnXpGUuC5Go .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wTXDCBnXpGUuC5Go .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wTXDCBnXpGUuC5Go .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wTXDCBnXpGUuC5Go .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wTXDCBnXpGUuC5Go .marker.cross{stroke:#333333;}#mermaid-svg-wTXDCBnXpGUuC5Go svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wTXDCBnXpGUuC5Go p{margin:0;}#mermaid-svg-wTXDCBnXpGUuC5Go .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wTXDCBnXpGUuC5Go .cluster-label text{fill:#333;}#mermaid-svg-wTXDCBnXpGUuC5Go .cluster-label span{color:#333;}#mermaid-svg-wTXDCBnXpGUuC5Go .cluster-label span p{background-color:transparent;}#mermaid-svg-wTXDCBnXpGUuC5Go .label text,#mermaid-svg-wTXDCBnXpGUuC5Go span{fill:#333;color:#333;}#mermaid-svg-wTXDCBnXpGUuC5Go .node rect,#mermaid-svg-wTXDCBnXpGUuC5Go .node circle,#mermaid-svg-wTXDCBnXpGUuC5Go .node ellipse,#mermaid-svg-wTXDCBnXpGUuC5Go .node polygon,#mermaid-svg-wTXDCBnXpGUuC5Go .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wTXDCBnXpGUuC5Go .rough-node .label text,#mermaid-svg-wTXDCBnXpGUuC5Go .node .label text,#mermaid-svg-wTXDCBnXpGUuC5Go .image-shape .label,#mermaid-svg-wTXDCBnXpGUuC5Go .icon-shape .label{text-anchor:middle;}#mermaid-svg-wTXDCBnXpGUuC5Go .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wTXDCBnXpGUuC5Go .rough-node .label,#mermaid-svg-wTXDCBnXpGUuC5Go .node .label,#mermaid-svg-wTXDCBnXpGUuC5Go .image-shape .label,#mermaid-svg-wTXDCBnXpGUuC5Go .icon-shape .label{text-align:center;}#mermaid-svg-wTXDCBnXpGUuC5Go .node.clickable{cursor:pointer;}#mermaid-svg-wTXDCBnXpGUuC5Go .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wTXDCBnXpGUuC5Go .arrowheadPath{fill:#333333;}#mermaid-svg-wTXDCBnXpGUuC5Go .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wTXDCBnXpGUuC5Go .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wTXDCBnXpGUuC5Go .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wTXDCBnXpGUuC5Go .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wTXDCBnXpGUuC5Go .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wTXDCBnXpGUuC5Go .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wTXDCBnXpGUuC5Go .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wTXDCBnXpGUuC5Go .cluster text{fill:#333;}#mermaid-svg-wTXDCBnXpGUuC5Go .cluster span{color:#333;}#mermaid-svg-wTXDCBnXpGUuC5Go div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wTXDCBnXpGUuC5Go .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wTXDCBnXpGUuC5Go rect.text{fill:none;stroke-width:0;}#mermaid-svg-wTXDCBnXpGUuC5Go .icon-shape,#mermaid-svg-wTXDCBnXpGUuC5Go .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wTXDCBnXpGUuC5Go .icon-shape p,#mermaid-svg-wTXDCBnXpGUuC5Go .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wTXDCBnXpGUuC5Go .icon-shape .label rect,#mermaid-svg-wTXDCBnXpGUuC5Go .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wTXDCBnXpGUuC5Go .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wTXDCBnXpGUuC5Go .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wTXDCBnXpGUuC5Go :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} bindingKey = red
bindingKey = blue
bindingKey = red
bindingKey = yellow
routingKey = green

无匹配,丢弃
publisher

convertAndSend(exchange, routingKey, msg)
itcast.direct

DirectExchange
direct.queue1
direct.queue2
consumer1

listenDirectQueue1
consumer2

listenDirectQueue2
(丢弃)

维度 FanoutExchange DirectExchange
路由依据 不看 key routingKey 与 bindingKey 全等
一条消息的落点 所有绑定队列 匹配到的队列(0 个到 N 个)
广播能力 天然广播 所有队列绑同一个 key 时可以模拟广播
发送方负担 routingKey 填空串即可 必须正确填写 routingKey
灵活性 低 高,代价是多维护一份 key 约定

反过来说,Direct 比 Fanout 灵活的代价是"多了一个会写错的东西"。key 写错不报错,直接丢消息,这是后面要重点讲的坑。

路由原理与投递结果

以课程案例的绑定关系为准:direct.queue1 绑定 red、blue;direct.queue2 绑定 red、yellow。四条消息的投递结果如下。

routingKey direct.queue1(blue, red) direct.queue2(red, yellow) 结果说明
red 收到 收到 两个队列都绑定了 red,等价于一次广播
blue 收到 未收到 只有 queue1 绑定 blue
yellow 未收到 收到 只有 queue2 绑定 yellow
green 未收到 未收到 没有队列绑定 green,消息被交换机丢弃

最后一行是 Direct 模式最容易被忽略的行为:交换机不会因为你发了个陌生 key 而报错,消息直接被丢掉 。RabbitMQ 只在 mandatory=true 且配置了 ReturnCallback 时才把无法路由的消息退回给生产者,默认是静默丢弃。
direct.queue2 direct.queue1 itcast.direct publisher direct.queue2 direct.queue1 itcast.direct publisher #mermaid-svg-lABQ6c6a3qXe6gLW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lABQ6c6a3qXe6gLW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lABQ6c6a3qXe6gLW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lABQ6c6a3qXe6gLW .error-icon{fill:#552222;}#mermaid-svg-lABQ6c6a3qXe6gLW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lABQ6c6a3qXe6gLW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lABQ6c6a3qXe6gLW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lABQ6c6a3qXe6gLW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lABQ6c6a3qXe6gLW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lABQ6c6a3qXe6gLW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lABQ6c6a3qXe6gLW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lABQ6c6a3qXe6gLW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lABQ6c6a3qXe6gLW .marker.cross{stroke:#333333;}#mermaid-svg-lABQ6c6a3qXe6gLW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lABQ6c6a3qXe6gLW p{margin:0;}#mermaid-svg-lABQ6c6a3qXe6gLW .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lABQ6c6a3qXe6gLW text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-lABQ6c6a3qXe6gLW .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lABQ6c6a3qXe6gLW .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-lABQ6c6a3qXe6gLW .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-lABQ6c6a3qXe6gLW .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-lABQ6c6a3qXe6gLW #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-lABQ6c6a3qXe6gLW .sequenceNumber{fill:white;}#mermaid-svg-lABQ6c6a3qXe6gLW #sequencenumber{fill:#333;}#mermaid-svg-lABQ6c6a3qXe6gLW #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-lABQ6c6a3qXe6gLW .messageText{fill:#333;stroke:none;}#mermaid-svg-lABQ6c6a3qXe6gLW .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lABQ6c6a3qXe6gLW .labelText,#mermaid-svg-lABQ6c6a3qXe6gLW .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-lABQ6c6a3qXe6gLW .loopText,#mermaid-svg-lABQ6c6a3qXe6gLW .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-lABQ6c6a3qXe6gLW .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-lABQ6c6a3qXe6gLW .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-lABQ6c6a3qXe6gLW .noteText,#mermaid-svg-lABQ6c6a3qXe6gLW .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-lABQ6c6a3qXe6gLW .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lABQ6c6a3qXe6gLW .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lABQ6c6a3qXe6gLW .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-lABQ6c6a3qXe6gLW .actorPopupMenu{position:absolute;}#mermaid-svg-lABQ6c6a3qXe6gLW .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-lABQ6c6a3qXe6gLW .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-lABQ6c6a3qXe6gLW .actor-man circle,#mermaid-svg-lABQ6c6a3qXe6gLW line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-lABQ6c6a3qXe6gLW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} routingKey=red bindingKey=red 命中 bindingKey=red 命中 routingKey=green 无匹配 binding,消息丢弃(默认不通知生产者)

动手前的工程结构

案例沿用 mq-demo 父子工程,publisher 发消息,consumer 收消息。本文涉及的 Direct 相关文件如下。

text 复制代码
mq-demo
├── pom.xml                                    # 父工程,Spring Boot 2.x + spring-boot-starter-amqp
├── publisher
│   └── src
│       ├── main/java/cn/itcast/mq/PublisherApplication.java
│       ├── main/resources/application.yml
│       └── test/java/cn/itcast/mq/spring/SpringAmqpTest.java      # Direct 发送测试
└── consumer
    └── src
        ├── main/java/cn/itcast/mq/ConsumerApplication.java
        ├── main/java/cn/itcast/mq/config/FanoutConfig.java         # @Bean 声明式(对照)
        ├── main/java/cn/itcast/mq/listener/SpringRabbitListener.java  # 注解式声明(本文重点)
        └── main/resources/application.yml

两个服务的连接配置一致:

yaml 复制代码
spring:
  rabbitmq:
    host: 192.168.150.101 # RabbitMQ 服务地址
    port: 5672
    username: itcast
    password: 123321
    virtual-host: /

基于注解声明队列与交换机

Fanout 那节课用的是 @Bean 声明式:一个交换机、两个队列、两个 Binding,光声明就写了五个 Bean。Direct 还要额外指定 key,会更啰嗦,所以这里换成注解式------直接在消费者方法上把交换机、队列、绑定关系一次性写完。

cn.itcast.mq.listener.SpringRabbitListener(真实项目代码,Direct 部分):

java 复制代码
package cn.itcast.mq.listener;

import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.time.LocalTime;
import java.util.Map;

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenWorkQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now());
        Thread.sleep(20);
    }

    @RabbitListener(queues = "simple.queue")
    public void listenWorkQueue2(String msg) throws InterruptedException {
        System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now());
        Thread.sleep(200);
    }

    // 注解式声明:队列 direct.queue1 绑定 itcast.direct,bindingKey = red / blue
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "blue"}
    ))
    public void listenDirectQueue1(String msg) {
        System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
    }

    // 注解式声明:队列 direct.queue2 绑定同一个交换机,bindingKey = red / yellow
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
            key = {"red", "yellow"}
    ))
    public void listenDirectQueue2(String msg) {
        System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
    }

    @RabbitListener(queues = "object.queue")
    public void listenObjectQueue(Map<String, Object> msg) {
        System.out.println("接收到object.queue的消息:" + msg);
    }
}

一个容易踩的启动问题:上面 simple.queue 和 object.queue 这两个方法只是按名字订阅 ,不负责创建队列。simple.queue 在前面简单队列模型一篇里已创建过,object.queue 在消息转换器那篇里也创建过,所以课程环境里直接能跑;但全新环境(管理台里没有这两个队列)启动 consumer 会报 404 NOT_FOUND 并反复重连。补两个最小声明即可,声明是幂等的,队列已存在时不会重复建:

java 复制代码
package cn.itcast.mq.config;

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class InitialQueuesConfig {

    @Bean
    public Queue simpleQueue() {
        return new Queue("simple.queue");   // 构造方法默认 durable=true
    }

    @Bean
    public Queue objectQueue() {
        return new Queue("object.queue");
    }
}

三个注解的分工:

注解 作用 关键属性
@QueueBinding 描述"队列 + 交换机 + key"这组绑定关系 value(队列)、exchange(交换机)、key(bindingKey 数组)
@Queue 声明队列 name、durable、exclusive、autoDelete
@Exchange 声明交换机 name、type(默认就是 DIRECT)、durable

@Exchange 的 type 默认值已经是 DIRECT,案例里仍然显式写了 ExchangeTypes.DIRECT。建议保持这个习惯:读代码的人不用去查默认值,也不会在复制粘贴去写 Topic 时漏改。

启动 consumer 后打开 RabbitMQ 管理台(15672 端口),可以看到 itcast.direct 交换机下挂了两条绑定:direct.queue1 → red, blue,direct.queue2 → red, yellow。声明动作发生在监听容器启动时,consumer 不起来的话这些资源不会存在。

消息发送

发送侧只多了第二个参数 routingKey,其余和 Fanout 完全一致。

java 复制代码
package cn.itcast.mq.spring;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendDirectExchange() {
        // 交换机名称
        String exchangeName = "itcast.direct";
        // 消息
        String message = "hello, red!";
        // 发送消息,第二个参数就是 routingKey
        rabbitTemplate.convertAndSend(exchangeName, "red", message);
    }
}

想一次验证多条 key,直接循环发:

java 复制代码
package cn.itcast.mq.spring;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpDirectBatchTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 依次发送 red / blue / yellow / green,观察 consumer 控制台:
     * red    -> queue1 + queue2 都收到
     * blue   -> 只有 queue1 收到
     * yellow -> 只有 queue2 收到
     * green  -> 无人收到,消息被交换机丢弃
     */
    @Test
    public void testSendDirectExchangeBatch() throws InterruptedException {
        String exchangeName = "itcast.direct";
        String[] keys = {"red", "blue", "yellow", "green"};
        for (String key : keys) {
            String message = "hello, " + key + "!";
            rabbitTemplate.convertAndSend(exchangeName, key, message);
            System.out.println("已发送 routingKey=" + key + " 的消息:" + message);
            Thread.sleep(500);
        }
    }
}

实测结果(Thread.sleep(500) 是为了让控制台输出按 key 分组,方便对照)。注意 publisher 是 JUnit 起的独立 JVM,consumer 是 Spring Boot 起的另一个 JVM,输出在两个终端里分别看,谁先谁后不影响结果------消息发进 RabbitMQ 后由 consumer 异步消费:

publisher 控制台:

text 复制代码
已发送 routingKey=red 的消息:hello, red!
已发送 routingKey=blue 的消息:hello, blue!
已发送 routingKey=yellow 的消息:hello, yellow!
已发送 routingKey=green 的消息:hello, green!

consumer 控制台:

text 复制代码
消费者接收到direct.queue1的消息:【hello, red!】
消费者接收到direct.queue2的消息:【hello, red!】
消费者接收到direct.queue1的消息:【hello, blue!】
消费者接收到direct.queue2的消息:【hello, yellow!】
(routingKey=green 的消息无任何消费者输出)

两种声明写法对比

对比项 @Bean 声明式 @RabbitListener 注解式
代码位置 独立的 @Configuration 类 消费者方法上方
一个队列绑两个 key 需要两个 Binding Bean key = {"red", "blue"} 一行
声明时机 容器刷新时创建 Bean 监听容器启动时解析注解
适合场景 交换机/队列需要被多个服务复用、要精细控制参数 队列只给当前消费者用,快速开发
可读性 声明集中,但类会越写越长 声明与消费逻辑在一起,一眼看清"我消费的是哪个队列、哪个 key"

同样的绑定关系,用 @Bean 写是这样的:

java 复制代码
package cn.itcast.mq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectConfig {

    // itcast.direct
    @Bean
    public DirectExchange directExchange() {
        return new DirectExchange("itcast.direct");
    }

    // direct.queue1
    @Bean
    public Queue directQueue1() {
        return new Queue("direct.queue1");
    }

    // direct.queue1 绑定 red
    @Bean
    public Binding directBinding1Red(Queue directQueue1, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
    }

    // direct.queue1 绑定 blue:一个 key 一个 Binding,不能省
    @Bean
    public Binding directBinding1Blue(Queue directQueue1, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
    }

    // direct.queue2
    @Bean
    public Queue directQueue2() {
        return new Queue("direct.queue2");
    }

    @Bean
    public Binding directBinding2Red(Queue directQueue2, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
    }

    @Bean
    public Binding directBinding2Yellow(Queue directQueue2, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
    }
}

BindingBuilder.bind(queue).to(directExchange).with("red") 里的 with(...) 一次只能给一个 key,所以四个 key 就写四个 Binding。这就是课程里放弃 @Bean 改用注解的直接原因。

典型落地场景:日志分级

日志是 Direct 模式最贴切的用法:同一份日志流,error 要立刻告警,info 只要落归档,warn 两边都不急。
#mermaid-svg-ymKWw1wlJzbTGzoc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ymKWw1wlJzbTGzoc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ymKWw1wlJzbTGzoc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ymKWw1wlJzbTGzoc .error-icon{fill:#552222;}#mermaid-svg-ymKWw1wlJzbTGzoc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ymKWw1wlJzbTGzoc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ymKWw1wlJzbTGzoc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ymKWw1wlJzbTGzoc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ymKWw1wlJzbTGzoc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ymKWw1wlJzbTGzoc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ymKWw1wlJzbTGzoc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ymKWw1wlJzbTGzoc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ymKWw1wlJzbTGzoc .marker.cross{stroke:#333333;}#mermaid-svg-ymKWw1wlJzbTGzoc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ymKWw1wlJzbTGzoc p{margin:0;}#mermaid-svg-ymKWw1wlJzbTGzoc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ymKWw1wlJzbTGzoc .cluster-label text{fill:#333;}#mermaid-svg-ymKWw1wlJzbTGzoc .cluster-label span{color:#333;}#mermaid-svg-ymKWw1wlJzbTGzoc .cluster-label span p{background-color:transparent;}#mermaid-svg-ymKWw1wlJzbTGzoc .label text,#mermaid-svg-ymKWw1wlJzbTGzoc span{fill:#333;color:#333;}#mermaid-svg-ymKWw1wlJzbTGzoc .node rect,#mermaid-svg-ymKWw1wlJzbTGzoc .node circle,#mermaid-svg-ymKWw1wlJzbTGzoc .node ellipse,#mermaid-svg-ymKWw1wlJzbTGzoc .node polygon,#mermaid-svg-ymKWw1wlJzbTGzoc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ymKWw1wlJzbTGzoc .rough-node .label text,#mermaid-svg-ymKWw1wlJzbTGzoc .node .label text,#mermaid-svg-ymKWw1wlJzbTGzoc .image-shape .label,#mermaid-svg-ymKWw1wlJzbTGzoc .icon-shape .label{text-anchor:middle;}#mermaid-svg-ymKWw1wlJzbTGzoc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ymKWw1wlJzbTGzoc .rough-node .label,#mermaid-svg-ymKWw1wlJzbTGzoc .node .label,#mermaid-svg-ymKWw1wlJzbTGzoc .image-shape .label,#mermaid-svg-ymKWw1wlJzbTGzoc .icon-shape .label{text-align:center;}#mermaid-svg-ymKWw1wlJzbTGzoc .node.clickable{cursor:pointer;}#mermaid-svg-ymKWw1wlJzbTGzoc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ymKWw1wlJzbTGzoc .arrowheadPath{fill:#333333;}#mermaid-svg-ymKWw1wlJzbTGzoc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ymKWw1wlJzbTGzoc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ymKWw1wlJzbTGzoc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ymKWw1wlJzbTGzoc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ymKWw1wlJzbTGzoc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ymKWw1wlJzbTGzoc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ymKWw1wlJzbTGzoc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ymKWw1wlJzbTGzoc .cluster text{fill:#333;}#mermaid-svg-ymKWw1wlJzbTGzoc .cluster span{color:#333;}#mermaid-svg-ymKWw1wlJzbTGzoc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ymKWw1wlJzbTGzoc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ymKWw1wlJzbTGzoc rect.text{fill:none;stroke-width:0;}#mermaid-svg-ymKWw1wlJzbTGzoc .icon-shape,#mermaid-svg-ymKWw1wlJzbTGzoc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ymKWw1wlJzbTGzoc .icon-shape p,#mermaid-svg-ymKWw1wlJzbTGzoc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ymKWw1wlJzbTGzoc .icon-shape .label rect,#mermaid-svg-ymKWw1wlJzbTGzoc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ymKWw1wlJzbTGzoc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ymKWw1wlJzbTGzoc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ymKWw1wlJzbTGzoc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} routingKey = error
routingKey = warn
routingKey = info
bindingKey = error
bindingKey = error, warn, info
业务服务

logProducer
log.direct
log.alarm.queue
log.archive.queue
告警服务

钉钉/短信 实时推送
归档服务

写入 ES / 对象存储

生产者按级别取 routingKey:

java 复制代码
package cn.itcast.mq.log;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class LogProducer {

    private static final String LOG_EXCHANGE = "log.direct";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 发送日志消息,routingKey 就是日志级别
     *
     * @param level   error / warn / info
     * @param content 日志正文
     */
    public void send(String level, String content) {
        rabbitTemplate.convertAndSend(LOG_EXCHANGE, level, content);
    }
}

消费者用注解声明绑定,告警队列只绑 error,归档队列绑三个级别:

java 复制代码
package cn.itcast.mq.log;

import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class LogListener {

    // 告警队列:只关心 error,fatal 级别要加就往 key 数组里补
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "log.alarm.queue"),
            exchange = @Exchange(name = "log.direct", type = ExchangeTypes.DIRECT),
            key = {"error"}
    ))
    public void listenAlarm(String msg) {
        System.err.println("[告警] 触发实时告警:" + msg);
    }

    // 归档队列:全量接收,注意 error 也要绑,否则告警过的日志反而丢了归档
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "log.archive.queue"),
            exchange = @Exchange(name = "log.direct", type = ExchangeTypes.DIRECT),
            key = {"error", "warn", "info"}
    ))
    public void listenArchive(String msg) {
        System.out.println("[归档] 写入存储:" + msg);
    }
}

这里有个容易漏的点:归档队列如果不绑 error,那些触发了告警的 error 日志反而不会进归档,排查问题时就查不到原始日志。绑多个 key 正好是 Direct 的强项。

实战坑

key 不匹配,消息被丢且没有报错。 「red」写成「Red」、多了个空格、写成了「read」,交换机都不会报错,消息直接消失。排查顺序:

  1. 管理台打开交换机详情页,核对 Bindings 里的 key 与代码里的 routingKey 是否逐字符一致
  2. 命令行核对:rabbitmqctl list_bindings
  3. 想让生产者感知到"没路由出去",开启 mandatory 并注册回调
java 复制代码
package cn.itcast.mq.config;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class RabbitReturnConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * mandatory=true:消息无法路由到任何队列时,退回给生产者而不是静默丢弃。
     * 没有这段配置,routingKey 写错就完全没有任何痕迹。
     */
    @PostConstruct
    public void initReturnCallback() {
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnsCallback(returned -> {
            System.err.println("消息未路由成功:exchange=" + returned.getExchange()
                    + ", routingKey=" + returned.getRoutingKey()
                    + ", replyCode=" + returned.getReplyCode()
                    + ", replyText=" + returned.getReplyText());
        });
    }
}

交换机不存在和绑定不匹配是两回事。 向不存在的交换机发消息会触发 channel 级错误 404 NOT_FOUND,Spring 这边抛异常、连接被关掉;而交换机存在但没有匹配 binding,是静默丢弃,什么都不报。看日志里有没有 NOT_FOUND 可以快速区分这两种情况。

一个队列绑多个 key。 注解式写 key = {"red", "blue"};@Bean 式必须一个 key 一条 Binding,写到一个 with(...) 里编译不过。

Direct 是精确匹配,不是前缀匹配。 把 bindingKey 写成 order.*,只有 routingKey 字面量恰好等于 order.* 才会命中,order.create 匹配不上。要玩通配符得换 TopicExchange,bindingKey 用 order.*(匹配一个词)或 order.#(匹配零到多个词),那是下一篇的内容。

声明是幂等的,但参数冲突会报错。 同名队列用不同参数(比如一边 durable 一边非 durable)重复声明,会抛 PRECONDITION_FAILED - inequivalent arg 'durable'。改队列名或修改参数前,先把旧队列在管理台删掉。

API 速览

API / 注解 说明
rabbitTemplate.convertAndSend(exchange, routingKey, object) 向指定交换机发送消息,第二参数是 routingKey;默认用 SimpleMessageConverter 做 JDK 序列化
@RabbitListener(bindings = @QueueBinding(...)) 消费方法的注解,同时完成交换机、队列、绑定关系的声明
@QueueBinding 绑定关系容器,属性 value(队列)、exchange(交换机)、key(bindingKey 数组)
@Queue(name = "direct.queue1") 声明队列,name 为队列名
@Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT) 声明交换机,type 默认 DIRECT
ExchangeTypes.DIRECT / TOPIC / FANOUT / HEADERS 交换机类型枚举,来自 org.springframework.amqp.core.ExchangeTypes
new DirectExchange("itcast.direct") @Bean 式的交换机声明
BindingBuilder.bind(queue).to(directExchange).with("red") @Bean 式的绑定声明,一次只能绑一个 key
rabbitTemplate.setMandatory(true) + setReturnsCallback(...) 让不可路由的消息退回生产者,避免静默丢消息

官方文档

总结

  • Direct 的核心就一句话:routingKey 与 bindingKey 全等才投递,比 Fanout 多了一个 key 维度,也因此多了一个会写错的地方
  • 所有队列绑同一个 key 时,Direct 等价于 Fanout;这是它的上限,不是它的典型用法
  • 声明优先用注解式 @QueueBinding,一个队列多个 key 写在一个数组里,@Bean 式则是一个 key 一条 Binding
  • key 对不上消息会被静默丢弃,生产环境建议开 mandatory + ReturnsCallback
  • Direct 不支持通配符,order.* 在 Direct 里只是普通字符串,需要模糊匹配就换 TopicExchange
相关推荐
七夜zippoe1 小时前
Agent 中间件架构:钩子链、插件注册与横切治理
ai·中间件·架构·agent·钩子链
Sweet锦1 小时前
jDCS 开源项目:面向工业现场的 Modbus RTU 数据采集基础框架
java·spring boot·物联网·开源
海马1 小时前
Spring Boot 开发知识整理
java·spring boot·后端
xiaolinudao1231 小时前
将多个 Excel 表格中的数据合并到单个表中|6 种实现方案全解析
java·前端·excel
谢亮_vipxieliang1 小时前
Spring Boot 3.x 从零开始——环境搭建与第一个 REST 项目
java·spring boot·后端
Wang's Blog1 小时前
Java 项目部署之 Docker工具快速入门: Docker 架构拆解:镜像、容器、守护进程与 Registry
java·docker·架构
栗子~~2 小时前
SpringCloud Gateway 基于 Nacos 实现动态路由
java·spring cloud·gateway
FYKJ_20102 小时前
django个性化新闻推荐35173-计算机课程设计、毕业设计
java·spring boot·后端·python·架构·django·课程设计
计算机毕设定制辅导-无忧学长2 小时前
《校园食堂在线订餐小程序的设计与实现》
java·vue.js·spring boot·微信小程序·uniapp·校园食堂在线订餐小程序