2018-06-14: Java 訊息佇列之 RabbitMQ 使用

lynn_發表於2018-06-14

---------------------------- BEGIN ---------------------------------

1、訊息(Message): 是指在應用間傳送的資料。訊息可以非常簡單,比如只包含文字字串,也可以更復雜,可能包含嵌入物件。

2、訊息佇列(Message Queue):是一種應用間的通訊方式,訊息傳送後可以立即返回,由訊息系統來確保訊息的可靠傳遞。訊息釋出者只管把訊息釋出到 MQ 中而不用管誰來取,訊息使用者只管從 MQ 中取訊息而不管是誰釋出的。這樣釋出者和使用者都不用知道對方的存在。

3、AMQP :Advanced Message Queue,高階訊息佇列協議。 它是應用層協議的一個開放標準,為面向訊息的中介軟體設計,基於此協議的客戶端與訊息中介軟體可傳遞訊息,並不受產品、開發語言等條件的限制。

4、RabbitMQ :是一個由 Erlang 語言開發的 AMQP 的開源實現.

5、Exchange 型別:Exchange分發訊息時根據型別的不同分發策略有區別,目前共四種型別:direct、fanout、topic、headers 。headers 匹配 AMQP 訊息的 header而不是路由鍵,此外 headers 交換器和 direct交換器完全一致,但效能差很多,目前幾乎用不到了。

6、RabbitMQ 安裝:一般來說安裝 RabbitMQ 之前要安裝 Erlang ,可以去Erlang官網下載。接著去RabbitMQ官網下載安裝包,之後解壓縮即可。根據作業系統不同官網提供了相應的安裝說明:Windows、Debian / Ubuntu、RPM-based Linux、Mac;具體安裝百度

7、Java 客戶端訪問: a、maven工程的pom檔案中新增依賴

<!-- RabbitMQ依賴  -->
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>4.1.0</version>
</dependency>
複製程式碼

b.1、新建一個 抽象 rabbitmq連線通道 類:ConnectionChannel

package com.aaa.bbb.ccc.ddd;

import java.io.IOException;
import com.goldpac.config.JGroupsConfig;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
* @Function : 抽象 rabbitmq連線通道 類
* @Author & @Date : lynn_ - 2018年06月14日    
*/
public abstract class ConnectionChannel {

    //安裝 RabbitMQ 的主機IP
    protected static final String HOST = "127.0.0.1";
    protected static final String USER = "test";
    protected static final String PASSWORD = "test";

    //訊息服務請求URL
    public static final String CMS_HOST = "http://" + HOST + ":8080";

    // routingKey 
    public static final String ROUTING_KEY = "YOU.SELF.KEY";

    // 連線
    protected Connection connection;
    // 連線通道
    protected Channel channel;
    // 連線通道路由地址
    protected String routingKey;

    // 交換機名稱
    protected final static String EXCHANGE_NAME = "cms";

    // 構造方法; 接收一個路由地址引數
    public ConnectionChannel(String routingKey) throws Exception {
	this.routingKey = routingKey;

	// 建立一個連線工廠 connection factory
	ConnectionFactory factory = new ConnectionFactory();

	// 設定rabbitmq-server服務IP地址、使用者名稱、密碼、埠
	factory.setHost(HOST);
	factory.setUsername(USER);
	factory.setPassword(PASSWORD);
	factory.setPort(5672); //預設埠
	factory.setVirtualHost("/");

	// 宣告一個連線
	connection = factory.newConnection();

	// 宣告訊息通道
	channel = connection.createChannel();

	/*
	 * 宣告轉發器 - 定義一個交換機 引數1:交換機名稱 引數2:交換機型別 引數3:交換機永續性,如果為true則伺服器重啟時不會丟失
	 * 引數4:交換機在不被使用時是否刪除 引數5:交換機的其他屬性
	 */
	channel.exchangeDeclare(EXCHANGE_NAME, "topic", false, false, null);
    }

    /**
    * 關閉channel和connection; 非必須,因為隱含是自動呼叫的。
    * @throws IOException
     */
    public void close() throws Exception {
	this.channel.close();
	this.connection.close();
    }
}
複製程式碼

b.2、新建一個 訊息生產傳送 類:Sender

package com.aaa.bbb.ccc.ddd;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** 
* @Function :  傳送訊息 - 生產者
* @Author & @Date : lynn_  - 2018年06月14日 
*/
public class Sender extends ConnectionChannel {

    //日誌列印 - 所有logger.info()
    private final Logger logger = LoggerFactory.getLogger(getClass());

    //持久化 佇列 名稱
    private String queueName;

    /**
     * Creates a new instance of Sender
     * @param routingKey
     * @throws Exception
     */
    public Sender(String routingKey) throws Exception {
	super(routingKey);
	this.queueName = "queue_topic";
    }

    public Sender(String routingKey, String queueName) throws Exception {
	super(routingKey);
	this.queueName = queueName;
    }

    /**
     * @Title : sendMessage
     * @Function: 往轉發器[交換機]上傳送訊息 
     * @param byte[]
     * @throws Exception 
     */
    public void sendMessage(byte[] bodys) throws Exception{
	
        //宣告一個佇列 - 持久化  
        channel.queueDeclare(queueName, true, false, false, null);  
    
        //設定通道預取計數
        channel.basicQos(1); 
    
        //將訊息佇列繫結到Exchange  
        channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
	
	/**
	 * 傳送訊息到佇列中
	 * 引數1:交換機exchange名字,若為空則使用預設的exchange[]
	 * 引數2:routing key - 路由地址
	 * 引數3:其他的屬性
	 * 引數4:訊息體
	 * RabbitMQ預設有一個exchange,叫default exchange,它用一個空字串表示,它是direct exchange型別,
	 * 任何發往這個exchange的訊息都會被路由到routing key的名字對應的佇列上,如果沒有對應的佇列,則訊息會被丟棄
	 */
        channel.basicPublish(EXCHANGE_NAME, routingKey, null, bodys);
        logger.info("PDM訊息傳送成功 -- [ " + EXCHANGE_NAME + " ] - " + routingKey);
    }
}
複製程式碼

b.3、新建一個 接收訊息 - 消費者 類:Receiver

package com.aaa.bbb.ccc.ddd;

import com.rabbitmq.client.QueueingConsumer;

/** 
* @Function :  接收訊息 - 消費者
* @Author & @Date : lynn_  - 2018年06月14日 
*/
@SuppressWarnings("deprecation")
public class Receiver extends ConnectionChannel {

    private String queueName;
 
    /**
    * Creates a new instance of Receiver
    * @param routingKey
    * @throws Exception
    */
    public Receiver(String routingKey) throws Exception {
	super(routingKey);
	this.queueName = "queue_topic";
    }

    public Receiver(String routingKey, String queueName) throws Exception {
	super(routingKey);
	this.queueName = queueName;
    }

   /**
    * @Title : getMessage
    * @Function: 從 交換機 上獲取訊息 
    * @throws Exception 
    */
    public void getMessage() throws Exception{
	
        //宣告一個臨時佇列,該佇列會在使用完比後自動銷燬 - 非必需
        queueName = channel.queueDeclare().getQueue();
	
        // - 宣告要關注的佇列 - 非必需
        //channel.queueDeclare(queueName, true, false, false, null);  
	
        //server push訊息時的佇列長度 - 同一時刻伺服器只會發一條訊息給消費者  - 非必需
        channel.basicQos(1); 	
    
        //將訊息佇列繫結到Exchange - 將佇列繫結到交換機 - 繫結一個routing key
	channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
	
	//宣告消費者 - 用來快取伺服器推送過來的訊息
	QueueingConsumer consumer = new QueueingConsumer(channel);
    
        /*
        * 監聽佇列,手動返回完成  - 為channel宣告一個consumer,伺服器會推送訊息
        * 引數1:持久化佇列名稱
        * 引數2:是否傳送ack包,不傳送ack訊息會持續在服務端儲存,直到收到ack。  可以通過channel.basicAck手動回覆ack
        * 引數3:消費者
        */
        channel.basicConsume(queueName, false, consumer);
    
        //wait for the next message delivery and return it
        while (true) {
    	
    	    //獲取訊息,如果沒有訊息,這一步將會一直阻塞
    	    QueueingConsumer.Delivery delivery = consumer.nextDelivery(); 
    	
    	    //獲取訊息主體資料
    	    byte[] bytes = delivery.getBody();
    	
    	    //路由地址
    	    String routingKey = delivery.getEnvelope().getRoutingKey();
    	
    	    //對訊息主體進行處理
            //我這裡新建了一個專用於處理訊息的類ReceiverHandler -- 後面給出程式碼
    	    ReceiverHandler.HandleMessage(bytes, routingKey);
    	
    	    //確認訊息已經收到 - 回覆ack包,如果不回覆,訊息不會在伺服器刪除
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}
複製程式碼

b.4、現在開始測試:...

1)、測試訊息傳送類 Test_Send

package package com.aaa.bbb.ccc.ddd.test;

import java.util.HashMap;
import java.util.Map;
import com.alibaba.fastjson.JSON;
import com.goldpac.ito.pdm.api.plm._rabbitmq.Sender;
import com.goldpac.ito.pdm.api.plm.vo.CardNodeObject;

/** 
* @Function :  測試 往轉發器[交換機]上傳送訊息 
* @Author & @Date : lynn_  - 2018年06月14日 
*/

public class Test_Send {

    public static void main(String[] args) {
        run();
    }

    void run(){
        //傳送訊息資料物件類 - 傳送訊息到轉發器[交換機]佇列中的訊息資料載體
        final CardNodeObject object = new CardNodeObject();
        byte[] bodys = null;
        //資料準備
        object.setMessage("success");
        object.setType("1");
        Map<String, Object> m = new HashMap<String, Object>();
        m.put("params1", "123456789");
        m.put("params1", "987654321");
        m.put("params1", "備註說明");
        object.setData(m);

        try {
	    //將資料物件object 轉換成JSON字串格式
	    String jsonStr = JSON.toJSONString(object);
	    System.out.println("PDM生產者傳送訊息!" + "    ---   '" + jsonStr+"'");
	
	    //用 UTF-16LE 解碼字串 - C#中的 Unicode 編碼對應 JAVA  中的是 UTF-16LE 格式
	    bodys = jsonStr.getBytes("UTF-16LE");
	
	    //例項化一個 傳送訊息 的生產者; 傳入 需要繫結的routingKey
	    Sender sender = new Sender("YOU.SELF.KEY");
	    sender.sendMessage(bodys);
	
        } catch (Exception e1) {
	    e1.printStackTrace();
        }
    }
}
複製程式碼

2)、測試 接收交換機推送的訊息類 Test_Rec

package package com.aaa.bbb.ccc.ddd.test;

import com.goldpac.ito.pdm.api.plm._rabbitmq.Receiver;

/** 
* @Function :  測試 接收 交換機推送的訊息 
* @Author & @Date : lynn_  - 2018年06月14日 
*/
public class Test_Rec {

    public static void main(String[] args) {
        run();
    }

    void run(){
	try {
		//傳入 監聽的routingKey - 
		Receiver receiver = new Receiver("YOU.SELF.KEY");
		receiver.getMessage();
	} catch (Exception e) {
		e.printStackTrace();
	}
}
複製程式碼

}

b.5、附上 CardNodeObject 類...

package com.aaa.bbb.ccc.ddd;

import java.io.Serializable;

/** 
* @Function :  訊息佇列 - 傳送訊息資料物件類 - 傳送訊息到轉發器[交換機]佇列中的訊息資料載體
* @Author & @Date : lynn_  - 2018年06月14日 
*/
public class CardNodeObject implements Serializable{

    private static final long serialVersionUID = 3439055380741158411L;

    /**
    * 訊息型別
    */
    private String type;

    /**
    * 訊息提示資料 default : success
    */
    private String message;

    /**
    * 附帶的資料物件{} default : null
    */
    private Object data;

    public String getType() {
        return type;
    }

    public void setType(String type) {
	this.type = type;
    }

    public String getMessage() {
	return message;
    }

    public void setMessage(String message) {
	this.message = message;
    }

    public Object getData() {
	return data;
    }

    public void setData(Object data) {
	this.data = data;
    }
}
複製程式碼

b.5、附上 b.3 中呼叫的 ReceiverHandler 類...

package ...;

/**
* @Function : 接收訊息 - 消費者 - 資料處理
* @Author & @Date : lynn_ - 2018年06月14日
*/
public class ReceiverHandler {

//新增日誌列印 - 所有logger.info()
private final static Logger logger = LoggerFactory.getLogger(ReceiverHandler.class.getName());

/**
 * @Title : HandleMessage
 * @Function: 消費者接收伺服器推送的訊息後,對訊息主體進行處理
 * @param bytes
 *            - 訊息主體, 位元組陣列
 * @param routingKey
 *            - 接收訊息通道的路由鍵
 * @throws UnsupportedEncodingException
 */
public static void HandleMessage(byte[] bytes, String routingKey) throws UnsupportedEncodingException {
	if (null == bytes || bytes.length == 0) {
	    logger.error("message (delivery.getBody() - bytes) is null or length == 0");
	    return;
	}
	logger.info(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " - 接收到稿樣卡款訊息 routingKey:" + routingKey);
	
	// 將訊息主體轉成字串格式; C#中的 Unicode 編碼對應 JAVA 中的是 UTF-16LE 格式
	String msg = new String(bytes, "UTF-16LE");
	// 根據 routingKey, 呼叫不同的處理方法
	if (null != routingKey && StringUtils.isNotBlank(msg)) {

		// 根據需要做具體處理...
		
	}
}
複製程式碼

}

---------------------------- END ---------------------------------

相關文章