Spring消息之AMQP.

本文介绍了AMQP协议及其实现RabbitMQ的基本概念,包括Exchange、队列和路由等核心组件。并通过Spring框架展示了如何配置和使用RabbitMQ进行消息发送与接收。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、AMQP 概述

    AMQP(Advanced Message Queuing Protocol),高级消息队列协议。

    简单回忆一下JMS的消息模型,可能会有助于理解AMQP的消息模型。在JMS中,有三个主要的参与者:消息的生产者、消息的消费者以及在生产者和消费者之间传递消息的通道(队列或主题)。在JMS中,通道有助于解耦消息的生产者和消费者,但是这两者依然会与通道相耦合。与之不同的是,AMQP的生产者并不会直接将消息发布到队列中。AMQP在消息的生产者以及传递信息的队列之间引入了一种间接的机制:Exchange。如下图:

    

    哈哈,笔主从今天开始也要学着自己画图了。

    来看看 AMQP 消息的通信过程。首先,生产者把消息发给 Exchange,并带有一个 routing key。其次,Exchange 和 队列 之间 通过 binging 通信,binging 上也有 一个 routing key,AMQP定义了四种不同类型的Exchange,每一种都有不同的路由算法,根据Exchange的算法不同,它可能会使用消息的routing key或参数,并与 binding 的routing key或参数进行对比,来决定是否要将信息放到队列中。然后,消费者从每个队列中取出消息。

    Exchange 的路由算法:

  • Direct:如果 消息的routing key 与 binding的routing key 直接匹配的话,消息将会路由到该队列上;
  • Topic:如果 消息的routing key 与 binding的routing key 符合通配符匹配的话,消息将会路由到该队列上;
  • Headers:如果 消息参数表中的头信息和值 都与 bingding参数表中 相匹配,消息将会路由到该队列上;
  • Fanout:不管消息的routing key和参数表的头信息/值是什么,消息将会路由到所有队列上。

    AMQP 与 JMS 的区别:

1、AMQP为消息定义了线路层(wire-level protocol)的协议,而JMS所定义的是API规范。JMS的API协议能够确保所有的实现都能通过通用的API来使用,但是并不能保证某个JMS实现所发送的消息能够被另外不同的JMS实现所使用。而AMQP的线路层协议规范了消息的格式,消息在生产者和消费者间传送的时候会遵循这个格式。这样AMQP在互相协作方面就要优于JMS——它不仅能跨不同的AMQP实现,还能跨语言和平台。

2、JMS 支持TextMessage、MapMessage 等复杂的消息类型;而AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送),个人认为这也是它能够跨平台和跨语言使用的原因之一。

3、由于Exchange 提供的路由算法,AMQP可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。 

二、Spring 集成 RabbitMQ

    RabbitMQ是一个流行的开源消息代理,它实现了AMQP。Spring AMQP为RabbitMQ提供了支持,包括RabbitMQ连接工厂、模板以及Spring配置命名空间。

    首先,需要安装 RabbitMQ,我们可以在 http://www.rabbitmq.com/download.html 上找到安装指南,具体怎么安装,不是这篇博文的重点,请笔友们自行解决。

    接下来,让我们一起来看看,Spring 和 RabbitMQ 的集成:

1、pom 依赖
    <dependency>
      <groupId>org.springframework.amqp</groupId>
      <artifactId>spring-rabbit</artifactId>
      <version>2.0.3.RELEASE</version>
    </dependency>
2、连接工厂 和 admin
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.userName}"
                               password="${rabbitmq.password}"/>
    
    <rabbit:admin connection-factory="connectionFactory"/>
View Code

admin 元素会自动创建一个RabbitMQ管理组件,它会自动创建队列、Exchange以及binding

3、声明队列、Exchange以及binding

声明队列:

    <rabbit:queue name="queue1"/>
    <rabbit:queue name="queue2"/>
    <rabbit:queue name="queue3"/>
    <rabbit:queue name="queue4"/>
    <rabbit:queue name="queue5"/>
    <rabbit:queue name="queue6"/>
View Code

声明 Exchange 以及 binding:

direct-exchange:

    <rabbit:direct-exchange name="directExchange">
        <rabbit:bindings>
            <rabbit:binding key="queue1" queue="queue1"/>
            <rabbit:binding key="queue2" queue="queue2"/>
            <rabbit:binding key="queue3" queue="queue3"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>
View Code

        如果消息的routing key 与 routing key 直接匹配的话,消息将会路由到该队列上。

topic-exchange

    <rabbit:topic-exchange name="topicExchange">
        <rabbit:bindings>
            <rabbit:binding pattern="routing.*" queue="queue2"/>
            <rabbit:binding pattern="routing.*" queue="queue3"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>
View Code

消息的 routing key 与 binding的routing key 符合通配符匹配的话,消息将会路由到该队列上。

这个通配符匹配特别坑,贼坑!我本来写了个 "routing*" ,自以为能匹配 "routingrrr" 这样的字符,不行!然后我又写了个"routing?"、"rounting.",预想着能不能匹配单个任意字符,不行!

终于我得出了一个结论,只能使用 "*"(匹配 0 个或任意多个)通配符,并且,并且!"*" 前面一定要有 个 "."  ! 太可怕了,不知道我总结的对不对哈!

headers-exchange

    <rabbit:headers-exchange name="headersExchange">
        <rabbit:bindings>
            <rabbit:binding queue="queue4" key="betty" value="rubble"   />
            <rabbit:binding queue="queue5" key="barney" value="rubble"   />
        </rabbit:bindings>
    </rabbit:headers-exchange>
View Code

消息参数表中的头信息和值都与bingding参数表中相匹配,消息将会路由到该队列上。

这个用法比较少用,也比较难用,原因是因为它仅支持 发送 byte[] 的消息类型。

fanout-exchange

    <rabbit:fanout-exchange name="fanoutExchange">
        <rabbit:bindings>
            <rabbit:binding queue="queue5"/>
            <rabbit:binding queue="queue6"/>
        </rabbit:bindings>
    </rabbit:fanout-exchange>
View Code

这个是最简单粗暴的匹配规则,不管消息的routing key和参数表的头信息/值是什么,消息将会路由到所有队列上。

 4、发送和接收消息

    还是Spring的那一套,Spring 为我们提供了一个模板 bean(rabbitTemplate) 来发送和接收消息。其中,像前文提到的 jmsTemplate 那样,rabbitTemplate 也为我们 提供了 convertAndSend() 方法来自动转换和发送消息,提供了receiveAndConvret() 方法来接收和自动转换成对象(消息和对象之间默认的消息转换器是SimpleMessageConverter,它适用于String、Serializable实例以及字节数组)。另外,rabbitTemplate 也照常提供了 send() 和 receive() 方法来发送和接收消息,不过貌似仅支持发送字节数组...

配置 rabbitTemplate:

    <rabbit:template id="rabbitTemplate"
                     connection-factory="connectionFactory"
                     exchange="directExchange"
                     routing-key="queue1"/>
View Code

   

    下面仅演示 通配符路由方式 和 header 路由方式 发送和接收消息。其他具体详细的内容可参考我下面附上的源码:

通配符路由方式:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TopicExchange {

    @Autowired
    private RabbitTemplate rabbitTemplate;


    @Test
    public void convertAndSend(){
        List<String> list = new ArrayList<>();
        list.add("java");
        list.add("python");
        list.add("c++");
        rabbitTemplate.convertAndSend("topicExchange","routing.123", list);
    }


    @Test
    public void receiveAndConvert(){
        List<String> queue2List =(List) rabbitTemplate.receiveAndConvert("queue2");
        printList(queue2List);

        System.out.println("----------------华丽的分隔符-----------------");

        List<String> queue3List =(List) rabbitTemplate.receiveAndConvert("queue3");
        printList(queue3List);

    }


    private <E> void printList(List<E> list){
        if (list != null && list.size() > 0){
            for (Object o : list){
                System.out.println("-----------------"+ o +"---------------");
            }
        }
    }
}
View Code

header 路由方式:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class HeadersExchangeTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void convertAndSend(){
        MessageProperties messageProperties = new MessageProperties();
        messageProperties.setHeader("betty", "rubble");
        messageProperties.setHeader("fred", "flintstone");
        messageProperties.setHeader("barney", "rubble");

        String str = new String("Hello RabbitMQ");
        Message message = new Message(str.getBytes(), messageProperties);
        rabbitTemplate.convertAndSend("headersExchange","",message);
    }

    @Test
    public void receiveAndConvert(){

        Message queue4 = rabbitTemplate.receive("queue4");
        System.out.println("第一个输出:" + new String(queue4.getBody()));
        Message queue5 = rabbitTemplate.receive("queue5");
        System.out.println("第三个输出:" + new String(queue5.getBody()));

    }

}
View Code
5、定义消息驱动的AMQP POJO

    用 receive()和 receiveAndConvert()方法都会立即返回,如果队列中没有等待的消息时,将会得到 null。Spring AMQP提供了消息驱动POJO的支持,也就是相当于一个监听器,监听某些队列,当消息到达指定队列的时候,可以立即调用方法处理该消息。

listener-container 配置:

    <rabbit:listener-container connection-factory="connectionFactory" concurrency="2" prefetch="3" type="direct">
        <rabbit:listener ref="handlerListener" method="handler" queue-names="queue5,queue6"/>
    </rabbit:listener-container>

其中,ref 指定Spring bean 的 id,method 指定 该bean中处理队列中消息的方法,queue-names 指定要监听哪些队列,队列之间用 "," 分隔。

三、结语

    祝大家五一节快乐!

    演示源码下载链接:https://github.com/JMCuixy/SpringMessageRabbitMQ

    参考资料:1、《Spring 实战第四版》

              2、 Spring-amqp文档

<think>我们正在处理SpringAMQPRabbitMQ监听器启动失败的问题,具体错误是`Failedtostartbean'org.springframework.amqp.rabbit.config.internalRabbitListenerEndpointRegistry'`,并伴随`AmqpIllegalStateException:Fatalexceptiononlistenerstartup`。根据引用,常见原因包括队列声明失败(如队列属性冲突、队列名称为null)等。解决方案步骤:1.**检查队列声明属性冲突**(最常见原因):当代码中声明的队列属性(如持久性、TTL、其他参数)与RabbitMQ服务器上已存在的队列不一致时,会抛出此异常。-解决方案:-修改代码中的队列声明,使其与现有队列的属性一致(通过RabbitMQ管理界面或CLI命令查看现有队列属性)。-或者删除现有队列(如果允许),让应用程序重新创建队列。-示例:使用`rabbitmqadmin`删除队列:```bashrabbitmqadmindeletequeuename='heartbeat.alarm.queues.msg'```2.**检查队列名称动态生成**:引用[3]指出,由于配置参数未赋值导致队列名称为`zndz.copy_task.send.queue.null`(即包含`null`),从而引发异常。-解决方案:-确保注入的配置值不为空,使用默认值或进行非空校验。-示例代码:```java@Value("${queue.suffix:}")//使用默认值(空字符串)防止nullprivateStringsuffix;@BeanpublicQueuedynamicQueue(){//如果suffix为null,则使用默认后缀,或者抛出有意义的异常if(suffix==null){suffix="default_suffix";}returnnewQueue("base.name."+suffix);}```3.**设置`missingQueuesFatal=false`**(引用[1]):当监听器启动时,如果队列不存在,默认行为是抛出致命异常并停止监听器容器。通过设置`missingQueuesFatal`为`false`,可以让容器在队列不存在时继续重试(而不是立即失败)。-解决方案:配置监听器容器工厂```java@BeanpublicSimpleRabbitListenerContainerFactorycontainerFactory(ConnectionFactoryconnectionFactory){SimpleRabbitListenerContainerFactoryfactory=newSimpleRabbitListenerContainerFactory();factory.setConnectionFactory(connectionFactory);//设置队列不存在时非致命factory.setMissingQueuesFatal(false);//设置重连间隔,单位毫秒factory.setRecoveryInterval(5000L);//5秒重试一次returnfactory;}```在SpringBoot配置文件中(application.yml)的等效配置:```yamlspring:rabbitmq:listener:simple:missing-queues-fatal:false```4.**检查权限问题**:连接RabbitMQ的用户可能没有足够的权限操作队列(如声明队列)。-解决方案:赋予用户对虚拟主机的完全权限(配置、写、读)。```bashrabbitmqctlset_permissions-p/vhostusername".*"".*"".*"```5.**网络连接问题**:配置连接超时和重试机制,确保在网络波动时能够自动恢复。-在application.yml中配置:```yamlspring:rabbitmq:connection-timeout:2000#连接超时时间(毫秒)template:retry:enabled:truemax-attempts:10initial-interval:2000```6.**队列声明原子性**:使用`Declarables`一次性声明交换器、队列和绑定,避免部分声明失败。```java@BeanpublicDeclarablesdeclarables(){returnnewDeclarables(newDirectExchange("myExchange"),newQueue("myQueue",true,false,false,Collections.singletonMap("x-message-ttl",60000)),newBinding("myBinding",Binding.DestinationType.QUEUE,"myExchange","routingKey",null));}```7.**调试与验证**:-查看现有队列属性:使用CLI命令`rabbitmqctllist_queuesnamedurableauto_deletearguments`。-启用详细日志:在`application.properties`中设置:```propertieslogging.level.org.springframework.amqp=DEBUG```8.**预防措施**:-队列命名规范:避免使用非法字符,确保动态生成的队列名称不会出现`null`。-生产环境配置:设置`spring.rabbitmq.admin.ignore-declaration-exceptions=true`以忽略声明异常(不中断启动)。通过以上步骤,应能解决监听器启动失败的问题。重点检查队列声明冲突和设置`missingQueuesFatal=false`。相关问题:1.SpringAMQP中`missingQueuesFatal`参数的工作原理是什么?[^1]2.如何动态检测RabbitMQ队列属性变化并自动适配?3.RabbitMQ权限管理系统的最佳实践是什么?[^1]</think>### Spring AMQP RabbitMQ 监听器启动失败解决方案 针对 `Failed to start bean 'org.springframework.amqp.rabbit.config.internalRabbitListenerEndpointRegistry'` 和 `AmqpIllegalStateException: Fatal exception on listener startup` 错误,以下是系统性解决方案: --- #### 一、核心问题诊断(基于引用分析) 1. **队列声明冲突**(最常见原因) - 代码声明的队列属性与 RabbitMQ 中已存在队列不一致[^1] - 示例:TTL 参数不匹配、持久化设置冲突 ```java // 错误示例:新声明与现有队列参数冲突 @Bean public Queue conflictQueue() { return new Queue("existing.queue", true, false, false, Map.of("x-message-ttl", 5000)); // 与现有TTL值冲突 } ``` 2. **动态队列名称错误** - 配置参数未赋值导致队列名包含 `null`[^3] ```properties # application.properties 错误配置 queue.suffix= # 空值导致队列名变为 xxx.null ``` 3. **队列不存在或权限不足** - 监听器启动时队列尚未创建或用户无访问权限[^1] --- #### 二、分步解决方案 ##### 1. 解决队列声明冲突(立即生效) ```bash # 查看冲突队列属性 rabbitmqctl list_queues name durable auto_delete arguments # 方案1:删除冲突队列(开发环境) rabbitmqadmin delete queue name='heartbeat.alarm.queues.msg' # 方案2:代码适配现有队列参数 @Bean public Queue fixedQueue() { return QueueBuilder.durable("target.queue") .withArgument("x-message-ttl", 30000) // 与现有参数一致 .build(); } ``` ##### 2. 修复动态队列名称问题 ```java // 确保配置注入安全 @Bean public Queue safeDynamicQueue( @Value("${queue.suffix:default}") String suffix) { // 使用默认值 Assert.hasText(suffix, "队列后缀不能为空"); return new Queue("base.prefix." + suffix); } ``` ##### 3. 配置容错机制(关键) ```yaml # application.yml spring: rabbitmq: listener: simple: missing-queues-fatal: false # 允许队列不存在时继续启动[^1] retry: enabled: true max-attempts: 10 initial-interval: 3s ``` ##### 4. 权限修复 ```bash # 授予完整权限 rabbitmqctl set_permissions -p /vhost username ".*" ".*" ".*" ``` --- #### 三、预防性配置 ##### 队列声明原子化 ```java @Bean public Declarables robustDeclarations() { return new Declarables( new Queue("transaction.queue", true, false, false, Map.of("x-message-ttl", 60000)), new DirectExchange("transaction.exchange"), new Binding("transaction.binding", Binding.DestinationType.QUEUE, "transaction.exchange", "routing.key", null) ); } ``` ##### 启动顺序控制 ```java @Bean public RabbitAdmin rabbitAdmin(ConnectionFactory cf) { RabbitAdmin admin = new RabbitAdmin(cf); admin.setAutoStartup(true); admin.afterPropertiesSet(); // 强制优先初始化 return admin; } ``` --- #### 四、调试与验证 1. **日志诊断** ```properties logging.level.org.springframework.amqp=DEBUG ``` 2. **队列存在性验证** ```java // 在@PostConstruct中验证 @Autowired RabbitTemplate rabbitTemplate; @PostConstruct void validateQueue() { try { rabbitTemplate.execute(ch -> { ch.queueDeclarePassive("target.queue"); return null; }); } catch (Exception e) { log.error("队列不存在: target.queue"); } } ``` 3. **监听器状态监控** ```java // 检查监听器容器状态 @Autowired RabbitListenerEndpointRegistry registry; registry.getListenerContainers().forEach(container -> { System.out.println("Container ID: " + container.getListenerId()); System.out.println("Active: " + container.isRunning()); }); ``` --- #### 五、生产环境最佳实践 ```yaml spring: rabbitmq: host: cluster.rabbitmq.com connection-timeout: 2000 admin: auto-startup: true ignore-declaration-exceptions: true # 忽略声明异常 template: retry: enabled: true max-attempts: 5 listener: direct: missing-queues-fatal: false # 全局容错配置 ``` > **关键点**:`missing-queues-fatal=false` 是解决监听器启动失败的核心配置,允许系统在队列恢复后自动重连[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值