概述
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」,交换机都不会报错,消息直接消失。排查顺序:
- 管理台打开交换机详情页,核对 Bindings 里的 key 与代码里的 routingKey 是否逐字符一致
- 命令行核对:
rabbitmqctl list_bindings - 想让生产者感知到"没路由出去",开启 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(...) |
让不可路由的消息退回生产者,避免静默丢消息 |
官方文档
- RabbitMQ 官方教程四:Routing(Direct 交换机)
- RabbitMQ 官方文档:Exchange 与 Binding
- Spring AMQP 参考文档
- Spring AMQP
@RabbitListener注解声明
总结
- Direct 的核心就一句话:
routingKey与bindingKey全等才投递,比 Fanout 多了一个 key 维度,也因此多了一个会写错的地方 - 所有队列绑同一个 key 时,Direct 等价于 Fanout;这是它的上限,不是它的典型用法
- 声明优先用注解式
@QueueBinding,一个队列多个 key 写在一个数组里,@Bean式则是一个 key 一条 Binding - key 对不上消息会被静默丢弃,生产环境建议开
mandatory+ReturnsCallback - Direct 不支持通配符,
order.*在 Direct 里只是普通字符串,需要模糊匹配就换 TopicExchange