RabbitMQ的Java應用(1) -- Rabbit Java Client使用

Java環境下使用RabbitMQ客戶端需要導入ampq-client庫(RabbitMQ的Java Client庫,這裏我們使用3.6.5版本) ,RabbitMQ服務器使用的是本地RabbitMQ 3.6.6版本。

Maven環境配置

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>3.6.5</version>
</dependency>

Gradle環境

compile group: 'com.rabbitmq', name: 'amqp-client', version: '3.6.5'

RabbitMQ使用的結構示意圖如下:


從示意圖可以看出消息生產者並沒有直接將消息發送給消息隊列,而是通過建立與Exchange的Channel,將消息發送給Exchange,Exchange根據規則,將消息轉發給指定的消息隊列。消費者通過建立與消息隊列相連的Channel,從消息隊列中獲取消息。

這裏談到的Channel可以理解爲建立在生產者/消費者和RabbitMQ服務器之間的TCP連接上的虛擬連接,一個TCP連接上可以建立多個Channel。 RabbitMQ服務器的Exchange對象可以理解爲生產者發送消息的郵局,消息隊列可以理解爲消費者的郵箱。Exchange對象根據它定義的規則和消息包含的routing key以及header信息將消息轉發到消息隊列。

根據轉發消息的規則不同,RabbitMQ服務器中使用的Exchange對象有四種,Direct Exchange, Fanout Exchange, Topic Exchange, Header Exchange,如果定義Exchange時沒有指定類型和名稱, RabbitMQ將會爲每個消息隊列設定一個Default Exchange,它的Routing Key是消息隊列名稱。

RabbitMQ Java Client的官網示例有6個,本篇只使用三個例程,分別是使用默認Default Exchange的消息生產/消費,使用Direct Exchange的消息生產/消費,以及RPC方式的消息生產/消費。

爲了測試方便,我們新定義了一個virutal host,名字是test_vhosts,定義了兩個用戶rabbitmq_producer和rabbitmq_consumer, 設置其user_tag爲administrator(可以進行遠程連接), 爲它們設置了訪問test_vhosts下所有資源的權限。



使用默認Default Exchange的消息生產/消費

我們定義一個生產者程序,一個消費者程序。

生產者程序代碼如下:

public class ProducerApp
{
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = null;
        Channel channel = null;
        try
        {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("localhost");
            factory.setPort(5672);
            factory.setUsername("rabbitmq_producer");
            factory.setPassword("123456");
            factory.setVirtualHost("test_vhosts");
 
            //創建與RabbitMQ服務器的TCP連接
            connection  = factory.newConnection();
            channel = connection.createChannel();
            channel.queueDeclare("firstQueue", true, false, false, null);
            String message = "First Message";           
            channel.basicPublish("", "firstQueue", null, message.getBytes());
            System.out.println("Send Message is:'" + message + "'");            
        }
        catch(Exception ex)
        {
            ex.printStackTrace();
        }
        finally
        {
            if(channel != null)
            {
                channel.close();
            }
            if(connection != null)
            {
                connection.close();
            }
        }
    }

關於生產者的代碼有幾點說明:

1) RabbitMQ Java Client示例提供的ConnectionFactory屬性設置的代碼只有一句:

factory.setHost("localhost");


這句代碼表示使用rabbitmq服務器默認的virutal host(“/”),默認的用戶guest/guest進行連接,但是如果這段代碼運行在遠程機器上時, 將因爲guest用戶不能用於遠程連接RabbitMQ服務器而運行失敗,上面提供的代碼是可以進行建立遠程連接的代碼。

2)Channel建立後,調用Channel.queueDeclare方法創建消息隊列firstQueue。

Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                 Map<String, Object> arguments) throws IOException;   
這個方法的第二個參數durable表示建立的消息隊列是否是持久化(RabbitMQ重啓後仍然存在,並不是指消息的持久化),第三個參數exclusive 表示建立的消息隊列是否只適用於當前TCP連接,第四個參數autoDelete表示當隊列不再被使用時,RabbitMQ是否可以自動刪除這個隊列。 第五個參數arguments定義了隊列的一些參數信息,主要用於Headers Exchange進行消息匹配時。

3)生產者發送消息使用Channel.basicPublish方法。

void basicPublish(String exchange, String routingKey, 
 BasicProperties props, byte[] body) throws IOException;

第一個參數exchange是消息發送的Exchange名稱,如果沒有指定,則使用Default Exchange。 第二個參數routingKey是消息的路由Key,是用於Exchange將消息路由到指定的消息隊列時使用(如果Exchange是Fanout Exchange,這個參數會被忽略), 第三個參數props是消息包含的屬性信息。RabbitMQ的消息屬性和消息體是分開的,不像JMS消息那樣同時包含在javax.jms.Message對象中,這一點需要特別注意。 第四個參數body是RabbitMQ消息體。 我們這裏調用basicPublish方法發送消息時,props參數爲null,因而我們發送的消息是非持久化消息,如果要發送持久化消息,我們需要進行如下設置:

AMQP.BasicProperties props =
                    new AMQP.BasicProperties("text/plain",
                            "UTF-8",
                            null,
                            2,
                            0, null, null, null,
                            null, null, null, null,
                            null, null);
 channel.basicPublish("", "firstQueue", props, message.getBytes());   


定義props時的參數2表示消息的類型爲持久化消息。 運行生產者程序後,我們可以執行rabbitmqctl命令查看隊列消息,我們看到firstQueue隊列有一條消息。


消費者代碼如下:

public class ConsumerApp
{
    public static void main(String[] args)
    {
        Connection connection = null;
        Channel channel = null;
        try
        {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("localhost");
            factory.setPort(5672);
            factory.setUsername("rabbitmq_consumer");
            factory.setPassword("123456");
            factory.setVirtualHost("test_vhosts");
            connection = factory.newConnection();
            channel = connection.createChannel();
 
            Consumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                        throws IOException {
                    String message = new String(body, "UTF-8");
                    System.out.println(" Consumer have received '" + message + "'");
                }
            };
            channel.basicConsume("firstQueue", true, consumer);
        }
        catch(Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

消費者代碼中,建立Connection,Channel的代碼和生產者程序類似。它主要定義了一個Consumer對象,這個對象重載了DefaultCustomer類 的handleDelivery方法:

void handleDelivery(String consumerTag,
                        Envelope envelope,
                        AMQP.BasicProperties properties,
                        byte[] body) 
handleDelivery方法的第一個參數consumerTag是接收到消息時的消費者Tag,如果我們沒有在basicConsume方法中指定Consumer Tag,RabbitMQ將使用隨機生成的Consumer Tag(如下圖所示)


第二個參數envelope是消息的打包信息,包含了四個屬性:


1._deliveryTag,消息發送的編號,表示這條消息是RabbitMQ發送的第幾條消息,我們可以看到這條消息是發送的 第一條消息。

2._redeliver,重傳標誌,確認在收到對消息的失敗確認後,是否需要重發這條消息,我們這裏的值是false,不需要重發。

3._exchange,消息發送到的Exchange名稱,正如我們上面發送消息時一樣,exchange名稱爲空,使用的是Default Exchange。

4._routingKey,消息發送的路由Key,我們這裏是發送消息時設置的“firstQueue”。

第三個參數properties就是上面使用basicPublish方法發送消息時的props參數,由於我們上面設置它爲null,這裏接收到的properties 是默認的Properties,只有bodySize,其他全是null。

第四個參數body是消息體.

我們這裏重載的handleDelivery方法僅僅打印出了生產者發送的消息內容,實際使用時可以轉發給後臺程序進行處理。

在Consumer對象定義後,我們調用了Channel.basicConsume方法將Consumer與消息隊列綁定,否則Consumer無法從消息隊列獲取消息。

String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException

basicConsume方法的第一個參數是Consumer綁定的隊列名,第二個參數是自動確認標誌,如果爲true,表示Consumer接受到消息後,會自動發確認消息(Ack消息)給消息隊列,消息隊列會將這條消息從消息隊列裏刪除,第三個參數就是Consumer對象,用於處理接收到的消息。

如果我們想讓消費者接收到消息後對消息進行手動確認(Manual Ack),我們需要對代碼進行兩處改動:

1)在調用basicConsume方法時,將autoAck屬性設置爲false。

channel.basicConsume("firstQueue", false, consumer);

2)在handleDelivery方法中調用Channel.basicAck方法,發送手動確認消息給消息隊列。

public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                        throws IOException
{
      this.getChannel().basicAck(envelope.getDeliveryTag(), false);
}

basicAck方法有兩個參數,第一個參數deliverTag是消息的發送編號,第二個參數multiple是消息確認方式,如果值爲true,表示對消息隊列裏所有編號小於或等於當前消息編號的未確認消息進行手動確認,如果爲false,表示僅確認當前消息。

消費者代碼執行後,我們可以看到消費者程序的控制檯輸出了這條消息的內容,而且使用rabbitmqctl命令查看隊列消息時,隊列裏的消息數爲0。





使用Direct Exchange的消息生產/消費

使用Direct Exchange的生產者/消費者代碼與Default Exchange比較類似,不過生產者程序的代碼需要添加創建Direct Exchange和 將Exchange和消息隊列綁定的代碼,具體添加和修改的代碼如下:

channel.exchangeDeclare("directExchange", "direct");
channel.queueDeclare("directQueue", true, false, false, null);
channel.queueBind("directQueue", "directExchange", "directMessage");
String message = "First Direct Message";
 
channel.basicPublish("directExchange", "directMessage", null, message.getBytes());
System.out.println("Send Direct Message is:'" + message + "'");


首先我們調用Channel.exchangeDeclare方法創建名爲“directExchange”的Direct Exchange。

Exchange.DeclareOk exchangeDeclare(String exchange, String type,boolean durable) throws IOException

exchangeDeclare方法的第一個參數exchange是exchange名稱,第二個參數type是Exchange類型,有“direct”,“fanout”,“topic”,“headers”四種,分別對應RabbitMQ的四種Exchange。第三個參數durable是設置Exchange是否持久化( 即在RabbitMQ服務器重啓後Exchange是否仍存在,如果沒有設置,默認是非持久化的)

創建“directQueue”消息隊列後,我們再調用Channel.queueBind方法,將我們創建的Direct Exchange和消息隊列綁定。

Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException;

queueBind方法第一個參數queue是消息隊列的名稱,第二個參數exchange是Exchange的名稱,第三個參數routingKey是消息隊列和Exchange之間綁定的路由key,我們這裏綁定的路由key是“directMessage”。從Exchange過來的消息,只有routing key爲“directMessage”的消息會被轉到消息隊列“directQueue”,其他消息將不會被轉發,下面將證實這一點。

運行ProducerApp程序,使用rabbitmq_producer用戶登錄管理頁面,我們可以看到名爲“directExchange”的Direct Exchange被創建出來。



消息隊列directQueue與它綁定,routing key爲directMessage。


消息隊列directQueue裏有一條消息



我們修改ProducerApp的程序,將消息的routing key改爲“indirectMessage”

 String message = "First Indirect Message";
 channel.basicPublish("directExchange", "indirectMessage", null, message.getBytes());
 System.out.println("Send Indirect Message is:'" + message + "'");

再次運行程序後,打開管理頁面,我們看到“directQueue”隊列裏仍然只有一條消息。


我們向Exchange發送的第二條消息由於和綁定的routing key不一致,沒有被轉發到“directQueue”消息隊列,被RabbitMQ丟棄了。

我們通過管理界面再創建一個消息隊列“indirectQueue”,在它和“directExchange”之間建立bind關係,routingkey爲“indirectMessage” 。



再次運行ProducerApp程序,我們可以看到“directQueue”消息隊列消息數仍是1,但“indirectQueue”消息隊列接收到了從Exchange轉發來的消息。



使用RPC方式的消息生產/消費

RPC方式的消息生產和消費示意圖如下:


在這種方式下,生產者和消費者之間的消息發送/接收流程如下:

1)生產者在發送消息的同時,將返回消息的消息隊列名(replyTo中指定)以及消息關聯Id(correlationId)附帶在消息Properties中發送給消費者。

2)消費者在接收到消息,處理完成後,將結果作爲返回消息發送到replyTo指定的返回消息隊列中,同時附帶接收消息中的corrleationId, 以便讓生產者接收到到返回消息後,根據corrleationId確認是針對1)中發送消息的返回消息,如果correlationId確認一致,則將返回消息 取出,進行後續處理。

示意圖中的生產者和消費者在發送消息時使用的都是Default Exchange,我們接下來的程序做一點改動,使用Direct Exchange。

在我們的程序中,生產者發送一個數字給消費者,消費者接收到消息後,計算這個數字的階乘結果,返回給生產者。 生產者程序的主要代碼如下:

   //創建RPC發送消息的Direct Exchange,消息隊列和綁定關係。
   channel.exchangeDeclare("rpcSendExchange", "direct",true);
   channel.queueDeclare("rpcSendQueue", true, false, false, null);
   channel.queueBind("rpcSendQueue", "rpcSendExchange", "rpcSendMessage");
 
   //建立RPC返回消息的Direct Exchange, 消息隊列和綁定關係         
   channel.exchangeDeclare("rpcReplyExchange", "direct",true);
   channel.queueDeclare("rpcReplyQueue", true, false, false, null);
   channel.queueBind("rpcReplyQueue", "rpcReplyExchange", "rpcReplyMessage");
 
   //創建接收RPC返回消息的消費者,並將它與RPC返回消息隊列相關聯。
   QueueingConsumer replyCustomer = new QueueingConsumer(channel);
   channel.basicConsume("rpcReplyQueue", true,replyCustomer);
 
   String number = "10";
 
   //生成RPC請求消息的CorrelationId
   String correlationId = UUID.randomUUID().toString();
   //在RabbitMQ消息的Properties中設置RPC請求消息的CorrelationId以及
   //ReplyTo名稱(我們這裏使用的是Exchange名稱,
   //而不是消息隊列名稱)
   BasicProperties props = new BasicProperties
	                      .Builder()
	                      .correlationId(correlationId)
	                      .replyTo("rpcReplyExchange")
	                      .build();
 
   System.out.println("The send message's correlation id is:" + correlationId);            
   channel.basicPublish("rpcSendExchange", "rpcSendMessage", props, number.getBytes());
 
   String response = null;
 
   while(true)
   {
           //從返回消息中取一條消息
	   Delivery delivery = replyCustomer.nextDelivery();
	   //如果消息的CorrelationId與發送消息的CorrleationId一致,表示這條消息是
           //發送消息對應的返回消息,是階乘運算的計算結果。
           System.out.println("The received reply message's correlation id is:" + messageCorrelationId);
           String messageCorrelationId = delivery.getProperties().getCorrelationId();
	   if (!Strings.isNullOrEmpty(messageCorrelationId) && messageCorrelationId.equals(correlationId)) 
           {
		response = new String(delivery.getBody());
		break;
	   }
   }
 
   //輸出階乘運算結果
   if(!Strings.isNullOrEmpty(response))
   {
	System.out.println("Factorial(" + number + ") = " + response);
   }
消費者程序的主要代碼如下:
 Consumer consumer = new DefaultConsumer(channel)
 {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException
    {
       //獲取返回消息發送到的Exchange名稱
       String replyExchange = properties.getReplyTo();
 
       //設置返回消息的Properties,附帶發送消息的CorrelationId.
       AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder()
                            .correlationId(properties.getCorrelationId())
                            .build();
 
       String message = new String(body,"UTF-8");
       System.out.println("The received message is:" + message);
       System.out.println("The received message's correlation id is:" + properties.getCorrelationId());
 
       //計算階乘,factorial方法是計算階乘的方法。
       int number = Integer.parseInt(message);
       String response = factorial(number);
 
       //將階乘消息發送到Reply Exchange
       this.getChannel().basicPublish(replyExchange, "rpcReplyMessage",replyProps, response.getBytes());
   }
};
 
channel.basicConsume("rpcSendQueue", true, consumer);

先運行生產者程序,發送請求消息到Send Exchange,然後等待消費者發送的返回消息。 
再啓動消費者程序,計算階乘並返回結果給Reply Exchange。 兩個程序的控制檯信息如下圖所示

生產者程序控制臺
消費者程序控制臺

從控制檯信息可以看出生產者端根據返回消息中包含的Correlation Id判斷出這是發送消息對應的返回消息,獲取了階乘的計算結果。

這個例子只是簡單的生產者和消費者之間的方法調用,實際使用時,我們可以基於這個實例,實現更爲複雜的操作。

RabbitMQ Client的重連機制

RabbitMQ Java Client提供了重連機制,不過在RabbitMQ Java Client 4.0版本之前,自動重連默認是關閉的。從Rabbit Client 4.0版本開始,自動重連默認是打開的。控制自動重連的屬性是com.rabbitmq.client.ConnectionFactory類的automaticRecovery和topologyRecovery屬性。

設置automaticRecovery屬性爲true時,會執行以下recovery:

1)Connection的重連。

2)偵聽Connection的Listener的恢復。

3)重新建立在Connection基礎上的Channel。

4)偵聽Channel的Listener的恢復。

5)Channel上的設置,如basicQos,publisher confirm以及事務屬性等的恢復。

當設置topologyRecovery屬性爲true時,會執行以下recovery:

1)exchange的重新定義(不包含預定義的exchange)

2)queue的重新定義(不包含預定義的queue)

3)binding的重新定義(不包含預定義的binding)

4)所有Consumer的恢復

我們定義一個帶auto recovery的消費者程序,我們使用RabbitMQ Java Client 4.0.0版本,這個版本引入了AutorecoveringConnection和

AutorecoveringChannel類,可以添加RecoveryListener對Recovery過程進行監控。

public class RecoveryConsumerApp
{
    public static void main( String[] args ) throws IOException, TimeoutException {
            ConnectionFactory connectionFactory = new ConnectionFactory();
            ...................
 
            AutorecoveringConnection connection = (AutorecoveringConnection)connectionFactory.newConnection();
            String originalLocalAddress =
                    connection.getLocalAddress() + ":" + connection.getLocalPort();
            System.out.println("The origin connection's local address is:" + originalLocalAddress);
 
            AutorecoveringChannel  channel = (AutorecoveringChannel)connection.createChannel();
            System.out.println("The origin channel's channel number is:" + channel.getChannelNumber());
 
            channel.exchangeDeclare("recoveryExchange", BuiltinExchangeType.DIRECT, false, true ,null);
            channel.queueDeclare("recoveryQueue", false, false, true,null);
            channel.queueBind("recoveryQueue", "recoveryExchange", "recoveryMessage");
 
            connection.addRecoveryListener(new RecoveryListener() {
                public void handleRecovery(Recoverable recoverable) {
                    System.out.println("Connection handleRecovery method is called");
                    AutorecoveringConnection recoveredConnection =
                            (AutorecoveringConnection)recoverable;
                    String recoveredLocalAddress =
                            recoveredConnection.getLocalAddress() + ":" + recoveredConnection.getLocalPort();
                    System.out.println("The recovered connection's local address is:" + recoveredLocalAddress);
                }
 
                public void handleRecoveryStarted(Recoverable recoverable) {
                    System.out.println("Connection handleRecoveryStarted method is called");
                }
            });
 
            channel.addRecoveryListener(new RecoveryListener() {
                    public void handleRecovery(Recoverable recoverable) {
                        System.out.println("Channel handleRecovery method is called");
                        AutorecoveringChannel recoveryChannel =
                                (AutorecoveringChannel)recoverable;
                        System.out.println("The recovered Channel's number is:" + recoveryChannel.getChannelNumber());
                    }
 
                    public void handleRecoveryStarted(Recoverable recoverable) {
                        System.out.println("Channel handleRecoveryStarted method is called");
                    }
            });
 
    }
}

這個程序中Exchange, Queue都是非持久化並且自動刪除的。 我們爲Connection和Channel分別添加了Recovery Listener匿名對象,

便於確認他們確實進行了Recovery操作。

啓動程序後,我們可以看到recoveryExchange和recoveryQueue都被創建出來,且Binding關係建立了。


連接的本地地址是0.0.0.0:8109,Channel編號是1

此時我們關閉RabbitMQ服務器,再重啓RabbitMQ服務器,我們可以從控制檯界面看到有連接超時的警告信

息以及重連信息。



從重連日誌信息中我們可以看出Channel的編號還是1,但是Connection的本地地址已經變成了0.0.0.0:8470,證明進行了重連。

連接到recoveryQueue隊列上的Consumer Tag也進行了恢復,而且Consumer Tag與之前的Consumer Tag一致,這是因爲設置了

topologyRecovery屬性爲true。


我們再在生產者程序中使用重連機制,依然使用Rabbit Java Client 4.0版本 生產者程序的片段如下:

  factory.setAutomaticRecoveryEnabled(true);
   factory.setNetworkRecoveryInterval(60000);
   factory.setTopologyRecoveryEnabled(true);
 
   AutorecoveringConnection connection = (AutorecoveringConnection)factory.newConnection();
   AutorecoveringChannel channel = (AutorecoveringChannel)connection.createChannel();   
   //設置Channel爲Publish Confirm模式
   channel.confirmSelect();    


登錄管理界面,我們可以看到生產者建立的Channel是Confirm模式(圖中Mode列用C表示)


我們關掉RabbitMQ服務器,再重啓RabbitMQ服務器,可以看到生產者Channel被恢復,但是本地端口號已經從13684變成了13874,

說明這是重新創建的Channel,創建的Channel仍然是Confirm模式,和最初的Channel一致。


如果我們設置Channel爲Transaction模式(調用Channel.txSelect()方法),重連後恢復的Channel的模式也仍然是Transaction模式。

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