使用Spring AMQP开发消费者应用

前一篇中我们介绍了使用RabbitMQ Java Client访问RabbitMQ的方法。但是使用这种方式访问RabbitMQ,开发者在程序中需要自己管理Connection,Channel对象,Consumer对象的创建,销毁,这样会非常不方便。我们下面介绍使用spring AMQP连接RabbitMQ,进行消息的接收和发送。

Spring AMQP是一个Spring子项目,它提供了访问基于AMQP协议的消息服务器的解决方案。它包含两部分,spring-ampq是基于AMQP协议的消息发送和接收的高层实现,spring-rabbit是基于RabbitMQ的具体实现。这两部分我们下面都会使用到。

 

Spring-AMQP中的基础类/接口

spring-amqp中定义了几个基础类/接口,Message,Exchange,Queue,Binding

 

 

Message

[java] view plain copy
 
  1. public class Message implements Serializable   
  2. {  
  3.   private final MessageProperties messageProperties;  
  4.    
  5.   private final byte[] body;  

 

spring-amqp中的Message类类似于javax的Message类,封装了消息的Properties和消息体。

 

Exchange

spring-amqp定义了Exchange接口

 

 

[java] view plain copy
 
  1. public interface Exchange extends Declarable {  
  2.         //Exchange名称  
  3.     String getName();  
  4.         //Exchange的类型  
  5.     String getType();  
  6.         //Exchange是否持久化  
  7.     boolean isDurable();  
  8.         //Exchange不再被使用时(没有任何绑定的情况下),是否由RabbitMQ自动删除  
  9.     boolean isAutoDelete();  
  10.         //Exchange相关的参数  
  11.     Map<String, Object> getArguments();  


这个接口和RabbitMQ Client中的Exchange类相似。 spring-amqp中的Exchange继承关系如下图所示

 

AbstractExchange类是所有Exchange类的父类,实现Exchange接口的具体方法。 CustomExchange针对用户自定义的Exchange对象。其他四个Exchange类,分别对应四种Exchange。 我们在Spring配置文件中配置Exchange对象时,使用的就是这几种Exchange类。

 

Queue

spring-amqp定义了Queue类,和RabbitMQ Client中的Queue相似,对应RabbitMQ中的消息队列。

 

 

[java] view plain copy
 
  1. public class Queue extends AbstractDeclarable {  
  2.    
  3.     private final String name;  
  4.    
  5.     private final boolean durable;  
  6.    
  7.     private final boolean exclusive;  
  8.    
  9.     private final boolean autoDelete;  
  10.    
  11.     private final java.util.Map<java.lang.String, java.lang.Object> arguments;  
  12.    
  13.         public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete) {  
  14.         this(name, durable, exclusive, autoDelete, null);  
  15.     }   

 

 

Binding

Binding类是对RabbitMQ中Exchange-Exchange以及Exchange-Queue绑定关系的抽象。

 

 

[java] view plain copy
 
  1. public class Binding extends AbstractDeclarable   
  2. {  
  3.    
  4.     public enum DestinationType {  
  5.         QUEUE, EXCHANGE;  
  6.     }  
  7.    
  8.     private final String destination;  
  9.    
  10.     private final String exchange;  
  11.    
  12.     private final String routingKey;  
  13.    
  14.     private final Map<String, Object> arguments;  
  15.    
  16.     private final DestinationType destinationType;  
  17.    
  18.     public Binding(String destination, DestinationType destinationType, String exchange, String routingKey,  
  19.             Map<String, Object> arguments) {  
  20.         this.destination = destination;  
  21.         this.destinationType = destinationType;  
  22.         this.exchange = exchange;  
  23.         this.routingKey = routingKey;  
  24.         this.arguments = arguments;  
  25.     }  


对照RabbitMQ Java Client中Channel接口的queueBind和ExchangeBind方法

 

 

[java] view plain copy
 
  1. Exchange.BindOk exchangeBind(String destination, String source, String routingKey, Map<String, Object> arguments)   
  2.    
  3. Queue.BindOk queueBind(String queue, String exchange, String routingKey, Map<String, Object> arguments)  


我们可以看出Binding类实际是对底层建立的Exchange-Queue和Exchange-Exchange绑定关系的高层抽象记录类,它使用枚举类型DestinationType区分Exchange-Queue和Exchange-Exchange两种绑定。

 

 

Spring AMQP搭建消费者应用

消费者应用程序框架搭建

我们接下来使用spring-amqp搭建一个RabbitMQ的消费者Web应用,我们先创建一个maven webapp应用程序,再添加一个dependency。

 

 

[html] view plain copy
 
  1. <dependency>  
  2.     <groupId>org.springframework.amqp</groupId>  
  3.     <artifactId>spring-rabbit</artifactId>  
  4.     <version>1.6.5.RELEASE</version>  
  5.  </dependency>   

 

 

spring-rabbit库的引入是为了使用它里面的RabbitAdmin类,创建Exchange,Queue和Binding对象,在导入这个库的时候同时引入了 spring-ampq和rabbitmq-client的库,不需要另行导入。

在src/main/resources目录下创建application.properties文件,用于记录RabbitMQ的配置信息。

 

[plain] view plain copy
 
  1. mq.ip=localhost  
  2. mq.port=5672  
  3. mq.userName=rabbitmq_consumer  
  4. mq.password=123456  
  5. mq.virutalHost=test_vhosts  

在src/main/resource目录下创建applicationContext.xml文件:

 

 

[html] view plain copy
 
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2.    
  3. <beans xmlns="http://www.springframework.org/schema/beans"  
  4.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  5.        xmlns:context="http://www.springframework.org/schema/context"  
  6.        xmlns:util="http://www.springframework.org/schema/util"  
  7.        xsi:schemaLocation="  
  8.          http://www.springframework.org/schema/beans  
  9.          http://www.springframework.org/schema/beans/spring-beans-4.0.xsd  
  10.          http://www.springframework.org/schema/util  
  11.          http://www.springframework.org/schema/util/spring-util-4.0.xsd  
  12.          http://www.springframework.org/schema/context  
  13.          http://www.springframework.org/schema/context/spring-context-4.0.xsd" >  
  14.    
  15.     <context:annotation-config/>  
  16.    
  17.     <context:property-placeholder  
  18.             ignore-unresolvable="true" location="classpath*:/application.properties" />  
  19.    
  20.     <!--从RabbitMQ Java Client创建RabbitMQ连接工厂对象-->  
  21.     <bean id="rabbitMQConnectionFactory" class="com.rabbitmq.client.ConnectionFactory">  
  22.         <property name="username" value="${mq.userName}" />  
  23.         <property name="password" value="${mq.password}" />  
  24.         <property name="host" value="${mq.ip}" />  
  25.         <property name="port" value="${mq.port}" />  
  26.         <property name="virtualHost" value="${mq.virutalHost}" />  
  27.     </bean>  
  28.    
  29.     <!--基于RabbitMQ连接工厂对象构建spring-rabbit的连接工厂对象Wrapper-->  
  30.     <bean id="connectionFactory" class="org.springframework.amqp.rabbit.connection.CachingConnectionFactory">  
  31.         <constructor-arg name="rabbitConnectionFactory" ref="rabbitMQConnectionFactory" />  
  32.     </bean>  
  33.    
  34.     <!--构建RabbitAmdin对象,它负责创建Queue/Exchange/Bind对象-->  
  35.     <bean id="rabbitAdmin" class="org.springframework.amqp.rabbit.core.RabbitAdmin">  
  36.         <constructor-arg name="connectionFactory" ref="connectionFactory" />  
  37.         <property name="autoStartup" value="true"></property>  
  38.     </bean>  
  39.    
  40.     <!--构建Rabbit Template对象,用于发送RabbitMQ消息,本程序使用它发送返回消息-->  
  41.     <bean id="rabbitTemplate" class="org.springframework.amqp.rabbit.core.RabbitTemplate">  
  42.         <constructor-arg name="connectionFactory" ref="connectionFactory" />  
  43.     </bean>  
  44.    
  45.     <!--RabbitMQ消息转化器,用于将RabbitMQ消息转换为AMQP消息,我们这里使用基本的Message Converter -->  
  46.     <bean id="serializerMessageConverter"  
  47.           class="org.springframework.amqp.support.converter.SimpleMessageConverter" />  
  48.    
  49.     <!--Message Properties转换器,用于在spring-amqp Message对象中的Message Properties和RabbitMQ的  
  50.      Message Properties对象之间互相转换 -->        
  51.     <bean id="messagePropertiesConverter"  
  52.           class="org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter" />        
  53.    
  54.     <!--定义AMQP Queue-->  
  55.     <bean id="springMessageQueue" class="org.springframework.amqp.core.Queue">  
  56.         <constructor-arg name="name" value="springMessageQueue" />  
  57.         <constructor-arg name="autoDelete" value="false" />  
  58.         <constructor-arg name="durable" value="true" />  
  59.         <constructor-arg name="exclusive" value="false" />  
  60.         <!--定义AMQP Queue创建所需的RabbitAdmin对象-->  
  61.         <property name="adminsThatShouldDeclare" ref="rabbitAdmin" />  
  62.         <!--判断是否需要在连接RabbitMQ后创建Queue-->  
  63.         <property name="shouldDeclare" value="true" />  
  64.     </bean>  
  65.    
  66.     <!--定义AMQP Exchange-->  
  67.     <bean id="springMessageExchange" class="org.springframework.amqp.core.DirectExchange">  
  68.         <constructor-arg name="name" value="springMessageExchange" />  
  69.         <constructor-arg name="durable" value="true" />  
  70.         <constructor-arg name="autoDelete" value="false" />  
  71.         <!--定义AMQP Queue创建所需的RabbitAdmin对象-->  
  72.         <property name="adminsThatShouldDeclare" ref="rabbitAdmin" />  
  73.         <!--判断是否需要在连接RabbitMQ后创建Exchange-->  
  74.         <property name="shouldDeclare" value="true" />  
  75.     </bean>  
  76.    
  77.     <util:map id="emptyMap" map-class="java.util.HashMap" />  
  78.    
  79.     <!--创建Exchange和Queue之间的Bind-->  
  80.     <bean id="springMessageBind" class="org.springframework.amqp.core.Binding">  
  81.         <constructor-arg name="destination" value="springMessageQueue" />  
  82.         <constructor-arg name="destinationType" value="QUEUE" />  
  83.         <constructor-arg name="exchange" value="springMessageExchange" />  
  84.         <constructor-arg name="routingKey" value="springMessage" />  
  85.         <constructor-arg name="arguments" ref="emptyMap" />  
  86.     </bean>  
  87.    
  88.     <!--侦听springMessageQueue队列消息的Message Listener-->  
  89.     <bean id="consumerListener"   
  90.         class="com.qf.rabbitmq.listener.RabbitMQConsumer" />  
  91.    
  92.     <!--创建侦听springMessageQueue队列的Message Listener Container-->  
  93.     <bean id="messageListenerContainer"  
  94.           class="org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer">  
  95.         <property name="messageConverter" ref="serializerMessageConverter" />  
  96.         <property name="connectionFactory" ref="connectionFactory" />  
  97.         <property name="messageListener" ref="consumerListener" />  
  98.         <property name="queues" ref="springMessageQueue" />  
  99.         <!--设置消息确认方式为自动确认-->  
  100.         <property name="acknowledgeMode" value="AUTO" />  
  101.     </bean>  
  102. </beans>  

我们定义了侦听消息队列的Message Listener类RabbitMQConsumer

 

 

[java] view plain copy
 
  1. public class RabbitMQConsumer implements MessageListener  
  2. {  
  3.     @Autowired  
  4.     private MessagePropertiesConverter messagePropertiesConverter;  
  5.    
  6.     @Override  
  7.     public void onMessage(Message message)  
  8.     {  
  9.         try   
  10.         {  
  11.              //spring-amqp Message对象中的Message Properties属性  
  12.              MessageProperties messageProperties = message.getMessageProperties();               
  13.              //使用Message Converter将spring-amqp Message对象中的Message Properties属性  
  14.              //转换为RabbitMQ 的Message Properties对象  
  15.              AMQP.BasicProperties rabbitMQProperties =  
  16.                 messagePropertiesConverter.fromMessageProperties(messageProperties, "UTF-8");               
  17.              System.out.println("The message's correlationId is:" + rabbitMQProperties.getCorrelationId());  
  18.              String messageContent = null;  
  19.              messageContent = new String(message.getBody(),"UTF-8");  
  20.              System.out.println("The message content is:" + messageContent);  
  21.         }   
  22.         catch (UnsupportedEncodingException e) {  
  23.             e.printStackTrace();  
  24.         }  
  25.     }  
  26. }  

上面的Listener类是实现了MessageListener接口的类,当容器接收到消息后,会自动触发onMessage方法。 如果我们想使用普通的POJO类作为Message Listener,需要引入org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter类

 

 

[java] view plain copy
 
  1. public class MessageListenerAdapter extends AbstractAdaptableMessageListener {  
  2.    
  3.   public MessageListenerAdapter(Object delegate) {  
  4.         doSetDelegate(delegate);  
  5.     }  
  6. }  

这里的delegate对象就是我们的POJO对象。 假设我们定义一个Delegate类ConsumerDelegate

 

 

[java] view plain copy
 
  1. public class ConsumerDelegate  
  2. {  
  3.     public void processMessage(Object message)  
  4.     {  
  5.        //这里接收的消息对象仅是消息体,不包含MessageProperties  
  6.        //如果想获取带MessageProperties的消息对象,需要在Adpater中  
  7.        //定义MessageConverter属性。  
  8.        String messageContent = message.toString();  
  9.        System.out.println(messageContent);  
  10.     }  
  11. }  

在applicationContext.xml中定义Adapter对象,引用我们的Delegate对象。

 

 

[html] view plain copy
 
  1. <bean id="consumerDelegate"  
  2.          class="com.qf.rabbitmq.listener.ConsumerDelegate" />  
  3.   
  4. <bean id="consumerListenerAdapter"  
  5.          class="org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter">  
  6.        <property name="delegate" ref="consumerDelegate" />  
  7.        <!--指定delegate处理消息的默认方法 -->  
  8.        <property name="defaultListenerMethod" value="processMessage" />  
  9. </bean>  

最后将Message Listener Container中的Message Listener指向Adapter对象。

 

 

[html] view plain copy
 
  1. <bean id="messageListenerContainer"  
  2.           class="org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer">  
  3.         <property name="messageConverter" ref="serializerMessageConverter" />  
  4.         <property name="connectionFactory" ref="connectionFactory" />  
  5.         <!--设置Message Listener为Adapter对象 -->  
  6.         <property name="messageListener" ref="consumerListenerAdapter"/>  
  7.         <property name="queues" ref="springMessageQueue" />  
  8.         <property name="acknowledgeMode" value="AUTO" />  
  9.  </bean>  

启动Web应用后,我们从启动日志信息可以看出应用连接上了RabbitMQ服务器

 



从RabbitMQ的管理界面(用rabbitmq_consumer用户登录)可以看到springMessageExchange和springMessageQueue已经创建,绑定关系也已经创建。







 

 

Consumer Tag自定义

连接springMessageQueue的消费者Tag是RabbitMQ随机生成的Tag名

 

 

如果我们想设置消费者Tag为指定Tag,我们可以在Message Listener Container中 设置自定义consumer tag strategy。首先我们需要定义一个Consumer Tag Strategy类,它实现了ConsumerTagStrategy接口。

 

[java] view plain copy
 
  1. public class CustomConsumerTagStrategy implements ConsumerTagStrategy  
  2. {  
  3.     @Override  
  4.     public String createConsumerTag(String queue) {  
  5.         String consumerName = "Consumer1";  
  6.         return consumerName + "_" + queue;  
  7.     }  
  8. }  

在applicationContext.xml中设定自定义ConsumerTagStrategy

 

 

[html] view plain copy
 
  1. <bean id="consumerTagStrategy" class="com.qf.rabbitmq.strategy.CustomConsumerTagStrategy" />  
  2.  <!--创建侦听springMessageQueue队列的Message Listener Container-->  
  3.  <bean id="messageListenerContainer"  
  4.           class="org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer">  
  5.      <property name="messageConverter" ref="serializerMessageConverter" />  
  6.      <property name="connectionFactory" ref="connectionFactory" />  
  7.      <property name="messageListener" ref="consumerListener" />  
  8.      <property name="queues" ref="springMessageQueue" />  
  9.      <property name="acknowledgeMode" value="AUTO" />  
  10.      <property name="consumerTagStrategy" ref="consumerTagStrategy" />  
  11.   </bean>  


再次启动Web应用,查看RabbitMQ管理界面,我们可以看到Consumer Tag已经变成“Consumer1_springMessageQueue”,正如我们在CustomConsumerTagStrategy中设定的那样。

 



消费者应用接收消息验证

我们编写了一个生产者程序,向springMessageExchange发送消息。 生产者的主要代码如下,由于Exchange,Queue,Bind已经由消费者Web应用创建,因此生产者程序不再创建。
 
[java] view plain copy
 
  1. ConnectionFactory factory = new ConnectionFactory();  
  2. factory.setHost("localhost");  
  3. factory.setPort(5672);  
  4. factory.setUsername("rabbitmq_producer");  
  5. factory.setPassword("123456");  
  6. factory.setVirtualHost("test_vhosts");  
  7.    
  8. //创建与RabbitMQ服务器的TCP连接  
  9. connection  = factory.newConnection();  
  10. channel = connection.createChannel();  
  11.    
  12. String message = "First Web RabbitMQ Message";  
  13.    
  14. String correlationId = UUID.randomUUID().toString();  
  15. AMQP.BasicProperties props = new AMQP.BasicProperties  
  16.                     .Builder()  
  17.                     .correlationId(correlationId)  
  18.                     .build();  
  19.    
  20. channel.basicPublish("springMessageExchange","springMessage", props, message.getBytes());  

启动消费者Web应用,从控制台输出信息可以看到消费者接收到了生产者发送的消息。

设置消息手动确认模式

到目前为止,消费者端的Web应用对消息的确认是自动确认模式,如果我们想改为手动确认方式,需要做以下两点改动:

1)修改applicationContext.xml文件中Message Listener Container的acknowledgeMode属性的值为MANUAL。

 

[html] view plain copy
 
  1. <bean id="messageListenerContainer"  
  2.           class="org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer">  
  3.     ......  
  4.     <property name="acknowledgeMode" value="MANUAL" />   
  5. </bean>  

 

2)将自定义的Message Listener类从实现org.springframework.amqp.core.MessageListener接口,改为实现 org.springframework.amqp.rabbit.core.ChannelAwareMessageListener接口,实现它的 onMessage(Message,Channel)方法。

 

[java] view plain copy
 
  1. public class RabbitMQConsumer implements ChannelAwareMessageListener  
  2. {  
  3.     ...........  
  4.    
  5.     @Override  
  6.     public void onMessage(Message message, Channel channel)   
  7.     {  
  8.         try   
  9.         {  
  10.              //spring-amqp Message对象中的Message Properties属性  
  11.              MessageProperties messageProperties = message.getMessageProperties();               
  12.              //使用Message Converter将spring-amqp Message对象中的Message Properties属性  
  13.              //转换为RabbitMQ 的Message Properties对象  
  14.              AMQP.BasicProperties rabbitMQProperties =  
  15.                     messagePropertiesConverter.fromMessageProperties(messageProperties, "UTF-8");               
  16.              System.out.println("The message's correlationId is:" + rabbitMQProperties.getCorrelationId());  
  17.              String messageContent = null;  
  18.              messageContent = new String(message.getBody(),"UTF-8");  
  19.              System.out.println("The message content is:" + messageContent);  
  20.              channel.basicAck(messageProperties.getDeliveryTag(), false);  
  21.         }  
  22.         catch (IOException e) {  
  23.             e.printStackTrace();  
  24.         }  
  25.     }  
  26. }  

 

 

onMessage方法的最后一句代码调用Channel.basicAck方法对消息进行手动确认。再次运行生产者和消费者程序后,我们登录管理界面,从管理界面中可以看到springMessageQueue队列中未确认消息条数 (图中Unacked列)为0条,说明消费者接收消息后已经手动确认。

 

RPC模式设置

如果生产者和消费者Web应用之间使用RPC模式,即消费者接收消息后要向指定Exchange/Queue发送返回消息,我们需要修改生产者和消费者的程序。 消费者程序修改点如下:

1)在applicationContext.xml中定义返回消息对应的Exchange,Queue和Bind。

 

[java] view plain copy
 
  1. <!--定义AMQP Reply Queue-->  
  2. <bean id="springReplyMessageQueue" class="org.springframework.amqp.core.Queue">  
  3.         <constructor-arg name="name" value="springReplyMessageQueue" />  
  4.         <constructor-arg name="autoDelete" value="false" />  
  5.         <constructor-arg name="durable" value="true" />  
  6.         <constructor-arg name="exclusive" value="false" />  
  7.         <property name="adminsThatShouldDeclare" ref="rabbitAdmin" />  
  8.         <property name="shouldDeclare" value="true" />  
  9.     </bean>  
  10.    
  11.     <!--定义AMQP Reply Exchange-->  
  12.     <bean id="springReplyMessageExchange" class="org.springframework.amqp.core.DirectExchange">  
  13.         <constructor-arg name="name" value="springReplyMessageExchange" />  
  14.         <constructor-arg name="durable" value="true" />  
  15.         <constructor-arg name="autoDelete" value="false" />  
  16.         <!--定义AMQP Queue创建所需的RabbitAdmin对象-->  
  17.         <property name="adminsThatShouldDeclare" ref="rabbitAdmin" />  
  18.         <property name="shouldDeclare" value="true" />  
  19.     </bean>  
  20.    
  21.     <!--创建Reply Exchange和Reply Queue之间的Bind-->  
  22.     <bean id="springReplyMessageBind" class="org.springframework.amqp.core.Binding">  
  23.         <constructor-arg name="destination" value="springReplyMessageQueue" />  
  24.         <constructor-arg name="destinationType" value="QUEUE" />  
  25.         <constructor-arg name="exchange" value="springReplyMessageExchange" />  
  26.         <constructor-arg name="routingKey" value="springReplyMessage" />  
  27.         <constructor-arg name="arguments" ref="emptyMap" />  
  28. </bean>  
2)修改自定义Message Listener类的onMessage方法,添加发送返回消息的代码

 

 

[java] view plain copy
 
  1. public void onMessage(Message message, Channel channel) {  
  2. try   
  3.   {  
  4.     ......................  
  5.     String replyMessageContent = "Consumer1 have received the message '" + messageContent + "'";  
  6.     channel.basicPublish(rabbitMQProperties.getReplyTo(), "springReplyMessage",  
  7.     rabbitMQProperties, replyMessageContent.getBytes());  
  8.     ......................  

这里发送返回消息直接使用接收消息时创建的Channel通道,不过如果我们的Message Listener类是继承自MessageListener接口,无法获得Channel对象时,我们需要使用RabbitTemplate对象进行返回消息的发送(我们前面已经在applicationContext.xml中定义了这个对象)

 

 

[java] view plain copy
 
  1. public class RabbitMQConsumer implements MessageListener  
  2. {   
  3.    @Autowired  
  4.    private MessagePropertiesConverter messagePropertiesConverter;  
  5.    
  6.    @Autowired  
  7.    private RabbitTemplate rabbitTemplate;  
  8.    
  9.    @Override  
  10.    public void onMessage(Message message)   
  11.    {  
  12.     ..........  
  13.     //创建返回消息的RabbitMQ Message Properties  
  14.     AMQP.BasicProperties replyRabbitMQProps =  
  15.              new AMQP.BasicProperties("text/plain",  
  16.                            "UTF-8",  
  17.                             null,  
  18.                             2,  
  19.                             0, rabbitMQProperties.getCorrelationId(), null, null,  
  20.                             null, null, null, null,  
  21.                             null, null);  
  22.     //创建返回消息的信封头  
  23.     Envelope replyEnvelope =  
  24.              new Envelope(messageProperties.getDeliveryTag(), true,   
  25.                         "springReplyMessageExchange", "springReplyMessage");  
  26.    
  27.     //创建返回消息的spring-amqp Message Properties属性  
  28.     MessageProperties replyMessageProperties =  
  29.              messagePropertiesConverter.toMessageProperties(replyRabbitMQProps,   
  30.                         replyEnvelope,"UTF-8");  
  31.    
  32.     //构建返回消息(spring-amqp消息)  
  33.     Message replyMessage = MessageBuilder.withBody(replyMessageContent.getBytes())  
  34.                                          .andProperties(replyMessageProperties)  
  35.                                          .build();  
  36.    
  37.     rabbitTemplate.send("springReplyMessageExchange","springReplyMessage", replyMessage);   
生产者程序添加对返回消息队列侦听的Consumer

 

 

[java] view plain copy
 
  1. String correlationId = UUID.randomUUID().toString();  
  2. AMQP.BasicProperties props = new AMQP.BasicProperties  
  3.                     .Builder()  
  4.                     .correlationId(correlationId)  
  5.                     .replyTo("springReplyMessageExchange")  
  6.                     .build();  
  7.    
  8. channel.basicPublish("springMessageExchange","springMessage", props, message.getBytes());  
  9.    
  10. QueueingConsumer replyCustomer = new QueueingConsumer(channel);  
  11. channel.basicConsume("springReplyMessageQueue",true,"Producer Reply Consumer", replyCustomer);  
  12.    
  13. String responseMessage = null;  
  14.    
  15. while(true)  
  16. {  
  17.    QueueingConsumer.Delivery delivery = replyCustomer.nextDelivery();  
  18.    String messageCorrelationId = delivery.getProperties().getCorrelationId();  
  19.    if (messageCorrelationId != null && messageCorrelationId.equals(correlationId))   
  20.    {  
  21.        responseMessage = new String(delivery.getBody());  
  22.        System.out.println("The reply message's correlation id is:" + messageCorrelationId);  
  23.        break;  
  24.    }  
  25. }  
  26. if(responseMessage != null)  
  27. {  
  28.   System.out.println("The repsonse message is:'" + responseMessage + "'");  
  29. }  
启动修改后的生产者和消费者程序,我们从生产者的控制台界面可以看到它接收到了消费者发送的返回消息。

 

消费者控制台

生产者控制台

 

消费者并发数设置

到目前为止,消费者Web应用消费消息时,只有一个消费者接收并消费springMessageQueue队列的消息(如下图所示)

 

 

如果发送的消息量比较大时,我们需要增加消费者的数目。

增加消费者数目要修改Message Listener Container的concurrentConsumers和maxConcurrentConsumers属性,concurrentConsumers属性是Message Listener Container创建时创建的消费者数目,maxConcurrentConsumers属性是容器最大的消费者数目,我们下面把这两个属性都设置为5,使Message Listener Container中有5个消费者,同时修改CustomerConsumerTagStrategy类,在Tag中加入线程名,以区分不同的消费者。

 

[html] view plain copy
 
  1. <bean id="messageListenerContainer"  
  2.           class="org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer">  
  3.         ............  
  4.         <property name="consumerTagStrategy" ref="consumerTagStrategy" />  
  5.         <property name="concurrentConsumers" value="5" />  
  6.         <property name="maxConcurrentConsumers" value="5" />  
  7. </bean>  

[java] view plain copy
 
  1. public class CustomConsumerTagStrategy implements ConsumerTagStrategy  
  2. {  
  3.     @Override  
  4.     public String createConsumerTag(String queue) {  
  5.         String consumerName = "Consumer_" + Thread.currentThread().getName();  
  6.         return consumerName + "_" + queue;  
  7.     }  
  8. }  
启动消费者Web应用,从管理页面可以看到连接springMessageQueue队列的有5个消费者。

 

修改生产者程序,循环发送50条消息

 

[java] view plain copy
 
  1. ReplyConsumer replyCustomer = new ReplyConsumer(channel);  
  2. channel.basicConsume("springReplyMessageQueue",true,"Producer Reply Consumer", replyCustomer);  
  3.    
  4. for(int i=0; i<50; i++)  
  5. {  
  6.    String correlationId = UUID.randomUUID().toString();  
  7.    String message = "Web RabbitMQ Message " + i;  
  8.    
  9.    AMQP.BasicProperties props =   
  10.                    new AMQP.BasicProperties  
  11.                         .Builder()  
  12.                         .contentType("text/plain")  
  13.                         .deliveryMode(2)  
  14.                         .correlationId(correlationId)  
  15.                         .replyTo("springReplyMessageExchange")  
  16.                         .build();  
  17.    
  18.    channel.basicPublish("springMessageExchange","springMessage", props, message.getBytes());  
  19. }  
在修改的生产者代码中,我们将Consumer代码抽出,定义了ReplyCustomer类

 

 

[java] view plain copy
 
  1. public class ReplyConsumer extends DefaultConsumer  
  2. {  
  3.     public ReplyConsumer(Channel channel)  
  4.     {  
  5.         super(channel);  
  6.     }  
  7.    
  8.     @Override  
  9.     public void handleDelivery(String consumerTag,  
  10.                                          Envelope envelope,  
  11.                                          AMQP.BasicProperties properties,  
  12.                                          byte[] body)  
  13.             throws IOException  
  14.     {  
  15.         String consumerName = properties.getAppId();  
  16.         String replyMessageContent = new String(body, "UTF-8");  
  17.         System.out.println("The reply message's sender is:" + consumerName);  
  18.         System.out.println("The reply message is '" + replyMessageContent + "'");  
  19.     }  
  20. }  
修改消费者的Message Listener消息,将Consumer Tag作为参数,放在返回消息的Properties中,返回给生产者。
[java] view plain copy
 
  1. public void onMessage(Message message, Channel channel)  
  2. {  
  3.  try   
  4.  {  
  5.    String consumerTag = messageProperties.getConsumerTag();  
  6.    String replyMessageContent = consumerTag + " have received the message '" + messageContent + "'";  
  7.    
  8.    AMQP.BasicProperties replyRabbitMQProps =  
  9.                     new AMQP.BasicProperties("text/plain",  
  10.                             "UTF-8",  
  11.                             null,  
  12.                             2,  
  13.                             0, rabbitMQProperties.getCorrelationId(), null, null,  
  14.                             null, null, null, null,  
  15.                             consumerTag, null);   
  16.    .............      
修改消费者的CustomConsumerTagStrategy类,用“Consumer” + “_” + 线程名作为Consumer Tag。

 

 

[java] view plain copy
 
  1. public class CustomConsumerTagStrategy implements ConsumerTagStrategy  
  2. {  
  3.     @Override  
  4.     public String createConsumerTag(String queue) {  
  5.         String consumerName = "Consumer_" + Thread.currentThread().getName();  
  6.         return consumerName;  
  7.     }  
  8. }  
修改完成后,启动生产者和消费者程序,通过查看生产者的控制台输出,我们可以看到多个消费者接收了生产者发送的消息,发送了返回消息给生产者。

 



消费者消息预取数设置

上述的消费者Web应用中,每个消费者每次从队列中获取1条消息,如果我们想让每个消费者一次性从消息队列获取多条消息,需要修改Message Listener Container的prefetchCount属性,这样可以提高RabbitMQ的消息处理吞吐量。

 

 

[html] view plain copy
 
  1. <span style="font-size:10px;"><bean id="messageListenerContainer"  
  2.           class="org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer">  
  3.           <property name="prefetchCount" value="5" />  
  4. </bean></span>  
这个属性值最终被设置为底层Rabbit Client的Channel接口的basicQos方法参数

 

 

[java] view plain copy
 
  1. /** 
  2.      * Request a specific prefetchCount "quality of service" settings 
  3.      * for this channel. 
  4.      * 
  5.      * @see #basicQos(int, int, boolean) 
  6.      * @param prefetchCount maximum number of messages that the server 
  7.      * will deliver, 0 if unlimited 
  8.      * @throws java.io.IOException if an error is encountered 
  9. */  
  10. void basicQos(int prefetchCount) throws IOException  

 

这个方法设置从Channel上一次性可以读取多少条消息,我们在Container设置的PrefetchCount值为5,表示从一个消费者Channel上,一次性可以与预读取5条消息,按我们上面设置的5个消费者,5个消费者Channel计算,一次性可以预读取25条消息。为了证实这一点,我们修改消费者的代码,延长它处理一条消息的时间。

需要说明的是,对于每个消费者而言,只有一条预取的消息被接收且确认后,消费者才会再从消息队列中读取消息,并不是消费者在消息没有确认完成前,每次都从队列里预读取prefetchCount条消息。

 

[java] view plain copy
 
  1. public void onMessage(Message message, Channel channel) {  
  2. try   
  3.     {  
  4.      ...........  
  5.      String messageContent = null;  
  6.      messageContent = new String(message.getBody(),"UTF-8");  
  7.      String consumerTag = messageProperties.getConsumerTag();  
  8.      String replyMessageContent = consumerTag + " have received the message '" + messageContent + "'";  
  9.    
  10.      Thread.sleep(60000);  
  11.    
  12.      ...........  
  13.      rabbitTemplate.send("springReplyMessageExchange","springReplyMessage", replyMessage);  
  14.      channel.basicAck(messageProperties.getDeliveryTag(), false);   

 

我们在onMessage方法中添加Thread.sleep(60000),使得处理一条消息时间时间大于1分钟,便于查看消息预取的效果,而且使用手动确认方式。

生产者程序改为一次性发送200条消息。

启动生产者程序,发送200条消息,我们可以看到springMessageQueue队列里有200条处于Ready状态的消息

启动消费者程序,我们可以看到springMessageQueue队列里有25条消息被预取了,Ready状态的消息从200条变成了175条,而未确认状态的消息数(Unacked列)变成了25条,即25条被预取,但是没有被确认的消息。

过了一段时间,等5个消费者确认了5条消息后,又从消息队列预读取了5条消息,Ready状态的消息数变成170条,这时的消息队列的消息数如下图所示:

 

未确认的消息数仍然是25条,但是总的消息数变成了195条,表示已经有5条消息被处理且确认了。

随着消息逐渐被处理,确认,消费者会逐渐从消息队列预取新的消息,直到所有的消息都被处理和确认完成。

rabbit标签使用

上面的消费者Web应用使用了Spring传统的beans元素定义,spring-rabbit提供了rabbit namespace,我们可以在applicationContext.xml中使用rabbit:xxx形式的元素标签,简化我们的xml配置。 我们首先在applicationContext.xml的namespace定义中添加rabbit namespace定义:
[html] view plain copy
 
  1. <beans xmlns="http://www.springframework.org/schema/beans"  
  2.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  3.        xmlns:context="http://www.springframework.org/schema/context"  
  4.        xmlns:util="http://www.springframework.org/schema/util"  
  5.        xmlns:rabbit="http://www.springframework.org/schema/rabbit"  
  6.        xsi:schemaLocation="  
  7.          http://www.springframework.org/schema/beans  
  8.          http://www.springframework.org/schema/beans/spring-beans-4.0.xsd  
  9.          http://www.springframework.org/schema/util  
  10.          http://www.springframework.org/schema/util/spring-util-4.0.xsd  
  11.          http://www.springframework.org/schema/rabbit  
  12.          http://www.springframework.org/schema/rabbit/spring-rabbit-1.6.xsd  
  13.          http://www.springframework.org/schema/context  
  14.          http://www.springframework.org/schema/context/spring-context-4.0.xsd" >  
RabbitMQ Client ConnectionFactory的bean定义不需要修改,我们修改CachingConnectionFactory bean对象的定义
[html] view plain copy
 
  1. <pre name="code" class="html"><span style="font-size:10px;"><rabbit:connection-factory id ="connectionFactory" connection-factory="rabbitMQConnectionFactory" /></span></pre>  
  2. <pre></pre>  
  3. <span style="color:rgb(51,51,51); font-family:Arial,sans-serif"><span style="font-size:10px">修改RabbitAdmin bean对象定义,使用rabbit:admin标签</span></span>  
  4. <pre></pre>  
  5. <pre></pre>  
  6. <pre></pre>  
[html] view plain copy
 
  1. <rabbit:admin id="rabbitAdmin" connection-factory="connectionFactory" auto-startup="true"/>  
修改rabbitTemplate定义,使用rabbit:template标签
[html] view plain copy
 
  1. <rabbit:template connection-factory="connectionFactory" />  

MessageConverter和MessageProperties对象没有对应的rabbit标签,仍然使用bean标签。
修改Queue,Exchange和Bind定义,分别使用rabbit:queue,rabbit:exchange标签,Bind的内容放到了Exchange bean定义内部。

[html] view plain copy
 
  1. <rabbit:queue id="springMessageQueue" name="springMessageQueue" auto-delete="false"  
  2.            durable="true" exclusive="false" auto-declare="false" declared-by="rabbitAdmin" />  
  3.    
  4. <rabbit:direct-exchange id="springMessageExchange" name="springMessageExchange" durable="true"  
  5.                             auto-declare="false" auto-delete="false" declared-by="rabbitAdmin">  
  6.     <rabbit:bindings>  
  7.         <rabbit:binding queue="springMessageQueue" key="springMessage"></rabbit:binding>  
  8.     </rabbit:bindings>  
  9. </rabbit:direct-exchange>  
最后使用rabbit:listener-container修改Message Listener Container bean对象。
[html] view plain copy
 
  1. <rabbit:listener-container message-converter="serializerMessageConverter"  
  2.                                connection-factory="connectionFactory"  
  3.                                acknowledge="manual"  
  4.                                consumer-tag-strategy="consumerTagStrategy"  
  5.                                concurrency="5"  
  6.                                max-concurrency="5"  
  7.                                prefetch="5">  
  8.         <rabbit:listener ref="consumerListener" queues="springMessageQueue"/>  
  9. </rabbit:listener-container>  
如果上面没有创建queue的bean对象,这里的rabbit:listener中的queues属性也可以改成queueNames属性
[html] view plain copy
 
  1. <rabbit:listener ref="consumerListener" queue-names="springMessageQueue"/>  

这里如果Listener关联多个队列,设置queues属性或者queue-names属性时可以用逗号进行分割,例如:

[html] view plain copy
 
  1. <pre name="code" class="html" style="color: rgb(51, 51, 51);"><rabbit:listener ref="consumerListener" queue-names="messageQueue1,messageQueue2"/></pre>  
  2. <pre></pre>  
  3. <pre></pre>  
  4. <pre></pre>  
  5. <pre></pre>  

使用rabbit标签虽然可以简化RabbitMQ相关对象的bean定义,但是它也有局限性:

1)标签对应的bean对象类型是固定的,例如rabbit:listener-container标签对应的Listener Container是SimpleMessageListenerContainer类,如果我们想使用其他MessageListenerContainer类或者自定义Message Listener Container类,就不能使用rabbit标签。
 

2)有的标签无法设置id和name属性,这样一旦有多个同类型的bean对象定义时,就不能使用rabbit标签。

 

 

RabbitMQ的Channel和Connection缓存

spring-rabbit中的CachingConnectionFactory类提供了Connection和Channel级别的缓存,如果我们没有做任何设置,默认的缓存模式是Channel模式,Channel缓存默认最大数是25,所有的Channel复用一个Connection。我们在Message Listener Container中设置并发数为5,启动消费者应用后,我们从管理界面可以看到一个消费者Connection,5个Channel。
 
 
 
 
重新启动消费者应用后,我们可以看到有30个Channel被创建,但是只能有25个Channel被缓存,其他5个Channel只是临时存在于内存中,一旦不被使用,会被自动销毁,不会被回收到Channel缓存池中被复用。
 
 
如果我们想修改Channel模式下的最大缓存数的话,我们可以进行如下修改:
 
[html] view plain copy
 
  1. <rabbit:connection-factory id ="connectionFactory"  
  2.                                 connection-factory="rabbitMQConnectionFactory"  
  3.                                 cache-mode="CHANNEL"  
  4.                                 channel-cache-size="30" />  
 
我们也可以设置缓存模式为Connection模式,设置最大连接缓存数为10
[html] view plain copy
 
  1. <rabbit:connection-factory id ="connectionFactory"  
  2.                                 connection-factory="rabbitMQConnectionFactory"  
  3.                                 cache-mode="CONNECTION"  
  4.                                 connection-cache-size="10" />  
如果我们的Message Listener Container的消费者并发数小于最大缓存数,例如为5,管理界面中只显示有5个Connection,每个Connection上一条Channel。
 

如果消费者并发数大于最大缓存数,例如并发数为20,会出现与并发数对应的连接数,但是只有5个Connection能够被缓存,其他Connection,如果不再被使用,会被RabbitMQ自动销毁。
 
 
我们还可以设置Connection的上限,使用CachingConnectionFactory的connectionLimit属性
 
[java] view plain copy
 
  1. public class CachingConnectionFactory extends AbstractConnectionFactory  
  2. {  
  3.   ................  
  4.   private volatile int connectionLimit = Integer.MAX_VALUE;  

这个属性默认值是Integer.MAX_VALUE,可以理解为无上限,我们可以在applicationContext.xml中设置这个值为10。
[html] view plain copy
 
  1. <rabbit:connection-factory id ="connectionFactory"  
  2.                                 connection-factory="rabbitMQConnectionFactory"  
  3.                                 connection-limit="10"  
  4.                                 cache-mode="CONNECTION"  
  5.                                 connection-cache-size="10" />  
此时如果Message Listener Container的Message Listener总并发数大于这个上限,会抛出无法获取连接的异常。
[html] view plain copy
 
  1. <span style="font-size:10px;"><rabbit:listener-container   
  2.                                .............  
  3.                                concurrency="4"  
  4.                                max-concurrency="4">  
  5.         <rabbit:listener ref="Listener1" queues="messageQueue1"/>  
  6.     <rabbit:listener ref="Listener2" queues="messageQueue2"/>  
  7.     <rabbit:listener ref="Listener3" queues="messageQueue3"/>  
  8. </rabbit:listener-container></span>  

例如上面的Container中,一共定义了三个Listener,每个Listener的并发数是4,总的并发数为12,超过了上线10,因此抛出以下异常:

 

[java] view plain copy
 
  1. 一月 03, 2017 10:15:28 上午 org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer redeclareElementsIfNecessary  
  2. 严重: Failed to check/redeclare auto-delete queue(s).  
  3. org.springframework.amqp.AmqpTimeoutException: Timed out attempting to get a connection  
  4.     at org.springframework.amqp.rabbit.connection.CachingConnectionFactory.createConnection(CachingConnectionFactory.java:575)  
  5.     ..............  
此时,消费者应用与RabbitMQ服务器之间的Connection数只有上限数10条。

 

Spring AMQP的重连机制

我们在使用1中介绍了RabbitMQ Java Client提供的重连机制,Spring AMQP也提供了重连机制。我们可以使用Rabbit Java Client的重连设置,我们修改applicationContext.xml中“rabbitMQConnectionFactory”的重连属性设置。

 

 

[html] view plain copy
 
  1. <bean id="rabbitMQConnectionFactory" class="com.rabbitmq.client.ConnectionFactory">  
  2.         ...................  
  3.         <property name="automaticRecoveryEnabled" value="true" />  
  4.         <property name="topologyRecoveryEnabled" value="true" />  
  5.         <property name="networkRecoveryInterval" value="60000" />  
  6. </bean>  

我们启动消费者应用程序,打开管理页面,可以看到消费者应用创建了5个Connection,每个Connection下分别创建了一个Channel,对应5个Consumer。

 

 

我们停止RabbitMQ服务器,可以看到消费者控制台输出连接异常信息,不停试图恢复Consumer。


重新启动RabbitMQ服务器,从日志信息可以看出连接被重置,消费者被恢复。

 

登录管理界面,可以看到原先的5条Channel已经被恢复,但是本地连接端口号与之前的Channel不再一致。

点开一条Channel进去,可以看到连接Channel的Consumer Tag与最初的Consumer Tag也不一致,这可能是因为我们使用了自定义ConsumerTagStrategy,使用线程名为Tag名的原因。

我们也可以禁用RabbitMQ Java Client的重连设置,设置automaticRecoveryEnabled和topologyRecoveryEnabled属性为false。

 

[html] view plain copy
 
  1. <span style="font-size:10px;"><bean id="rabbitMQConnectionFactory" class="com.rabbitmq.client.ConnectionFactory">  
  2.    <property name="automaticRecoveryEnabled" value="false" />  
  3.    <property name="topologyRecoveryEnabled" value="false" />  
  4. </bean></span>  
我们再启动消费者应用,可以看到初始有5个Connection,5个Channel,每个Channel对应一个Connection。


 

 

当我们重启RabbitMQ服务器后,发现只有4个Connection恢复,5个Channel被恢复,但是有两个Channel复用同一个Connection,这一点与 使用RabbitMQ Java Client的重连机制时有所不同。

当执行RabbitMQ重连时,Message Listener Container也会对Consumer进行重新恢复,它的恢复间隔是由recoveryBackOff属性决定的。

 

[java] view plain copy
 
  1. public class SimpleMessageListenerContainer extends AbstractMessageListenerContainer  
  2.         implements ApplicationEventPublisherAware {  
  3.       ..........  
  4.       private BackOff recoveryBackOff = new FixedBackOff(DEFAULT_RECOVERY_INTERVAL, FixedBackOff.UNLIMITED_ATTEMPTS);  

 

 

SimpleMessageListenerContainer类的recoveryBackOff属性对象有两个属性,一个是恢复间隔,默认值是DEFAULT_RECOVERY_INTERVAL常量(5000ms,即每5秒试图进行一次恢复),还有一个尝试恢复次数,默认值是FixedBackOff.UNLIMITED_ATTEMPTS(Long.MaxValue,可以认为是无限次尝试)。我们可以根据需要 设置自己的recoveryBackOff属性,例如下面我们把恢复间隔设置为60000ms,尝试次数设置为100次。

 

[html] view plain copy
 
  1. <bean id="backOff" class="org.springframework.util.backoff.FixedBackOff">  
  2.         <constructor-arg name="interval" value="60000" />  
  3.         <constructor-arg name="maxAttempts" value="100" />  
  4. </bean>  
  5. <rabbit:listener-container message-converter="serializerMessageConverter"  
  6.                                ..........  
  7.                                recovery-back-off="backOff">   
  8.     <rabbit:listener ref="consumerListener" queues="springMessageQueue"/>  
  9. </rabbit:listener-container>  

 

修改后启动消费者应用,停掉RabbitMQ服务器,我们从异常日志可以看出Message Listener Container的重试间隔变成了1分钟,而不是默认的5000ms。(为了便于查看重试间隔起见,我们将Container的并发数调整为1)


 



posted on 2014-01-14 10:09  duanxz  阅读(6330)  评论(1编辑  收藏  举报