訊息佇列之非同步訊息的基本概念以及ActiveMQ整合Spring的常用用法介紹 | 掘金技術徵文

zifangsky發表於2016-12-18

一 簡介

(1)非同步訊息:

所謂非同步訊息,跟RMI遠端呼叫、webservice呼叫是類似的,非同步訊息也是用於應用程式之間的通訊。但是它們之間的區別是:

  • RMI、Hession/Burlap、webservice等遠端呼叫機制是同步的。也就是說,當客戶端呼叫遠端方法時,客戶端必須等到遠端方法響應後才能繼續執行
  • 非同步訊息,顧名思義訊息是非同步傳送,訊息傳送者不需要等待訊息消費者處理訊息,甚至不需要等待訊息投遞完成就可以繼續傳送訊息。這是因為訊息傳送者預設訊息接收最終可以收到這條訊息並進行處理

(2)Java訊息服務(JMS):

Java訊息服務(Java Message Service,即:JMS)是Java中關於面向訊息中介軟體(MOM)的API,用於在兩個應用程式之間或分散式系統中傳送訊息,進行非同步通訊。Java訊息服務是一個與具體平臺無關的API,絕大多數MOM提供商都對JMS提供支援

JMS是Java平臺上有關面向訊息中介軟體(MOM)的技術規範,它便於訊息系統中的Java應用程式進行訊息交換,並且通過提供標準的產生、傳送、接收訊息的介面簡化企業應用的開發,其作用類似於JDBC

(3)訊息代理(message broker)和目的地(destination):

在非同步訊息中有兩個重要的概念,分別是:訊息代理和目的地

當我們在一個應用中傳送一條訊息時,會將該訊息移交給一個訊息代理(PS:一般是一些訊息中介軟體,如:ActiveMQ )。在這裡,訊息代理就類似於郵局,訊息代理可以確保訊息被投遞到指定的目的地,同時解放訊息傳送者,使其能夠繼續進行其他業務

同樣,每條非同步訊息在被訊息傳送者傳送時都要指定一個目的地(PS:用於區別不同型別的訊息),然後訊息接收者就可以根據自己的業務需求從指定的目的地(PS:訊息還是在訊息中介軟體存放,目的是是用於區別不同型別的訊息)獲取自己所需的訊息並進行處理

(4)佇列(queue)與主題(topic):

i)佇列(queue):

佇列也即是:點對點訊息模型

在點對點模型中,每條訊息分別只有一個傳送者和接收者。也就是說,當訊息代理得到傳送者傳送的訊息時,它會將該訊息放入到一個佇列中。當某一個訊息接收者(PS:同一目的地的訊息接收者可能存在多個)請求佇列中的下一條訊息時,訊息會從佇列中取出並投遞給該接收者。之後該條訊息將會從佇列中刪除,這樣就可以保證一條訊息只投遞給一個接收者

訊息佇列之非同步訊息的基本概念以及ActiveMQ整合Spring的常用用法介紹 | 掘金技術徵文
點對點訊息模型

ii)主題(topic):

主題也即是:釋出——訂閱訊息模型

在釋出——訂閱訊息模型中,每條訊息可以由多個接收者接收。也就是說,訊息不再是隻投遞給一個接收者,而是主題的所有訂閱者都會收到該訊息的副本

訊息佇列之非同步訊息的基本概念以及ActiveMQ整合Spring的常用用法介紹 | 掘金技術徵文
釋出——訂閱訊息模型

(5)非同步訊息的優點:

i)無需等待:

在同步通訊中,如果客戶端與遠端服務頻繁通訊,或者遠端服務響應很慢,就會對客戶端應用的效能帶來負面影響

當使用JMS傳送訊息時,客戶端不必等待訊息被處理,甚至是被投遞,客戶端只需要將訊息傳送給訊息代理,就可以確信訊息會被投遞給相應的目的地

因為不需要等待,所以客戶端可以繼續執行其他任務,這種方式可以有效的節省時間,客戶端的效能能夠得到極大的提高

ii)面向訊息與解耦:

在同步通訊中,客戶端通過服務介面與遠端服務相耦合,如果服務的介面發生變化,那麼此服務的所有客戶端都需要做相應的改變

當使用JMS傳送訊息時,傳送非同步訊息是以資料為中心的。這意味著客戶端並沒有與特定的方法簽名繫結,任何可以處理資料的佇列或主題訂閱者都可以處理由客戶端傳送的訊息,而客戶端不必瞭解遠端服務的任何規範

iii)位置獨立:

同步RPC服務通常需要網路地址來定位。這意味著客戶端無法靈活地適應網路拓撲的改變。如果服務的IP地址改變了,或者服務被配置為監聽其他埠,客戶端必須進行相應的調整,否則無法訪問服務。

與之相反,訊息客戶端不必知道誰會處理它們的訊息,或者服務的位置在哪裡。客戶端只需要瞭解需要通過哪個佇列或主題來傳送訊息。因此,只服務能夠從佇列或主題中獲取即可,訊息客戶端根本不需要關注服務來自哪裡

在點對點模型中,可以利用這種位置的獨立性來建立訊息服務叢集。如果客戶端不知道服務的位置,並且服務的唯一要求就是可以訪問訊息代理,那麼我們就可以配置多個服務從同一個佇列中接收訊息。如果服務過載,處理能力不足,我們只需要新增一些新的的服務(接收者)例項來監聽相同的佇列即可平滑增強其處理能力

在釋出一訂閱模型中,位置獨立性會產生另一種有趣的效應。多個服務可以訂閱同一個主題,接收相同訊息的副本。但是每一個服務對訊息的處理方式卻可能不同。例如,假設我們有一組可以共同處理描述新員工資訊的訊息。一個服務可能會在工資系統中增加該員工,另一個服務則會將新員工增加到公司交流群中,同時還有一個服務為新員工分配內網系統的訪問許可權。在這裡,每一個服務都是基於相同的資料(都是從同一個主題接收而來),但是卻各自對資料進行了不同的處理

在上面的內容中,我介紹了一些關於非同步訊息的基本概念。下面我將介紹基於ActiveMQ框架的JMS訊息的傳送與接收以及ActiveMQ在Spring框架中的一些常用用法

二 基於JMS的訊息傳送與接收

(1)ActiveMQ的下載與啟動:

在正式開始編寫測試例項之前,我們需要做的是ActiveMQ的下載。其官方下載地址是:activemq.apache.org/download.ht…

然後執行:apache-activemq-5.14.1/bin/win64/activemq.bat ,接著保持控制檯視窗不關閉,訪問:http://127.0.0.1:8161/admin/

注:預設賬號密碼是:admin/admin

訊息佇列之非同步訊息的基本概念以及ActiveMQ整合Spring的常用用法介紹 | 掘金技術徵文

此時,我們可以通過訪問選單中的“ Queues”或者“ Topics”來實時監控佇列或者主題型別的訊息使用情況

訊息佇列之非同步訊息的基本概念以及ActiveMQ整合Spring的常用用法介紹 | 掘金技術徵文

當然,此時因為沒有任何訊息存在,所以介面是空白的

(2)使用JMS傳送訊息:

package cn.zifangsky.test.base;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;

/**
 * 訊息生產者
 * @author zifangsky
 *
 */
public class JMSProducer {

    //預設連線使用者名稱
    private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;
    //預設連線密碼
    private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;
    //預設連線地址
    private static final String BROKER_URL = ActiveMQConnection.DEFAULT_BROKER_URL;

    public static void main(String[] args) {
        //連線工廠
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(USERNAME, PASSWORD, BROKER_URL);

        try {
            //連線
            Connection connection = connectionFactory.createConnection();
            //啟動連線
            connection.start();
            //建立session
            Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
            //訊息目的地
            Destination destination = session.createQueue("hello");
            //訊息生產者
            MessageProducer producer = session.createProducer(destination);

            //傳送訊息
            for(int i=0;i<10;i++){
                //建立一條文字訊息
                TextMessage message = session.createTextMessage("ActiveMQ:這是第 " + i + " 條訊息");
                //生產者傳送訊息
                producer.send(message);
            }

            session.commit();
            session.close();
            connection.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }

    }

}複製程式碼

執行上面的程式碼之後可以發現ActiveMQ的佇列監控介面出現了變化:

訊息佇列之非同步訊息的基本概念以及ActiveMQ整合Spring的常用用法介紹 | 掘金技術徵文

很顯然,生成了10條目的地為“hello”的未被消費的訊息

(3)使用JMS接收訊息:

package cn.zifangsky.test.base;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;
import org.apache.activemq.ActiveMQConnectionFactory;

/**
 * 訊息消費者
 * @author zifangsky
 *
 */
public class JMSConsumer {

    //預設連線使用者名稱
    private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;
    //預設連線密碼
    private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;
    //預設連線地址
    private static final String BROKER_URL = ActiveMQConnection.DEFAULT_BROKER_URL;

    public static void main(String[] args) {
        //連線工廠
        ConnectionFactory connectionFactory = new ActiveMQConnectionFactory(USERNAME, PASSWORD, BROKER_URL);
        try {
            //連線
            Connection connection = connectionFactory.createConnection();
            //啟動連線
            connection.start();
            //建立session
            Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            //訊息目的地
            Destination destination = session.createQueue("hello");
            //訊息消費者
            MessageConsumer consumer = session.createConsumer(destination);
            while(true){
                TextMessage message = (TextMessage) consumer.receive();
                if(message != null){
                    System.out.println("接收到訊息: " + message.getText());
                }else{
                    break;
                }
            }
            session.close();
            connection.close();
        } catch (JMSException e) {
            e.printStackTrace();
        }


    }

}複製程式碼

執行程式碼之後,輸出如下:

接收到訊息: ActiveMQ:這是第 0 條訊息
接收到訊息: ActiveMQ:這是第 1 條訊息
接收到訊息: ActiveMQ:這是第 2 條訊息
接收到訊息: ActiveMQ:這是第 3 條訊息
接收到訊息: ActiveMQ:這是第 4 條訊息
接收到訊息: ActiveMQ:這是第 5 條訊息
接收到訊息: ActiveMQ:這是第 6 條訊息
接收到訊息: ActiveMQ:這是第 7 條訊息
接收到訊息: ActiveMQ:這是第 8 條訊息
接收到訊息: ActiveMQ:這是第 9 條訊息

當然,此時觀察ActiveMQ的佇列監控介面,可以發現這10條訊息已經被消費了

注:上面的程式碼很簡單,並且其思路跟JDBC很類似,因此這裡就不做過多解釋了

三 Spring框架整合ActiveMQ的常見用法

如果寫過很多的JDBC程式碼的話,可以發現使用基本的JMS來傳送和接收訊息就跟JDBC程式碼一樣,需要每次寫很多冗長重複的程式碼。

針對如何消除冗長和重複的JMS程式碼,Spring給出的解決方案是JmsTemplate。JmsTemplate可以建立連線、獲得會話以及傳送和接收訊息。這使得我們可以專注於構建要傳送的訊息或者處理接收到的訊息

另外,JmsTemplate可以處理所有丟擲的笨拙的JMsException異常。如果在使用JmsTemplate時丟擲JMsException異常,JmsTemplate將捕獲該異常,然後丟擲一個非檢查型異常,該異常是Spring自帶的JmsException異常的子類

(1)一個簡單的佇列型別的訊息傳送和接收例項:

i)activemq.properties:

activemq.ip=127.0.0.1
activemq.username=admin
activemq.passwd=admin複製程式碼

這個檔案配置了ActiveMQ的地址以及認證的賬號密碼

ii)在Spring的配置檔案中載入上面的配置檔案:

    <bean id="configProperties"
        class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <list>
                <value>classpath:jdbc.properties</value>
                <value>classpath:activemq.properties</value>
            </list>
        </property>
    </bean>

    <bean id="propertyConfigurer"
        class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer">
            <property name="properties" ref="configProperties" />  
    </bean>複製程式碼

iii)ActiveMQ的配置檔案context_activemq.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:amq="http://activemq.apache.org/schema/core"
    xmlns:jms="http://www.springframework.org/schema/jms"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
            http://www.springframework.org/schema/context 
            http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/jms
        http://www.springframework.org/schema/jms/spring-jms-4.0.xsd
        http://activemq.apache.org/schema/core
        http://activemq.apache.org/schema/core/activemq-core-5.14.1.xsd">

    <context:component-scan base-package="cn.zifangsky.activemq" />

    <!-- ActiveMQ 連線工廠 -->
    <!-- <amq:connectionFactory id="amqConnectionFactory"
        brokerURL="tcp://${activemq.ip}:61616" userName="${activemq.username}"
        password="${activemq.passwd}" /> -->
    <bean id="amqConnectionFactory" class="org.apache.activemq.spring.ActiveMQConnectionFactory">
        <property name="brokerURL" value="tcp://${activemq.ip}:61616"/>
        <property name="userName" value="${activemq.username}" />
        <property name="password" value="${activemq.passwd}" />
        <!-- <property name="trustAllPackages" value="true"/> -->
        <property name="trustedPackages">
            <list>
                <value>java.lang</value>
                <value>javax.security</value>
                <value>java.util</value>
                <value>org.apache.activemq</value>
                <value>cn.zifangsky.activemq</value>
                <value>cn.zifangsky.model</value>
            </list>
        </property>
    </bean>

    <!-- Spring Caching連線工廠 -->
    <bean id="connectionFactory" class="org.springframework.jms.connection.CachingConnectionFactory">
        <property name="targetConnectionFactory" ref="amqConnectionFactory" />
        <!-- Session快取數量 -->
        <property name="sessionCacheSize" value="100" />
    </bean>

    <!-- 定義Queue型別的JmsTemplate -->
    <bean id="jmsQueueTemplate" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="connectionFactory" />
        <!-- 非pub/sub模型(釋出/訂閱),即:佇列模型 -->
        <property name="pubSubDomain" value="false" />
    </bean>

    <!-- 定義Queue監聽器 -->
    <jms:listener-container destination-type="queue" container-type="default" connection-factory="connectionFactory" acknowledge="auto">
        <jms:listener destination="test.queue" ref="testQueueReceiver1"/>
        <jms:listener destination="test.queue" ref="testQueueReceiver2"/>
    </jms:listener-container>

</beans>複製程式碼

當然,在web.xml中需要載入該配置檔案才行:

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:context/context.xml
            classpath:context/context_*.xml
        </param-value>
    </context-param>複製程式碼

在上面的context_activemq.xml檔案中,首先是定義了自動掃描cn.zifangsky.activemq 這個包下面的註解,在後面配置的兩個接收者:testQueueReceiver1、testQueueReceiver2 的bean就是這樣被載入進來的

接著,amqConnectionFactory這個bean配置了ActiveMQ的連線引數(PS:通過配置檔案載入進來),以及可信任的可以被序列化的類的包路徑

再往後,jmsQueueTemplate這個bean配置了一個JmsTemplate的例項,當然這裡定義的是一個佇列模型

最後,使用jms:listener-container配置了兩個訊息監聽器,其監聽的目的地都是“test.queue”,處理的接收者分別是:testQueueReceiver1 和 testQueueReceiver2

iv)訊息傳送者:

package cn.zifangsky.activemq.producer;

import javax.annotation.Resource;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;

import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.stereotype.Component;

@Component("queueSender")
public class QueueSender {

    @Resource(name="jmsQueueTemplate")
    private JmsTemplate jmsTemplate;

    /**
     * 傳送一條訊息到指定佇列
     * @param queueName  佇列名稱
     * @param message  訊息內容
     */
    public void send(String queueName,final String message){
        jmsTemplate.send(queueName, new MessageCreator() {

            @Override
            public Message createMessage(Session session) throws JMSException {
                return session.createTextMessage(message);
            }
        });
    }

}複製程式碼

從上面的程式碼可以看出,這裡僅僅只是使用JmsTemplate的send( )方法建立了一條文字訊息

v)兩個訊息接收者:

QueueReceiver1.java:

package cn.zifangsky.activemq.consumer;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

import org.springframework.stereotype.Component;

@Component("testQueueReceiver1")
public class QueueReceiver1 implements MessageListener{

    @Override
    public void onMessage(Message message) {
        try {
            System.out.println("QueueReceiver1收到訊息: " + ((TextMessage)message).getText());
        } catch (JMSException e) {
            e.printStackTrace();
        }

    }

}複製程式碼

QueueReceiver2.java:

package cn.zifangsky.activemq.consumer;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

import org.springframework.stereotype.Component;

@Component("testQueueReceiver2")
public class QueueReceiver2 implements MessageListener{

    @Override
    public void onMessage(Message message) {
        try {
            System.out.println("QueueReceiver2收到訊息: " + ((TextMessage)message).getText());
        } catch (JMSException e) {
            e.printStackTrace();
        }

    }

}複製程式碼

vi)測試:

package cn.zifangsky.test.springjms;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import cn.zifangsky.activemq.producer.QueueSender;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:/context/context.xml","classpath:/context/context_activemq.xml"})
public class TestQueue {
    private final String QUEUENAME = "test.queue";

    @Resource(name="queueSender")
    private QueueSender queueSender;

    @Test
    public void test(){
        for(int i=0;i<10;i++){
            queueSender.send(QUEUENAME, "Hi,這是第 " + (i+1) + " 條訊息!");
        }
    }

}複製程式碼

執行這個單元測試方法之後,可以發現輸出結果如下:

QueueReceiver2收到訊息: Hi,這是第 1 條訊息!
QueueReceiver1收到訊息: Hi,這是第 2 條訊息!
QueueReceiver2收到訊息: Hi,這是第 3 條訊息!
QueueReceiver1收到訊息: Hi,這是第 4 條訊息!
QueueReceiver2收到訊息: Hi,這是第 5 條訊息!
QueueReceiver1收到訊息: Hi,這是第 6 條訊息!
QueueReceiver2收到訊息: Hi,這是第 7 條訊息!
QueueReceiver1收到訊息: Hi,這是第 8 條訊息!
QueueReceiver2收到訊息: Hi,這是第 9 條訊息!
QueueReceiver1收到訊息: Hi,這是第 10 條訊息!

從上面的輸出結果可以看出,佇列型別的訊息只能被某一個接收者接收並處理

(2)簡化程式碼:

上面的例子很顯然在傳送和接收訊息的時候寫的程式碼要比純粹的JMS要少很多,那麼是不是就真的沒有更簡潔的程式碼了呢?

答案當然是否,第一是在傳送訊息的時候使用了JmsTemplate的send( ) 方法來傳送訊息。其實,除了send( )方法,JmsTemplate還提供了convertAndSend( )方法,與send( ) 方法不同的是,convertAndSend( )方法並不需要MessageCreator作為引數。這是因為convertAndSend( )方法會使用內建的訊息轉換器(message converter)為我們建立訊息

i)改寫jmsQueueTemplate這個bean新增預設的目的地:

    <!-- 定義Queue型別的JmsTemplate -->
    <bean id="jmsQueueTemplate" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="connectionFactory" />
        <!-- 非pub/sub模型(釋出/訂閱),即:佇列模型 -->
        <property name="pubSubDomain" value="false" />
        <property name="defaultDestinationName" value="test.queue"/>
    </bean>複製程式碼

ii)改寫訊息傳送者的send()方法:

改寫之後的方法如下所示:

    /**
     * 傳送一條訊息到指定佇列
     * @param message  訊息內容
     */
    public void send(final String message){
        jmsTemplate.convertAndSend(message);
    }複製程式碼

除了上面使用的內建的訊息轉換器之外,Spring還為通用的轉換任務提供了多個訊息轉換器(org.springframework.jms.support.converter包中)

訊息轉換器 功能
MappingJackson2MessageConverter 使用Jackson2 JSON庫實現訊息與JSON格式的相互轉換
MarshallingMessageConverter 使用JAXB庫實現訊息與XML格式之間的相互轉換
SimpleMessageConverter 實現String與TextMessage之間的相互轉換、位元組陣列與BytesMessage之間的相互轉換、Map與MapMessage之間的相互轉換以及Serializable物件與ObjectMessage之間的相互轉換(PS:物件的序列化與反序列化)

注:預設情況下,JmsTemplate會在convertAndSend( )方法中使用SimpleMessageConverter這個訊息轉換器。如果需要手動執行訊息轉化器的話,可以這樣修改:

    <bean id="jmsMessageConverter" class="org.springframework.jms.support.converter.MappingJackson2MessageConverter" />

    <bean id="jmsQueueTemplate2" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="connectionFactory" />
        <property name="pubSubDomain" value="false" />
        <property name="messageConverter" ref="jmsMessageConverter" />
    </bean>複製程式碼

好了轉回正題,針對第一個例項的簡化還可以做其他的工作。比如:訊息接收者在處理訊息的時候實現了一個MessageListener介面,同時複寫了onMessage(Message message) 方法。那麼我們是否可以將之簡化,改寫成一個普通的POJO呢?

i)改寫QueueReceiver1.java變成一個普通的POJO:

package cn.zifangsky.activemq.consumer;

import org.springframework.stereotype.Component;

@Component("testQueueReceiver1")
public class QueueReceiver1{

    public void handle(String str){
        System.out.println("QueueReceiver1收到訊息: " + str);
    }

}複製程式碼

從上面的程式碼可以看出,這裡僅僅只定義了一個普通的handle(String str) 方法,完全看不出來任何JMS的痕跡

ii)修改佇列訊息監聽器:

    <!-- 定義Queue監聽器 -->
    <jms:listener-container destination-type="queue" container-type="default" connection-factory="connectionFactory" acknowledge="auto">
        <jms:listener destination="test.queue" ref="testQueueReceiver1" method="handle"/>
        <jms:listener destination="test.queue" ref="testQueueReceiver2"/>
    </jms:listener-container>複製程式碼

這裡只改寫了第一個監聽相關配置,手動指定了針對接收到的訊息的處理方法。當然,Spring會自動完成訊息格式的轉化

再次執行單元測試:

    @Test
    public void test(){
        for(int i=0;i<10;i++){
            queueSender.send("Hi,這是第 " + (i+1) + " 條訊息!");
        }
    }複製程式碼

輸出略

(3)釋出——訂閱型別的訊息傳送和接收例項:

明白了上面的佇列型別的訊息傳送與接收,那麼定義一個釋出——訂閱型別的訊息就很簡單了,只需要把JmsTemplate的型別改成“pubSubDomain”型別即可:

i)context_activemq.xml檔案裡面的配置:

新增以下內容:

    <!-- 定義Topic型別的JmsTemplate -->
    <bean id="jmsTopicTemplate" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="connectionFactory" />
        <!-- pub/sub模型(釋出/訂閱) -->
        <property name="pubSubDomain" value="true" />
        <property name="defaultDestinationName" value="test.topic"/>
    </bean>

    <!-- 定義Topic監聽器 -->
    <jms:listener-container destination-type="topic" container-type="default" connection-factory="connectionFactory" acknowledge="auto">
        <jms:listener destination="test.topic" ref="testTopicReceiver1" method="handle"/>
        <jms:listener destination="test.topic" ref="testTopicReceiver2" method="handle"/>
    </jms:listener-container>複製程式碼

ii)訊息傳送者:

package cn.zifangsky.activemq.producer;

import javax.annotation.Resource;

import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;

@Component("topicSender")
public class TopicSender {

    @Resource(name="jmsTopicTemplate")
    private JmsTemplate jmsTemplate;

    /**
     * 傳送一條訊息到指定佇列
     * @param message  訊息內容
     */
    public void send(final String message){
        jmsTemplate.convertAndSend(message);
    }
}複製程式碼

iii)兩個訊息接收者:

package cn.zifangsky.activemq.consumer;

import org.springframework.stereotype.Component;

@Component("testTopicReceiver1")
public class TopicReceiver1{

    public void handle(String str){
        System.out.println("TopicReceiver1收到訊息: " + str);
    }

}複製程式碼

另一個接收者程式碼跟上面相似,請自行完成,略

iv)單元測試:

package cn.zifangsky.test.springjms;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import cn.zifangsky.activemq.producer.TopicSender;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"classpath:/context/context.xml","classpath:/context/context_activemq.xml"})
public class TestTopic {

    @Resource(name="topicSender")
    private TopicSender topicSender;

    @Test
    public void test(){
        for(int i=0;i<5;i++){
            topicSender.send("Hi,這是第 " + (i+1) + " 條訊息!");
        }
    }

}複製程式碼

輸出如下:

TopicReceiver2收到訊息: Hi,這是第 1 條訊息!
TopicReceiver1收到訊息: Hi,這是第 1 條訊息!
TopicReceiver1收到訊息: Hi,這是第 2 條訊息!
TopicReceiver1收到訊息: Hi,這是第 3 條訊息!
TopicReceiver1收到訊息: Hi,這是第 4 條訊息!
TopicReceiver2收到訊息: Hi,這是第 2 條訊息!
TopicReceiver2收到訊息: Hi,這是第 3 條訊息!
TopicReceiver2收到訊息: Hi,這是第 4 條訊息!
TopicReceiver2收到訊息: Hi,這是第 5 條訊息!
TopicReceiver1收到訊息: Hi,這是第 5 條訊息!

從上面的輸出內容可以看出,釋出——訂閱型別的訊息每個接收者都會接收到一份訊息的副本

(4)傳送和接收物件型別的訊息:

如果我們想要傳送和接收物件型別的訊息,而不是普通的文字訊息。其實,因為Spring提供了預設的訊息轉換器——SimpleMessageConverter。所以我們只需要像傳送文字訊息那樣傳送物件訊息,關於物件的序列化和反序列化這些步驟Spring會自動幫我們完成

i)修改context_activemq.xml檔案:

新增以下內容:

    <bean id="jmsQueueTemplate2" class="org.springframework.jms.core.JmsTemplate">
        <constructor-arg ref="connectionFactory" />
        <!-- 非pub/sub模型(釋出/訂閱),即:佇列模型 -->
        <property name="pubSubDomain" value="false" />
        <property name="defaultDestinationName" value="object.queue"/>
    </bean>複製程式碼

這裡,定義了新的JmsTemplate,其預設的目的地是:object.queue

修改佇列監聽器,新增:

    <!-- 定義Queue監聽器 -->
    <jms:listener-container destination-type="queue" container-type="default" connection-factory="connectionFactory" acknowledge="auto">
        ...
        <jms:listener destination="object.queue" ref="testQueueReceiver3" method="handle"/>
    </jms:listener-container>複製程式碼

ii)訊息傳送者:

package cn.zifangsky.activemq.producer;

import javax.annotation.Resource;

import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;

import cn.zifangsky.model.User;

@Component("queueSender2")
public class QueueSender2 {

    @Resource(name="jmsQueueTemplate2")
    private JmsTemplate jmsTemplate;

    /**
     * 傳送一條訊息到指定佇列
     * @param user  一個User型別的實體
     */
    public void send(final User user){
        jmsTemplate.convertAndSend(user);
    }

}複製程式碼

可以看出,這裡的方法引數就不是普通的文字了,而是一個可以被序列化的物件

注:User.java:

package cn.zifangsky.model;

import java.io.Serializable;

public class User implements Serializable{
    private static final long serialVersionUID = 1L;
    private Long id;
    private String username;
    private String password;

    public User() {

    }

    public User(Long id, String username, String password) {
        this.id = id;
        this.username = username;
        this.password = password;
    }

    (getter和setter方法略)

    @Override
    public String toString() {
        return "User [id=" + id + ", username=" + username + ", password=" + password + "]";
    }

}複製程式碼

iii)訊息接收者:

package cn.zifangsky.activemq.consumer;

import javax.annotation.Resource;

import org.springframework.stereotype.Component;

import cn.zifangsky.model.User;

@Component("testQueueReceiver3")
public class QueueReceiver3{

    public void handle(User user){
        System.out.println("接收到訊息: " + user);
    }
}複製程式碼

可以看到,這裡的handle方法的引數時User型別。當然這個User物件是Spring將訊息進行反序列化後生成的

iv)測試:

    @Resource(name="queueSender2")
    private QueueSender2 queueSender2;

    @Test
    public void testObject(){
        User u = new User((long) 1,"test","123456");

        queueSender2.send(u);
    }複製程式碼

輸出如下:

接收到訊息: User [id=1, username=test, password=123456]

可以看出,我們實際需要做的工作還是很少的,很多繁瑣的步驟都由Spring在後臺自動完成了

(5)訊息被接收到之後進行回覆:

有時,為了保障訊息的可靠性,通常需要在接收到訊息之後給某個訊息目的地傳送一條確認收到的回覆訊息。當然,要實現這個功能也很簡單,只需要在收到訊息之後呼叫某個訊息傳送者傳送一條確認訊息即可

比如上面的QueueReceiver3可以改成這樣:

package cn.zifangsky.activemq.consumer;

import javax.annotation.Resource;

import org.springframework.stereotype.Component;

import cn.zifangsky.activemq.producer.QueueSender;
import cn.zifangsky.model.User;

@Component("testQueueReceiver3")
public class QueueReceiver3{

    @Resource(name="queueSender")
    private QueueSender queueSender;

    public void handle(User user){
        System.out.println("接收到訊息: " + user);
        queueSender.send("QueueReceiver3已經收到Object型別的訊息。。。");
    }

}複製程式碼

這樣就可以在收到訊息之後,使用QueueSender 這個傳送者給“test.queue”這個訊息目的地傳送一條確認訊息了(PS:實際情況的處理可能會比這裡稍微複雜一點,這裡為了測試只是傳送了一條文字訊息)

注:使用單元測試的時候最後一條訊息可能不會列印出來,因為此次單元測試的生命週期結束之後程式就自動停止了。解決辦法可以是手動執行一下第一個例項中的那個單元測試,或者啟動這個web專案就可以看到效果了

(6)設定多個並行的訊息監聽器:

在前面介紹佇列型別的監聽器的時候為了驗證一條佇列裡的訊息只能被一個接收者接收,因此新增了兩個功能完全一樣的接收者:testQueueReceiver1和testQueueReceiver2

其實在實際開發中,為了提高系統的訊息處理能力我們完全沒必要像這樣定義多個功能一樣的訊息接收者,相反我們只需要在配置監聽器的時候使用concurrency這個屬性配置多個並行的監聽器即可。比如像這樣:

    <jms:listener-container destination-type="queue" container-type="default" connection-factory="connectionFactory" concurrency="5" acknowledge="auto">
        <jms:listener destination="test.queue" ref="testQueueReceiver1" method="handle"/>
        <jms:listener destination="object.queue" ref="testQueueReceiver3" method="handle"/>
    </jms:listener-container>複製程式碼

如果concurrency屬性設定一個固定的數字則表示每個訊息監聽器都會被同時啟動指定個數的完全一樣的並行監聽器來監聽訊息並轉發給訊息接收者處理。當然,除了指定固定的數字之外,我們還可以手動指定一個監聽器的數目區間,比如:concurrency=”3-5″ ,表示最少開啟3個監聽器,最多開啟5個監聽器。訊息少時會少開啟幾個,訊息多時會多開啟幾個,這一過程會自動完成而不需要我們做其他額外的工作

基於ActiveMQ的非同步訊息的一些常用用法基本上就是這些了。當然,還有一些其他的內容在這裡沒有介紹到,比如:匯出基於JMS的服務、使用AMQP實現訊息功能等。限於文章篇幅,這些內容暫且讓我放在其他文章單獨介紹吧

參考:

相關文章