RabbitMQ的Java應用(3) -- 使用spring-boot-starter-amqp開發生產者應用

上一篇我們介紹瞭如何使用Spring AMQP和RabbitMQ結合,開發消費者應用程序,使用的是Xml配置的Spring框架。

本篇我們仍然使用Spring AMQP開發生產者應用,不過我們使用零 XML配置的Spring Boot環境進行開發,使用的庫是spring-boot-starter-amqp庫。

使用Spring-boot-starter-amqp搭建框架

我們使用Spring Boot 1.4.3 RELEASE版本,pom.xml文件內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.example</groupId>
    <artifactId>SpringMQProducer</artifactId>
    <version>0.1.0</version>
    <packaging>jar</packaging>
 
    <name>SpringMQProducer</name>
    <description>The MQ producer in spring boot environment</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.4.3.RELEASE</version>
        <relativePath/>
    </parent>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

需要注意的是,當使用spring-boot-configuration-processor庫時會默認打開RabbitAutoConfiguration,它會自動創建一個 CachingConnectionFactory對象,一個RabbitTemplate對象,一個AmqpAdmin對象以及一個RabbitMessageTemplate對象。

@Configuration
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {
   	@Configuration
	@ConditionalOnMissingBean(ConnectionFactory.class)
	protected static class RabbitConnectionFactoryCreator {
		@Bean
		public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties config)
                {
                 ........
 
        @Configuration
	@Import(RabbitConnectionFactoryCreator.class)
	protected static class RabbitTemplateConfiguration {
           @Bean
	   @ConditionalOnSingleCandidate(ConnectionFactory.class)
	   @ConditionalOnMissingBean(RabbitTemplate.class)
	   public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
           ........
 
        @Bean
	@ConditionalOnSingleCandidate(ConnectionFactory.class)
	@ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true)
	@ConditionalOnMissingBean(AmqpAdmin.class)
	public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
		return new RabbitAdmin(connectionFactory);
	}
 
        .......
        @Configuration
	@ConditionalOnClass(RabbitMessagingTemplate.class)
	@ConditionalOnMissingBean(RabbitMessagingTemplate.class)
	@Import(RabbitTemplateConfiguration.class)
	protected static class MessagingTemplateConfiguration {
 
	@Bean
	@ConditionalOnSingleCandidate(RabbitTemplate.class)
	public RabbitMessagingTemplate rabbitMessagingTemplate(
			RabbitTemplate rabbitTemplate) {
		return new RabbitMessagingTemplate(rabbitTemplate);
	}

如果我們想定製自己的ConnectionFactory,RabbitTemplate等對象,建議在Configuration中將其禁用:





@Configuration
@ComponentScan("com.qf.rabbitmq")
@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class)
@SpringBootApplication
public class SpringMqProducerApplication {
 
	public static void main(String[] args) {
		SpringApplication.run(SpringMqProducerApplication.class, args);
	}
}

在src/main/resources目錄下創建application.properties文件,添加rabbitmq相關設置,以spring.rabbitmq爲前綴,這樣它會自動映射到RabbitProperties配置類
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=rabbitmq_producer
spring.rabbitmq.password=123456
spring.rabbitmq.virtualHost=test_vhosts

添加RabbitConfig類,用於創建自定義ConnectionFactory,RabbitAdmin,RabbitTemplate等。 這個類頭部如下所示
@Configuration
@EnableConfigurationProperties(RabbitProperties.class)
public class RabbitConfig
{
    @Autowired
    private RabbitProperties rabbitProperties;

這裏用到了org.springframework.boot.autoconfigure.amqp.RabbitProperties類,記錄application.properties中的RabbitMQ連接設置。

再創建ConnectionFactory:

@Bean("connectionFactory")
 public ConnectionFactory getConnectionFactory() {
        com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory =
                new com.rabbitmq.client.ConnectionFactory();
        rabbitConnectionFactory.setHost(rabbitProperties.getHost());
        rabbitConnectionFactory.setPort(rabbitProperties.getPort());
        rabbitConnectionFactory.setUsername(rabbitProperties.getUsername());
        rabbitConnectionFactory.setPassword(rabbitProperties.getPassword());
        rabbitConnectionFactory.setVirtualHost(rabbitProperties.getVirtualHost());
 
        ConnectionFactory connectionFactory = new CachingConnectionFactory(rabbitConnectionFactory);
        return connectionFactory;
 }
這裏可以像消費者應用裏提到的那樣,設置CacheMode和最大緩存數,我們這裏暫不設置。 再創建RabbitAdmin對象

@Bean(name="rabbitAdmin")
public RabbitAdmin getRabbitAdmin()
{
    RabbitAdmin rabbitAdmin = new RabbitAdmin(getConnectionFactory());
    rabbitAdmin.setAutoStartup(true);
    return rabbitAdmin;
}
定義MessageConverter和MessagePropertiesConverter對象

@Bean(name="serializerMessageConverter")
public MessageConverter getMessageConverter(){
        return new SimpleMessageConverter();
}
 
@Bean(name="messagePropertiesConverter")
public MessagePropertiesConverter getMessagePropertiesConverter()
{
    return new DefaultMessagePropertiesConverter();
}
定義發送消息所用的RabbitTemplate對象,由於我們的場景要求發送之後立即從消費者處獲得返回消息,因此我們在RabbitTemplate對象中設置了ReplyAddress,而且在下面的MessageListenerContainer中將這個對象作爲Listener設置到Container中。

@Bean(name="rabbitTemplate")
public RabbitTemplate getRabbitTemplate()
{
   RabbitTemplate rabbitTemplate = new RabbitTemplate(getConnectionFactory());
   rabbitTemplate.setUseTemporaryReplyQueues(false);
   rabbitTemplate.setMessageConverter(getMessageConverter());
   rabbitTemplate.setMessagePropertiesConverter(getMessagePropertiesConverter());
   rabbitTemplate.setReplyAddress(AppConstants.REPLY_QUEUE_NAME);
   rabbitTemplate.setReceiveTimeout(60000);
   return rabbitTemplate;
}
使用RabbitAdmin對象定義發送消息Exchange/Queue/Binding,返回消息Exchange/Queue/Binding對象,如果它們在RabbitMQ中已經存在,下面的定義代碼可以省略.這裏的Exchange/Queue名稱和前面的消費者應用使用的相同。

@Bean(name="springMessageQueue")    
public Queue createQueue(@Qualifier("rabbitAdmin")RabbitAdmin rabbitAdmin)
{
    Queue sendQueue = new Queue(AppConstants.SEND_QUEUE_NAME,true,false,false);
    rabbitAdmin.declareQueue(sendQueue);
    return sendQueue;
}
 
@Bean(name="springMessageExchange")
public Exchange createExchange(@Qualifier("rabbitAdmin")RabbitAdmin rabbitAdmin)
{
    DirectExchange sendExchange = new DirectExchange(AppConstants.SEND_EXCHANGE_NAME,true,false);
    rabbitAdmin.declareExchange(sendExchange);
    return sendExchange;
}
 
@Bean(name="springMessageBinding")
public Binding createMessageBinding(@Qualifier("rabbitAdmin")RabbitAdmin rabbitAdmin)
{
    Map<String,Object> arguments = new HashMap<String,Object>();
    Binding sendMessageBinding =
            new Binding(AppConstants.SEND_QUEUE_NAME, Binding.DestinationType.QUEUE,
                        AppConstants.SEND_EXCHANGE_NAME, AppConstants.SEND_MESSAGE_KEY, arguments);
    rabbitAdmin.declareBinding(sendMessageBinding);
    return sendMessageBinding;
}
 
@Bean(name="springReplyMessageQueue")
public Queue createReplyQueue(@Qualifier("rabbitAdmin")RabbitAdmin rabbitAdmin)
{
    Queue replyQueue = new Queue(AppConstants.REPLY_QUEUE_NAME,true,false,false);
    rabbitAdmin.declareQueue(replyQueue);
    return replyQueue;
}
 
@Bean(name="springReplyMessageExchange")
public Exchange createReplyExchange(@Qualifier("rabbitAdmin")RabbitAdmin rabbitAdmin)
{
    DirectExchange replyExchange = new DirectExchange(AppConstants.REPLY_EXCHANGE_NAME,true,false);
    rabbitAdmin.declareExchange(replyExchange);
    return replyExchange;
}
 
@Bean(name="springReplyMessageBinding")
public Binding createReplyMessageBinding(@Qualifier("rabbitAdmin")RabbitAdmin rabbitAdmin)
{
    Map<String,Object> arguments = new HashMap<String,Object>();
    Binding replyMessageBinding =
            new Binding(AppConstants.REPLY_QUEUE_NAME, Binding.DestinationType.QUEUE,
                        AppConstants.REPLY_EXCHANGE_NAME, AppConstants.REPLY_MESSAGE_KEY, arguments);
    rabbitAdmin.declareBinding(replyMessageBinding);
    return replyMessageBinding;
}
最後定義接收返回消息的Message Listener Container,這裏的Listener屬性設置的是上面創建的RabbitTemplate對象。

@Bean(name="replyMessageListenerContainer")
public SimpleMessageListenerContainer createReplyListenerContainer() {
     SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer();
     listenerContainer.setConnectionFactory(getConnectionFactory());
     listenerContainer.setQueueNames(AppConstants.REPLY_QUEUE_NAME);
     listenerContainer.setMessageConverter(getMessageConverter());
     listenerContainer.setMessageListener(getRabbitTemplate());
     listenerContainer.setRabbitAdmin(getRabbitAdmin());
     listenerContainer.setAcknowledgeMode(AcknowledgeMode.AUTO);
     return listenerContainer;
}
我們還定義了一個ThreadPoolExecutor對象,用於RabbitTemplate異步執行,發送和接收消息使用。
@Bean(name="threadExecutor")
public ThreadPoolTaskExecutor createThreadPoolTaskExecutor()
{
    ThreadPoolTaskExecutor threadPoolTaskExecutor =
            new ThreadPoolTaskExecutor();
    threadPoolTaskExecutor.setCorePoolSize(5);
    threadPoolTaskExecutor.setMaxPoolSize(10);
    threadPoolTaskExecutor.setQueueCapacity(200);
    threadPoolTaskExecutor.setKeepAliveSeconds(20000);
    return threadPoolTaskExecutor;
}

我們的生產者-消費者場景是系列1中提到的RPC方式計算階乘,使用Rest接口調用生產者應用,生產者應用發送消息給消費者應用,計算出階乘結果返回給生產者。 
爲了實現這個場景,我們先在生產者程序中定義用於發送請求消息和接收返回消息的服務接口SendMessageService和它的實現類SendMessageServiceImpl類。
public interface SendMessageService
{
    String sendMessage(String message);
}
 
@Service("sendMessageService")
public class SendMessageServiceImpl implements SendMessageService
{
    @Autowired
    private ThreadPoolTaskExecutor executor;
 
    public String sendMessage(String message)
    {
        CompletableFuture<String> resultCompletableFuture =
                CompletableFuture.supplyAsync(new MQSenderSupplier(message), executor);
        try
        {
            String result = resultCompletableFuture.get();
            return result;
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
        catch (ExecutionException e)
        {
            e.printStackTrace();
        }
        return null;
    }
}
在實現類中,我們將發送請求消息和接收返回消息交給一個Supplier對象(JDK 1.8引入)在後臺線程池中異步執行,我們自定義了MQSenderSupplier類,實現了發送請求和接收返回消息的操作。
public class  MQSenderSupplier implements Supplier<String> {
 
    private String message;
 
    public MQSenderSupplier(String message)
    {
        this.message = message;
    }
 
    public String get()
    {
        Date sendTime = new Date();
        String correlationId = UUID.randomUUID().toString();
 
        MessagePropertiesConverter messagePropertiesConverter =
                (MessagePropertiesConverter) ApplicationContextUtil.getBean("messagePropertiesConverter");
 
        RabbitTemplate rabbitTemplate =
                 (RabbitTemplate)ApplicationContextUtil.getBean("rabbitTemplate");
 
        AMQP.BasicProperties props =
                new AMQP.BasicProperties("text/plain",
                        "UTF-8",
                        null,
                        2,
                        0, correlationId, AppConstants.REPLY_EXCHANGE_NAME, null,
                        null, sendTime, null, null,
                        "SpringProducer", null);
 
        MessageProperties sendMessageProperties =
                messagePropertiesConverter.toMessageProperties(props, null,"UTF-8");
        sendMessageProperties.setReceivedExchange(AppConstants.REPLY_EXCHANGE_NAME);
        sendMessageProperties.setReceivedRoutingKey(AppConstants.REPLY_MESSAGE_KEY);
        sendMessageProperties.setRedelivered(true);
 
        Message sendMessage = MessageBuilder.withBody(message.getBytes())
                .andProperties(sendMessageProperties)
                .build();
 
        Message replyMessage =
        		rabbitTemplate.sendAndReceive(AppConstants.SEND_EXCHANGE_NAME,
        				AppConstants.SEND_MESSAGE_KEY, sendMessage);
 
        String replyMessageContent = null;
        try {
            replyMessageContent = new String(replyMessage.getBody(),"UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }   
        return replyMessageContent;
    }
}              
由於這裏的MQSenderSupplier對象是臨時構建,因此它使用的RabbitTemplate和MessagePropertiesConverter對象都是從Spring Context環境中直接獲取,而沒有使用自動注入方式。ApplicationContextUtil是我們定義的一個Context輔助類(類似代碼可以從網上搜索到,這裏就不列出了),使用RabbitTemplate.sendAndReceive方法發送請求消息和接收返回消息,沒有另行定義Listener類。
再添加發送消息的Rest接口類SendMessageController
@RestController
public class SendMessageController
{
    @Autowired
    private SendMessageService sendMessageService;
 
    @RequestMapping(value = "/caculate", method = RequestMethod.POST)
    @ResponseBody
    public String sendMessage(@RequestBody String number)
    {
        String result =
                sendMessageService.sendMessage(number);
         return "Factorial(" + number + ") = " + result;
    }
}
修改消費者應用程序,添加一個用於計算階乘的Message Listener,綁定springMessageQueue隊列,替換之前定義的Message Listener。
public class CaculateListener implements ChannelAwareMessageListener {
 
    @Autowired
    private MessagePropertiesConverter messagePropertiesConverter;
 
    @Autowired
    private RabbitTemplate rabbitTemplate;
 
    public void onMessage(Message message, Channel channel) throws Exception 
    {
        MessageProperties messageProperties = message.getMessageProperties();
        AMQP.BasicProperties rabbitMQProperties =
                messagePropertiesConverter.fromMessageProperties(messageProperties, "UTF-8");
        String numberContent = null;
        numberContent = new String(message.getBody(),"UTF-8");
        System.out.println("The received number is:" + numberContent);
        String consumerTag = messageProperties.getConsumerTag();
        int number = Integer.parseInt(numberContent);
 
        String result = factorial(number);
 
        AMQP.BasicProperties replyRabbitMQProps =
                new AMQP.BasicProperties("text/plain",
                        "UTF-8",
                        null,
                        2,
                        0, rabbitMQProperties.getCorrelationId(), null, null,
                        null, null, null, null,
                        consumerTag, null);
        Envelope replyEnvelope =
                new Envelope(messageProperties.getDeliveryTag(), true,
                        "springReplyMessageExchange", "springReplyMessage");
 
        MessageProperties replyMessageProperties =
                messagePropertiesConverter.toMessageProperties(replyRabbitMQProps,
                        replyEnvelope,"UTF-8");
 
        Message replyMessage = MessageBuilder.withBody(result.getBytes())
                .andProperties(replyMessageProperties)
                .build();
 
        rabbitTemplate.send("springReplyMessageExchange","springReplyMessage", replyMessage);
        channel.basicAck(messageProperties.getDeliveryTag(), false);
    }
    ..........

這裏計算階乘的方法factorial和系列1中相同,代碼不再列出。

修改applicationContext.xml文件,替換Message Listener Container中的Listener對象爲CaculateListener對象。

<bean id="caculateListener"
            class="com.qf.rabbitmq.listener.CaculateListener" />
 
<rabbit:listener-container message-converter="serializerMessageConverter"
                               ..................
                               recovery-back-off="backOff">
        <rabbit:listener ref="caculateListener" queues="springMessageQueue"/>
</rabbit:listener-container>

修改完成後啓動消費者和生產者程序,使用生產者的rest接口計算20的階乘值(生產者程序使用8180端口啓動),我們可以看到消費者應用計算出了20的階乘結果,並返回給生產者應用。



假如消費者應用計算時間較長,生產者是否還能獲取正確的結算結果呢?

我們修改消費者應用,在計算後睡眠10s,再返回計算結果。 重新啓動消費者和生產者程序,調用計算階乘的rest接口,我們會看到生產者端拋出了空引用異常,沒有獲取到返回消息。



這是因爲RabbitTemplate對象默認的接收Reply消息的超時時間只有5000ms(5s),超過這個時間sendAndReceive方法會直接返回空對象。 查看RabbitTemplate的代碼,我們可以看到是內置的PendingReply對象從它自身的阻塞隊列queue中獲取返回消息。 RabbitTemplate對象是在onMessage方法中獲取到返回消息時才把消息放入這個阻塞隊列的。

public class RabbitTemplate {
  //默認Reply Timeout時間5000ms
  private volatile long replyTimeout = DEFAULT_REPLY_TIMEOUT;
  ..............
  //這個方法被sendAndReceive調用,實際處理消息的發送和返回消息的接收。
  private Message exchangeMessages(final String exchange, final String routingKey, final Message message,
	final CorrelationData correlationData, Channel channel, final PendingReply pendingReply, String messageTag)
		throws Exception {
                ............		
		//發送消息到阻塞隊列
                doSend(channel, exchange, routingKey, message, mandatory, correlationData);
                //從pendingReply的阻塞隊列queue中獲取返回消息。
		reply = this.replyTimeout < 0 ? pendingReply.get() : pendingReply.get(this.replyTimeout, TimeUnit.MILLISECONDS);
		return reply;
	}
 
  public void onMessage(Message message) {
		try {
			String messageTag;
                        ...............
                        PendingReply pendingReply = this.replyHolder.get(messageTag);
                        ...............
                        message.getMessageProperties().setReplyTo(savedReplyTo);
                        //PendingReply對象將返回消息放入阻塞隊列中
			pendingReply.reply(message);
  .............
  private static class PendingReply {
  .............
  //從阻塞隊列中獲取返回消息對象。
  public Message get(long timeout, TimeUnit unit) throws InterruptedException 
  {
	Object reply = this.queue.poll(timeout, unit);
        //如果返回消息不爲空,將返回對象轉換爲Amqp Message對象。
	return reply == null ? null : processReply(reply);
  }
  private Message processReply(Object reply) 
  {
      if (reply instanceof Message) {
	return (Message) reply;
      }	
      ............		
  }
 
  public void reply(Message reply) 
  {
	this.queue.add(reply);
  }
由於我們設置的消費者應用處理時間爲10000ms,導致PendingReply從阻塞隊列取返回消息時,onMessage方法還沒有被觸發,阻塞隊列爲空,因此返回的消息對象爲空,從而拋出異常。 
爲了解決這個問題,我們可以把RabbitTemplate的replyTimeout時間設置的稍微長一點,例如1分鐘(60000ms)。
@Bean(name="rabbitTemplate")
public RabbitTemplate getRabbitTemplate()
{
 ................
 rabbitTemplate.setReplyTimeout(60000);
 return rabbitTemplate;
}
再次啓動生產者程序,計算30的階乘值,我們看到空引用異常不再拋出,階乘計算的結果也正常返回。

使用AsyncRabbitTemplate發送異步消息

使用RabbitTemplate.sendAndReceive方法發送消息給消費者,獲得返回消息,這是一個同步的過程,發送消息和等待接收消息是 在同一個線程中執行,如果等待返回消息的時間過長,當前方法的後續操作將被卡住,無法執行。 
爲了驗證這一點,我們在生產者程序中添加日誌輸出代碼。
Message replyMessage = rabbitTemplate.sendAndReceive(AppConstants.SEND_EXCHANGE_NAME,
        				AppConstants.SEND_MESSAGE_KEY, sendMessage);
 
logger.info("Send the caculate number to consumer");
 
String replyMessageContent = null;
try {
       replyMessageContent = new String(replyMessage.getBody(),"UTF-8");
       logger.info("The reply message is:" + replyMessageContent);
    } 
    catch (UnsupportedEncodingException e) 
    {
       e.printStackTrace();
    }
 
logger.info("The following operation");

我們再調用階乘接口,生產者端控制檯的輸出日誌如下圖


從日誌可以看出“The following operation”日誌信息的輸出是在獲得返回消息之後執行的,假如當前方法的後續操作不依賴於返回消息的內容,但卻要等待返回消息的獲取完成後才能執行,這樣的同步操作是不合適的,我們需要分離消息發送和返回消息的接收,使這兩個操作在不同的線程中異步執行。

要做到這一點有兩種方式。

第一種方式是使用RabbitTemplate.send方法發送消息,定義一個Message Listener偵聽返回隊列,獲取並處理返回消息,這種方式在前面的代碼中已經使用過,不再贅述。

第二種方式是使用spring-amqp 1.6引入的AsyncRabbitTemplate類,實現消息的異步發送,下面我們詳細介紹這種方式的實現。

爲了區分同步消息和異步消息的發送,我們新定義兩個消息隊列,一個名爲“springAsyncMessageQueue”,用於接收AsyncRabbitTemplate發送的異步消息,一個名爲“springAsyncReplyMessageQueue”,用於接收異步消息處理後的返回消息,同時把這兩個消息隊列分別綁定到 “springMessageExchange”和“springReplyMessageExchange”上,routingKey分別是“springAsyncMessage”和“springAsyncReplyMessage”。 消費者端的applicationContext.xml修改如下:

<rabbit:queue id="springAsyncMessageQueue" name="springAsyncMessageQueue" auto-delete="false"
                  durable="true" exclusive="false" auto-declare="true" declared-by="rabbitAdmin" />
<rabbit:direct-exchange id="springMessageExchange" name="springMessageExchange" durable="true"
                            auto-declare="false" auto-delete="false" declared-by="rabbitAdmin">
        <rabbit:bindings>
            <rabbit:binding queue="springMessageQueue" key="springMessage" />
            <rabbit:binding queue="springAsyncMessageQueue" key="springAsyncMessage" />
        </rabbit:bindings>
</rabbit:direct-exchange>
 
<rabbit:queue id="springAsyncReplyMessageQueue" name="springAsyncReplyMessageQueue" auto-delete="false"
                  durable="true" exclusive="false" auto-declare="true" declared-by="rabbitAdmin" />
 
<rabbit:direct-exchange id="springReplyMessageExchange" name="springReplyMessageExchange"
            durable="true" auto-delete="false" auto-declare="true" declared-by="rabbitAdmin">
        <rabbit:bindings>
            <rabbit:binding queue="springReplyMessageQueue" key="springReplyMessage" />
            <rabbit:binding queue="springAsyncReplyMessageQueue" key="springAsyncReplyMessage" />
        </rabbit:bindings>
</rabbit:direct-exchange>

再添加一個處理異步消息的MessageListener類AsyncMessageListener,這個Listener類接收異步消息後,休眠10s後發送返回消息。

public class AsyncMessageListener implements ChannelAwareMessageListener {
 
    @Autowired
    private MessagePropertiesConverter messagePropertiesConverter;
 
    @Autowired
    private RabbitTemplate rabbitTemplate;
 
    @Override
    public void onMessage(Message message, Channel channel) throws Exception
    {
        MessageProperties messageProperties = message.getMessageProperties();
        AMQP.BasicProperties rabbitMQProperties =
                messagePropertiesConverter.fromMessageProperties(messageProperties, "UTF-8");
 
         String messageContent = new String(message.getBody(), "UTF-8");
 
         System.out.println("The received message is:" + messageContent);
 
        String consumerTag = messageProperties.getConsumerTag();
 
        String replyMessageContent = consumerTag + " have received the message '" + messageContent + "'";
 
        Thread.sleep(10000);
 
        AMQP.BasicProperties replyRabbitMQProps =
                new AMQP.BasicProperties("text/plain",
                        "UTF-8",
                        null,
                        2,
                        0, rabbitMQProperties.getCorrelationId(), null, null,
                        null, null, null, null,
                        consumerTag, null);
        //創建返回消息的信封頭
        Envelope replyEnvelope =
                new Envelope(messageProperties.getDeliveryTag(), true,
                        "springReplyMessageExchange", "springAsyncReplyMessage");
 
        MessageProperties replyMessageProperties =
                messagePropertiesConverter.toMessageProperties(replyRabbitMQProps,
                        replyEnvelope,"UTF-8");
 
        Message replyMessage = MessageBuilder.withBody(replyMessageContent.getBytes())
                .andProperties(replyMessageProperties)
                .build();
 
        rabbitTemplate.send("springReplyMessageExchange","springAsyncReplyMessage", replyMessage);
        channel.basicAck(messageProperties.getDeliveryTag(), false);
    }
}

在Message Listener Container中追加這個Listener
<bean id="asyncMessageListener"
          class="com.qf.rabbitmq.listener.AsyncMessageListener" />
<rabbit:listener-container message-converter="serializerMessageConverter"
                               ........................                               
                               recovery-back-off="backOff">
        <rabbit:listener ref="caculateListener" queues="springMessageQueue"/>
        <rabbit:listener ref="asyncMessageListener" queues="springAsyncMessageQueue" />
</rabbit:listener-container>

在生產者端,我們在RabitConfig中添加一個AsyncRabbitTemplate的bean定義
@Bean(name="asyncRabbitTemplate")
public AsyncRabbitTemplate getAsyncRabbitTemplate()
{
   AsyncRabbitTemplate asyncRabbitTemplate=
    	new AsyncRabbitTemplate(getConnectionFactory(),
    		AppConstants.SEND_EXCHANGE_NAME,
    		AppConstants.SEND_ASYNC_MESSAGE_KEY,
    		AppConstants.REPLY_ASYNC_QUEUE_NAME);
    	asyncRabbitTemplate.setReceiveTimeout(60000);
    	asyncRabbitTemplate.setAutoStartup(true);
    	return rabbitTemplate;
}

這裏設置的ReceiveTimeout屬性類似於RabbitTemplate的ReplyTimeout屬性,是獲取返回消息的超時時間。 AsyncRabbitTemplate構造函數中的四個參數分別是連接工廠,發送消息的Exchange名,發送異步消息的RoutingKey(“springAsyncMessage”,用於Exchange將異步消息轉發到springAsyncMessageQueue隊列),返回消息隊列名(“springAsyncMessageQueue”,用於接收異步消息對應的返回消息)

在SendMessageService和SendMessageServiceImpl中添加一個發送異步消息的接口方法sendAsyncMessage

public interface SendMessageService
{
  ..........
  void sendAsyncMessage(String message);
}
 
@Service("sendMessageService")
public class SendMessageServiceImpl implements SendMessageService
{
  @Autowired
  private ThreadPoolTaskExecutor executor;
 
  ..........
   @Override
   public void sendAsyncMessage(String message)
   {
        CompletableFuture<Void> resultCompletableFuture =
                CompletableFuture.runAsync(new AsyncMQSenderThread(message), executor);
 
   }
}
在SendMessageServiceImpl.sendAsyncMessage方法中,我們把發送異步消息的任務交給了一個後臺線程異步執行,
使用了CompletableFuture的runAsync方法,沒有返回值。
負責發送消息的後臺任務類AsyncMQSenderThread主要代碼如下:

public class AsyncMQSenderThread implements Runnable
{
  .................
  private String message;


  public AsyncMQSenderThread(String message)
  {
        this.message = message;
  }
  
  public void run()
  {
     AsyncRabbitTemplate rabbitTemplate =
       (AsyncRabbitTemplate) ApplicationContextUtil.getBean("asyncRabbitTemplate");
     .........
     Message sendMessage = MessageBuilder.withBody(message.getBytes())
                .andProperties(sendMessageProperties)
                .build();


     logger.info("Send message to consumer");
     
     AsyncRabbitTemplate.RabbitMessageFuture replyMessageFuture =
                rabbitTemplate.sendAndReceive("springMessageExchange", "springAsyncMessage", sendMessage);


        replyMessageFuture.addCallback(new ListenableFutureCallback<Message>() {
            @Override
            public void onFailure(Throwable ex)
            {


            }


            @Override
            public void onSuccess(Message result)
            {
                logger.info("Received the reply message");
                try {
                    String replyMessageContent =
                            new String(result.getBody(), "UTF-8");
                    logger.info("The reply message is:" + replyMessageContent);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
            }
        });
        logger.info("The following operation");
    }   

在run方法中,AsyncRabbitTemplate.sendAndReceive方法返回的結果是一個RabbitMessageFuture對象,它實現了Google Guava提供的ListenableFuture接口,可以通過添加CallBack對返回消息進行處理。我們這裏是添加了一個匿名的ListenableFutureCallback對象。

在SendMessageController中添加一個Rest接口sendAsyncMessage
@RestController
public class SendMessageController
{
 ............
 @RequestMapping(value = "/sendMessage", method = RequestMethod.POST)
 public void sendAsyncMessage(@RequestBody String message)
 {
    sendMessageService.sendAsyncMessage(message);
 }
}
啓動生產者和消費者應用,調用sendMessage接口,生產者端控制檯的日誌如下所示:


從日誌可以看出,發送消息和處理返回消息是由獨立的兩個線程異步處理的,在發送完消息後,當前線程並沒有阻塞,等待返回消息,而是直接執行後續的代碼,對返回消息的處理是由RabbitMessageFuture對象綁定的Callback進行處理的。

AsyncRabbitTemplate方式和Message Listener方式都可以實現對返回消息的異步處理,它們的區別在於對返回消息中包含的CorrelationId與發送消息中包含的CorrelationId的比較,前一種方式是交給AsyncRabbitTemplate對象的內置方法處理, 而Message Listener方式需要開發者自行處理。

使用BatchingRabbitTemplate批量發送消息

使用RabbitTemplate和AsyncRabbitTemplate都是發送單條消息,每條消息發送完進行確認,但如果我們想發送20條消息,對這些消息進行一次性的確認,應該如何實現呢?我們可以使用spring-amqp 1.4.1引入的BatchingRabbitTemplate進行消息發送。

BatchingRabbitTemplate的構造函數形式如下:

public BatchingRabbitTemplate(BatchingStrategy batchingStrategy, TaskScheduler scheduler) 
可以看出BatchingRabbitTemplate有兩個屬性,一個是批量策略,定義了批量發送消息時消息的組合方式以及消息Properties的設定, 另一個是任務調度器,用於定義發送批量消息的定時任務。 我們這裏使用的批量策略類是spring-ampq自帶的SimpleBatchingStrategy類,它的定義大致如下:
public class SimpleBatchingStrategy implements BatchingStrategy 
{
    private final int batchSize;
 
    private final int bufferLimit;
 
    private final long timeout;
 
    public SimpleBatchingStrategy(int batchSize, int bufferLimit, long timeout) 
    {
		this.batchSize = batchSize;
		this.bufferLimit = bufferLimit;
		this.timeout = timeout;
    }
    .......
    @Override
    public MessageBatch addToBatch(String exchange, String routingKey, Message message) {
    .........
這個批量策略的思想是設置一個消息緩存區,以及批量消息數的上限,當待發送消息的條數達到上限(batchSize),或者待發送的消息的總大小超過緩存區上限(bufferLimit),即將消息拼裝成一個大消息進行發送。 使用BatchingRabbitTemplate發送消息的流程如下圖所示


1)調用端調用BatchingRabbitTemplate的send方法發送消息。

2)send方法調用BatchingRabbitTemplate對象設置的BatchingStrategy屬性的addToBatch方法。

3)以SimpleBatchingStrategy.addToBatch方法爲例,如果累計的未發送消息數達到消息數上限,或者消息總長度大於緩存區大小,
會將未發送消息隊列裏的消息拼裝成一條完整消息,封裝在一個MessageBatch中返回給BatchingRabbitTemplate對象。

@Override
public MessageBatch addToBatch(String exchange, String routingKey, Message message) {
   ............
   //消息隊列裏現存消息加待發送消息大小超過緩存區上限,先將消息隊列裏已有消息拼裝爲
   //一條消息返回。
   if (this.messages.size() > 0 && this.currentSize + bufferUse > this.bufferLimit) 
   {
         //拼裝消息隊列裏已有消息到MessageBatch
         batch = doReleaseBatch();
         //使用新消息的exchange,routingKey初始化exchange和routingKey
	 this.exchange = exchange;
	 this.routingKey = routingKey;
   }
   this.currentSize += bufferUse;
   this.messages.add(message);
   //如果添加了新消息的消息隊列的消息數超過消息上限或者消息總大小超過緩存區上限,
   //將消息隊列裏所有消息拼裝成一條消息,封裝到MessageBatch對象中返回。
   if (batch == null && (this.messages.size() >= this.batchSize
	|| this.currentSize >= this.bufferLimit)) 
   {
	batch = doReleaseBatch();
   }
   ...................
}


private MessageBatch doReleaseBatch() 
{
   if (this.messages.size() < 1) 
   {
	return null;
   }
   Message message = assembleMessage();
   MessageBatch messageBatch = new MessageBatch(this.exchange, this.routingKey, message);
   this.messages.clear();
   this.currentSize = 0;
   this.exchange = null;
   this.routingKey = null;
   return messageBatch;
}

4)BatchingRabbitTemplate對象使用父類(RabbitTemplate)的send方法發送MessageBatch中包含的消息。

我們修改生產者程序,添加BatchingRabbitTemplate對象
@Bean(name="batchingStrategy")
public BatchingStrategy createBatchingStrategy()
{
   SimpleBatchingStrategy batchingStrategy =
        new SimpleBatchingStrategy(20,1000,60000);
   return batchingStrategy;
}
 
@Bean(name="batchRabbitTemplate")
public BatchingRabbitTemplate createBatchRabbitTemplate()
{
    BatchingRabbitTemplate batchTemplate =
            new BatchingRabbitTemplate(createBatchingStrategy(),  new ConcurrentTaskScheduler());
    batchTemplate.setConnectionFactory(getConnectionFactory());
 
    return batchTemplate;
}

我們這裏的BatchingStrategy設置的消息條數上限爲20條,消息總大小上限爲1000。使用的TaskScheduler是java自帶的 ConcurrentTaskScheduler對象,使用單線程池執行後續發送批量消息的計劃任務,實際使用時可以設置爲自定義的TaskScheduler.

爲了測試方便,我們再添加兩個消息隊列,一個消息隊列名爲springBatchMessageQueue,用於接收發送的批量消息,一個消息隊列名爲springBatchReplyMessageQueue。在生產者和消費者端分別添加定義隊列和消息綁定的代碼,由於和異步消息類似,這裏不再列出。 在消費者端定義一個BatchMessageListener,用於從springBatchMessageQueue接收和處理消息,它的主要代碼如下:

public class BatchMessageListener implements ChannelAwareMessageListener
{
    private Logger logger = LoggerFactory.getLogger(BatchMessageListener.class);
 
    public void onMessage(Message message, Channel channel) throws Exception
    {
      MessageProperties messageProperties = message.getMessageProperties();
      AMQP.BasicProperties rabbitMQProperties =
                messagePropertiesConverter.fromMessageProperties(messageProperties, "UTF-8");
 
      String messageContent = new String(message.getBody(), "UTF-8");
      String correlationId = rabbitMQProperties.getCorrelationId();
      //打印接收到的批量消息內容和correlationId,用於檢驗是否使用第一條消息的correaltionId.
      logger.info("The received batch message is:" + messageContent);
      logger.info("The correlation id is:" + correlationId);
      ......
   }       

在生產者端,我們在SendMessageService接口中定義發送批量消息的接口sendBatchMessages方法。

public interface SendMessageService
{
  ........
  void sendBatchMessage(List<String> messages);
}
 
public class SendMessageServiceImpl implements SendMessageService
{
 .........
 @Override
 public void sendBatchMessage(List<String> messages) 
 {
       CompletableFuture.runAsync(new BatchMQSenderThread(messages), executor);
 }
我們定義了BatchMQSenderThread類來實現後臺發送批量消息,它的主要代碼如下:
public class BatchMQSenderThread  implements Runnable
{
    private Logger logger = LoggerFactory.getLogger(BatchMQSenderThread.class);
 
    private List<String> messageList;
 
    public BatchMQSenderThread(List<String> messageList)
    {
        this.messageList = messageList;
    }
 
    public void run()
    {
        BatchingRabbitTemplate rabbitTemplate =
                (BatchingRabbitTemplate) ApplicationContextUtil.getBean("batchRabbitTemplate");
        for(String message:messageList)
        {
            Date sendTime = new Date();
            String correlationId = UUID.randomUUID().toString();
 
            AMQP.BasicProperties props =
                    new AMQP.BasicProperties("text/plain",
                            "UTF-8",
                            null,
                            2,
                            0, correlationId, AppConstants.REPLY_EXCHANGE_NAME, null,
                            null, sendTime, null, null,
                            "SpringProducer", null);
 
            MessageProperties sendMessageProperties =
                    messagePropertiesConverter.toMessageProperties(props, null,"UTF-8");
            sendMessageProperties.setReceivedExchange(AppConstants.REPLY_EXCHANGE_NAME);
            sendMessageProperties.setReceivedRoutingKey(AppConstants.REPLY_BATCH_MESSAGE_KEY);
            sendMessageProperties.setRedelivered(true);
 
            Message sendMessage = MessageBuilder.withBody(message.getBytes())
                    .andProperties(sendMessageProperties)
                    .build();
 
            logger.info("Send message '" + message + "' to batch");
            logger.info("The message's correlation id is:" + correlationId);
 
            rabbitTemplate.send(AppConstants.SEND_EXCHANGE_NAME, AppConstants.SEND_BATCH_MESSAGE_KEY, sendMessage, null);
        }
}

BatchMQSenderThread.run方法循環讀取消息List中的消息,調用batchingRabbitTemplate對象進行消息發送,每條消息我們都生成一個uuid作爲消息的correlationId,我們接下來會看到,在最後發送的批量消息中,只有第一條消息的correlationId被採用。

最後我們定義一個發送批量消息的rest接口,批量消息使用逗號進行分割。

@RestController
public class SendMessageController
{
  ............
  @RequestMapping(value = "/sendMessages", method = RequestMethod.POST)
  public void sendMessages(@RequestBody String messages)
  {
     List<String> messageList = Splitter.on(",").splitToList(messages);
     sendMessageService.sendBatchMessage(messageList);
  }  
修改完成後,我們啓動生產者和消費者應用,通過生產者rest接口發送30條消息(消息正文從Message0到Message29)

從生產者端日誌可以看出,SendMessageServiceImpl調用BatchRabbitTemplate.send方法,發送了30條消息,但是由於消息條數超過了我們預設的20條限制,實際只批量發送了20條消息(從消費者端的日誌截圖可以看到),消費者端仍然是把發送的批量消息作爲20條獨立的消息各自接收的。



由於我們定義的SimpleBatchingStrategy策略對象的超時時間是60000ms(1分鐘),1分鐘後,剩餘的10條消息被BatchRabbitTemplate對象批量發出,被消費者接收,消費者日誌顯示了這一點。


消費者日誌還顯示,BatchRabbitTemplate對象發送的30條消息,它們都具有相同的correlationId,是第一條消息的correlationId,而不是創建這些消息時生成的uuid,這是由SimpleBatchingStrategy類的批量策略決定的。

下面我們修改一下生產者的SimpleBatchingStrategy屬性設定,設定消息緩存上限爲200,超時時間爲120000ms(2分鐘),再發送30條消息。 消費者端的輸出日誌如下:




我們可以看出,由於消息緩存上限的限制,BatchRabbitTemplate只批量發送了16條消息,後14條消息是在2分鐘後延時發送。

在實際使用時,我們也可以根據業務需要,自定義BatchStrategy策略。

Rabbit Annotation的使用

在系列2裏我們提到了使用spring-rabbit的rabbit前綴簡化applicationContext.xml配置文件中的RabbitMQ beans配置,spring-amqp還提供了RabbitMQ常用對象對應的Annoation,這些註解如下圖所示:


下面我們使用這些Annotation簡化我們的生產者應用代碼。

我們直接修改ReplyBatchMessageListener,在它的onMessage方法頭部添加以下註解:

@RabbitListener(
        bindings = @QueueBinding(
        value = @Queue(value="springBatchReplyMessageQueue", durable = "true", exclusive = "false",autoDelete = "false"),
        exchange = @Exchange(value="springReplyMessageExchange", durable = "true"),
        key = "springBatchReplyMessage"),
        admin="rabbitAdmin")
public void onMessage(Message message)
{
     try 
     {
          String messageContent = new String(message.getBody(), "UTF-8");
          logger.info("The reply message content is:" + messageContent);
     } 
     catch (UnsupportedEncodingException e) 
     {
          e.printStackTrace();
      }
}

這個註解中使用到了@RabbitListener,將onMessage方法定義爲一個Message Listener, @QueueBinding用於定義Binding對象,@Queue定義Queue對象,@Exchange定義Exchange對象,key屬性是Bind對象的routingKey. spring-amqp內置的RabbitAdmin對象根據@Queue和@Exchange的屬性,在RabbitMQ服務器上創建消息隊列和Exchange, 根據@QueueBinding的屬性,在RabbitMQ服務器上建立對應的Bind關係。 admin屬性是創建Message Listener Container使用的RabbitAdmin對象,我們這裏引用RabbitConfig中定義的“rabbitAdmin”對象。

@RabbitListener定義的Message Listener,所在的Message Listener Container是spring-amqp根據默認設置創建的Message Listener Container對象。如果我們想根據需要使用定製的Message Listener Container,我們需要在RabbitConfig類中定製SimpleMessageListenerContainerFactory對象:

@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() 
{
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(getConnectionFactory());
        factory.setConcurrentConsumers(5);
        factory.setMaxConcurrentConsumers(5);
        factory.setConsumerTagStrategy(new ReplyConsumerTagStrategy());
        factory.setPrefetchCount(5);
        factory.setMessageConverter(getMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
        BackOff backOff = new FixedBackOff(60000,100);
        factory.setRecoveryBackOff(backOff);
        return factory;
}

我們可以看出這個方法設置的factory對象屬性,和我們之前定義的Message Listener Container bean對象的屬性有相似之處, 不過factory對象的屬性,並沒有完全覆蓋Message Listener Container對象的屬性定義,例如messagePropertiesConverter屬性的設置,在SimpleRabbitListenerContainerFactory類中就找不到,因此,如果想使用Message Listener Container比較複雜的屬性,建議還是使用@Bean定義。

如果@RabbitListener關聯的消息隊列,Exchange和Bind關係在RabbitMQ服務器中已經創建,我們可以在@RabbitListener中不使用@QueueBinding,直接使用queues屬性,此時onMessage方法頭部的註解如下:

@RabbitListener(admin="rabbitAdmin", queues="springBatchReplyMessageQueue")
public void onMessage(Message message)

爲了使Rabbit註解生效,還需要添加@EnableRabbit註解,我們在RabbitConfig類頭部添加這個註解

@Configuration
@EnableRabbit
@EnableConfigurationProperties(RabbitProperties.class)
public class RabbitConfig
{
  ........

啓動生產者和消費者應用,使用批量接口發送20條消息,從生產者日誌我們可以看出我們定義在ReplyBatchMessageListener.onMessage方法上的添加的@RabbitListener將onMessage方法變成了一個Message Listener,接收了springBatchReplyMessageQueue隊列的返回消息。


如果有很多方法想定義爲一個Message Listener,而他們使用的@RabbitListener註解內容又相同,我們可以自定義RabbitListener註解,例如,我們可以將上面的@RabbitListener註解定義爲ReplyRabbitListener註解接口

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@RabbitListener(admin="rabbitAdmin", queues="springBatchReplyMessageQueue")
public @interface ReplyRabbitListener {
}
我們註釋掉ReplyBatchMessageListener.onMessage方法頭部的@RabbitListener註解,重新定義一個POJO類ReplyConsumerDelegate, 在這個類中添加一個消息處理方法processReplyMessage,在這個方法頭部添加我們自定義的註解ReplyRabbitListener,把這個方法變成了一個Message Listener,在這個類頭部我們還添加了@Component註解,使這個對象在SpringBoot啓動時被初始化。

@Component
public class ReplyConsumerDelegate
{
    private Logger logger = LoggerFactory.getLogger(ReplyConsumerDelegate.class);
 
     @ReplyRabbitListener
     public void processReplyMessage(String message)
     {
         logger.info("The reply message content is:" + message);
     }
}

我們再啓動生產者應用,發送批量消息,從生產者日誌可以看出,@ReplyRabbitListener註解使處理返回消息的方法變成了ReplyConsumerDelegate.processReplyMessage



對象類型消息的發送

前面的例子中發送的消息消息本體都是String類型,如果我們想發送一個對象類型的消息時,應該怎麼處理呢?下面的實例將演示如何發送對象消息。

我們定義兩個實體類Company和Employee

public class Company implements java.io.Serializable
{
    private int id;
    private String companyName;
    private String establishDate;
 
 
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public String getCompanyName() {
        return companyName;
    }
 
    public void setCompanyName(String companyName) {
        this.companyName = companyName;
    }
 
    public String getEstablishDate() {
        return establishDate;
    }
 
    public void setEstablishDate(String establishDate) {
        this.establishDate = establishDate;
    }
 
    public String toString()
    {
        return "companyId=" + Integer.toString(getId()) + 
        ",companyName=" + getCompanyName() + ",establishDate=" + getEstablishDate();
    }
}
 
public class Employee implements java.io.Serializable
{
        private String employeeId;
        private String employeeName;
        private String employeeDate;
 
    public String getEmployeeId() {
        return employeeId;
    }
 
    public void setEmployeeId(String employeeId) {
        this.employeeId = employeeId;
    }
 
    public String getEmployeeName() {
        return employeeName;
    }
 
    public void setEmployeeName(String employeeName) {
        this.employeeName = employeeName;
    }
 
    public String getEmployeeDate() {
        return employeeDate;
    }
 
    public void setEmployeeDate(String employeeDate) {
        this.employeeDate = employeeDate;
    }
 
    public String toString()
    {
        return "EmployeeId=" + getEmployeeId() + ",EmployeeName=" + getEmployeeName() + ",EmployeeDate=" + getEmployeeDate();
    }
}

我們創建一個名爲“springRabbitHandlerQueue”的消息隊列,綁定到“springMessageExchange”Exchange對象上。


我們設定分別發送一條Company類型的消息,一條Employee類型的消息到springRabbitHandlerQueue消息隊列裏,消費者在同一個Message Listener中使用不同方法處理不同類型的消息。

我們在生產者程序中添加HandlerMQSenderThread類,用於發送對象類型的消息,它的主要代碼如下:

public class HandlerMQSenderThread  implements Runnable
{
    private Object message;
 
    public HandlerMQSenderThread(Object message)
    {
        this.message = message;
    }
 
    @Override
    public void run()
    {
        ............
        RabbitTemplate rabbitTemplate =
                    (RabbitTemplate)ApplicationContextUtil.getBean("rabbitTemplate");
 
        AMQP.BasicProperties props =
                    new AMQP.BasicProperties(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT,
                            "UTF-8",
                            null,
                            2,
                            0, correlationId, AppConstants.REPLY_EXCHANGE_NAME, null,
                            null, sendTime, null, null,
                            "SpringProducer", null);
        Message sendMessage = MessageBuilder.withBody(toByteArray(message))
                    .andProperties(sendMessageProperties)
                    .build();
        ......
        rabbitTemplate.send(AppConstants.SEND_EXCHANGE_NAME, AppConstants.SEND_HANDLER_MESSAGE_KEY, sendMessage);            
   }
 
   public byte[] toByteArray (Object obj) {
        byte[] bytes = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
            oos.flush();
            bytes = bos.toByteArray ();
            oos.close();
            bos.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        return bytes;
    }

run方法裏調用toByteArray方法,把對象消息轉換成byte數組。在MessageProperties中,我們設定content-type爲 “application/x-java-serialized-object”,表示發送的消息體類型爲Java對象,便於消費者端進行反序列化。

在SendMessageService中我們定義一個接口方法sendObjectMessage,用於發送對象消息。

public interface SendMessageService
{
 ..........
 void sendObjectMessage(Object message);
}
 
public class SendMessageServiceImpl implements SendMessageService
{
 .......
 @Override
 public void sendObjectMessage(Object message) 
 {
    CompletableFuture.runAsync(new HandlerMQSenderThread(message), executor);
 }
}

在SendMessageController類中添加兩個Rest接口方法sendCompanyMessage和sendEmployeeMessage,這兩個方法從客戶端接收Json形式的報文,將其轉換爲Company對象和Employee對象。
@RequestMapping(value = "/sendCompanyMessage", method = RequestMethod.POST)
public void sendCompanyMessage(@RequestBody String message)
{
   JSONObject obj = (JSONObject)JSON.parse(message);
   Company company = JSONObject.toJavaObject(obj, Company.class);
   sendMessageService.sendObjectMessage(company);
}
 
@RequestMapping(value = "/sendEmployeeMessage", method = RequestMethod.POST)
public void sendEmployeeMessage(@RequestBody String message)
{
    JSONObject obj = (JSONObject)JSON.parse(message);
    Employee employee = JSONObject.toJavaObject(obj, Employee.class);
    sendMessageService.sendObjectMessage(employee);
}

消費者端,我們使用spring-boot-starter-amqp創建一個消費者應用,啓動類和Config類的主要代碼如下:

@SpringBootApplication
@ComponentScan("com.qf.rabbitmq")
@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class)
public class SpringConsumerTestApplication
{
    public static void main(String[] args) 
    {
	SpringApplication.run(SpringConsumerTestApplication.class, args);
    }
}
 
@Configuration
@EnableRabbit
@EnableConfigurationProperties(RabbitProperties.class)
//public class RabbitConfig implements RabbitListenerConfigurer
public class RabbitConfig
{
    @Autowired
    private RabbitProperties rabbitProperties;
 
    /**
      * 創建RabbitMQ連接工廠
      * @return 根據application.properties設定參數建立的連接工廠對象
      */
    @Bean("connectionFactory")
    public ConnectionFactory getConnectionFactory() {
        com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory =
                new com.rabbitmq.client.ConnectionFactory();
        rabbitConnectionFactory.setHost(rabbitProperties.getHost());
        rabbitConnectionFactory.setPort(rabbitProperties.getPort());
        rabbitConnectionFactory.setUsername(rabbitProperties.getUsername());
        rabbitConnectionFactory.setPassword(rabbitProperties.getPassword());
        rabbitConnectionFactory.setVirtualHost(rabbitProperties.getVirtualHost());
        ConnectionFactory connectionFactory = new CachingConnectionFactory(rabbitConnectionFactory);
        return connectionFactory;
    }
 
 
    /**
      * 創建Message Listener Container工廠,它負責創建RabbitListener註解的
      * Message Listener所在的Container.
      * @return Message Listener Container工廠對象
      */
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
        SimpleRabbitListenerContainerFactory factory = 
        		new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(getConnectionFactory());
        factory.setConcurrentConsumers(5);
        factory.setMaxConcurrentConsumers(5);
        factory.setPrefetchCount(5);
        return factory;
    }
}

創建一個POJO類RabbitHandlerConsumer,通過添加@RabbitListener註解和@RabbitHandler註解,使它成爲springRabbitHandlerQueue 消息的Message Listener對象,並且根據不同類型的消息分別處理。
@Component
@RabbitListener(queues="springRabbitHandlerQueue")
public class RabbitHandlerConsumer
{
    private Logger logger = LoggerFactory.getLogger(RabbitHandlerConsumer.class);
 
    @RabbitHandler
    public void processCompanyMessage(Company message)
    {
        logger.info("Received the message having Company type.");
        logger.info("The message content is:" + message.toString());
    }
 
    @RabbitHandler
    public void processEmployeeMessage(Employee message)
    {
        logger.info("Received the message having Employee type.");
        logger.info("The message content is:" + message.toString());
    }
}

RabbitHandler註解是spring-amqp 1.5開始引入的,它用於將POJO類方法轉換爲消息處理方法,RabbitListener註解則提升到類級別, 可以在POJO類頭部添加,作爲全局設定。需要指出的是RabbitHandler起作用必須是在RabbitMQ消息本體被正常轉換爲對象後,否則它將無法根據方法參數類型確定實際的handlerMethod,在實際使用時需要對MessageConverter做相關設定,具體做法請參考spring-amqp文檔。

最後將Employee和Company實體類添加到消費者項目中。

啓動生產者和消費者應用。

先通過sendCompanyMessage接口發送一個Company類型的消息。


消費者端日誌顯示這條消息被RabbitHandlerConsumer.processCompanyMessage方法接收並處理。


再發送一條Employee類型的消息


消費者端日誌顯示這條消息被RabbitHandlerConsumer.processEmployeeMessage方法接收並處理。


我們上面的消費者應用是基於Spring Boot環境的,如果我們想在Spring AMQP框架接收並處理對象消息,我們不需要添加@EnableRabbit和@Configuration註解,只需要在applicationContext.xml文件中添加<rabbit:annotation-driven>標籤即可。

我們新建一個基於Maven的消費者項目,pom.xml添加的庫爲:

<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.amqp</groupId>
      <artifactId>spring-amqp</artifactId>
      <version>1.6.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.amqp</groupId>
      <artifactId>spring-rabbit</artifactId>
      <version>1.6.5.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>1.7.21</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.21</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.21</version>
    </dependency>
</dependencies>

在src/main/resources目錄下添加applicationContext.xml文件,它的主要內容如下:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:rabbit="http://www.springframework.org/schema/rabbit"
    xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/rabbit
    http://www.springframework.org/schema/rabbit/spring-rabbit-1.6.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.0.xsd" >
 
    <context:annotation-config/>
 
    <context:property-placeholder
        ignore-unresolvable="true" location="classpath*:/application.properties" />    
 
    <context:component-scan base-package="com.qf.rabbitmq"  />  
 
    <rabbit:annotation-driven container-factory="rabbitListenerContainerFactory" />
 
    <bean id="rabbitMQConnectionFactory" class="com.rabbitmq.client.ConnectionFactory">
        <property name="username" value="${mq.userName}" />
        <property name="password" value="${mq.password}" />
        <property name="host" value="${mq.ip}" />
        <property name="port" value="${mq.port}" />
        <property name="virtualHost" value="${mq.virutalHost}" />
        <property name="automaticRecoveryEnabled" value="false" />
        <property name="topologyRecoveryEnabled" value="false" />
        <property name="networkRecoveryInterval" value="60000" />
    </bean>
 
    <rabbit:connection-factory id ="connectionFactory"
        connection-factory="rabbitMQConnectionFactory"
        connection-timeout="10000"
        cache-mode="CHANNEL"
        channel-cache-size="20"/>
 
    <bean id="rabbitListenerContainerFactory"
        class="org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory">
        <property name="connectionFactory" ref="connectionFactory"/>
        <property name="concurrentConsumers" value="3"/>
        <property name="maxConcurrentConsumers" value="3"/>
    </bean>
 
    <rabbit:admin id="rabbitAdmin" 
        connection-factory="connectionFactory" auto-startup="true"/>
</beans>

再添加RabbitHandlerConsumer,Employee,Company類。

最後在主程序中加載applicationContext.xml文件,啓動消費者應用

public class RabbitCustomerTest 
{
    public static void main( String[] args )
    {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
                new String[] { "classpath:applicationContext.xml" });
        context.start();
    }
}

啓動生產者應用,發送Company消息和Employee消息,我們可以看到消費者應用的響應與基於SpringBoot的消費者應用程序相同。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章