JMS和ActiveMQ學習筆記

[1]
在介紹ActiveMQ之前,首先簡要介紹一下JMS規範。
JMS的簡介:
(1)
JMS(Java Message Service,Java消息服務)是一組Java應用程序接口(Java API),它提供創建、發送、接收、讀取消息的服務。JMS 使您能夠通過消息收發服務從一個 JMS 客戶機向另一個 JML 客戶機交流消息。

JMS是一種與廠商無關的 API,用來訪問消息收發系統。它類似於 JDBC (Java Database Connectivity):這裏,JDBC 是可以用來訪問許多不同關係數據庫的 API,而 JMS 則提供同樣與廠商無關的訪問方法,以訪問消息收發服務。許多廠商目前都支持 JMS,包括 IBM 的 MQSeries、BEA的 Weblogic JMS service和 Progress 的 SonicMQ.

(2)
JMS典型的應用場景:
操作可異步執行.
發email了, 發msn消息了.
或者一些比較耗時的操作, 比如要在某目錄下生成一個大報表. 操作者把指令發出去就完事.

[2]
JMS的基本構件:
(1)
Broker
什麼是Broker呢?可以把JMS Brokers 看成是服務器端。這個服務器可以獨立運行.也可以隨着其他容器
以內嵌方式雲心,如下配置:
使用顯示的Java代碼創建
BrokerService broker = new BrokerService();
// configure the broker
broker.addConnector("tcp://localhost:61616");
broker.start();
使用BrokerFacotry創建
BrokerService broker = BrokerFactory.getInstance().createBroker(someURI);
使用Spring Bean創建
<bean id=”broker” class=”org.apache.activemq.xbean.BrokerFactoryBean”>
<property name=”config” value=”classpath:org/apache/activemq/xbean/activemq.xml” />
<property name=”start” value=”true” />
</bean>
還可以使用XBean或Spring 2.0等多種配置方式配置,
通過ActiveMQConnectionFactory還可以隱含的創建內嵌的broker,這個broker就不是一個獨立的服務了。
<bean id=”jmsTemplate” class=”org.springframework.jms.core.JmsTemplate”>
<property name=”connectionFactory” ref=”jmsFactory”/>
<property name=”defaultDestination” ref=”destination” />
<property name=”destinationResolver” ref=”默認是DynamicDestionResolver” />
<property name=”pubSubDomain”><value>true or false默認是false,
false是QueneDestination, true是TopicDestination</value>
</bean>
上面的defaultDestination是指默認發送和接收的目的地,我們也可以不指定,而是通過目的地名稱讓jmsTemplate自動幫我們創建.

(2)
1 連接工廠
連接工廠是客戶用來創建連接的對象,例如ActiveMQ提供的ActiveMQConnectionFactory。
2 連接
JMS Connection封裝了客戶與JMS提供者之間的一個虛擬的連接。
3 會話
JMS Session是生產和消費消息的一個單線程上下文。會話用於創建消息生產者(producer)、消息消費者(consumer)和消息(message)等。會話提供了一個事務性的上下文,在這個上下文中,一組發送和接收被組合到了一個原子操作中。
(3)
目的地:
目的地是客戶用來指定它生產的消息的目標和它消費的消息的來源的對象。JMS1.0.2規範中定義了兩種
消息傳遞域:Point-to-Point消息(P2P),點對點;發佈訂閱消息(Publish Subscribe messaging,簡稱Pub/Sub)
兩者的區別:
P2P消息模型是在點對點之間傳遞消息時使用。如果應用程序開發者希望每一條消息都能夠被處理,那麼應該使用P2P消息模型。與Pub/Sub消息模型不同,P2P消息總是能夠被傳送到指定的位置。
P2P消息,每個消息只能有一個消費者。
  Pub/Sub模型在一到多的消息廣播時使用。如果一定程度的消息傳遞的不可靠性可以被接受的話,那麼應用程序開發者也可以使用Pub/Sub消息模型。換句話說,它適用於所有的消息消費程序並不要求能夠收到所有的信息或者消息消費程序並不想接收到任何消息的情況。
Pub/Sub,每個消息可以有多個消費者。
在點對點消息傳遞域中,目的地被成爲隊列(queue);在發佈/訂閱消息傳遞域中,目的地被成爲主題(topic)。
(3)
3.1
消息生產者
消息生產者是由會話創建的一個對象,用於把消息發送到一個目的地。
3.2
消息消費者
消息消費者是由會話創建的一個對象,它用於接收發送到目的地的消息。消息的消費可以採用以下兩種
方法之一:
? 異步消費。客戶可以爲消費者註冊一個消息監聽器,以定義在消息到達時所採取的動作。(異步操作)
? 同步消費。通過調用消費者的receive方法從目的地中顯式提取消息。receive方法可以一直阻塞到消息到達。
3.3
消息是 JMS 中的一種類型對象,由兩部分組成:報頭和消息主體。報頭由路由信息以及有關該消息的元數據組成。消息主體則攜帶着應用程序的數據或有效負載。根據有效負載的類型來劃分,可以將消息分爲幾種類型,它們分別攜帶:
簡單文本 (TextMessage)、可序列化的對象 (ObjectMessage)、屬性集合
(MapMessage)、字節流 (BytesMessage)、原始值流 (StreamMessage),還有無有效負載的消息 (Message)。
(4)
JMS定義了從0到9的優先級路線級別,0是最低的優先級而9則是最高的。更特殊的是0到4是正常優先級的變化幅度,而5到9是加快的優先級的變化幅度。

[3]
ActiveMQ簡介:
ActiveMQ 是開源的JMS實現,Geronimo應用服務器就是使用的ActiveMQ提供JMS服務。
安裝
在http://activemq.apache.org/download.html 下載5.0.0發行包,解壓即可,
啓動
window環境運行解壓目錄下的/bin/activemq.bat
測試
ActiveMQ默認使用的TCP連接端口是61616, 通過查看該端口的信息可以測試ActiveMQ是否成功啓動
window環境運行 netstat -an|find "61616"
監控
ActiveMQ5.0版本默認啓動時,啓動了內置的jetty服務器,提供一個demo應用和用於監控ActiveMQ的admin應用。
admin:http://127.0.0.1:8161/admin/
demo:http://127.0.0.1:8161/demo/
點擊demo應用中的“ Market data publisher ”,就會發一些測試的消息。轉到admin頁面的topics menu下面(queue和topic的區別見 http://andyao.javaeye.com/blog/153173 ),可以看到消息在增長。
ActiveMQ5.0的配置文件在解壓目錄下的/conf目錄下面。主要配置文件爲activemq.xml.

[4]
實例一:(沒有結合spring框架)
public class QueueProducer {
/*
* 創建的簡圖
ConnectionFactory---->Connection--->Session--->Message
Destination + Session------------------------------------>Producer
Destination + Session------------------------------------>MessageConsumer
*/
public static void main(String[] args) {
// ConnectionFactory :連接工廠,JMS 用它創建連接
ConnectionFactory connectionFactory;
// Connection :JMS 客戶端到JMS Provider 的連接
Connection connection = null;
// Session: 一個發送或接收消息的線程
Session session;
// Destination :消息的目的地;消息發送給誰.
Queue queue;
//設置回覆的目的地
Queue replyQueue;
// MessageProducer:消息發送者
MessageProducer producer;
MessageConsumer replyer;
connectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,
"tcp://192.168.1.191:61616");
try {
// 構造從工廠得到連接對象
connection = connectionFactory.createConnection();
// 啓動
connection.start();
// 獲取操作連接
session = connection.createSession(Boolean.TRUE,Session.AUTO_ACKNOWLEDGE);
// 創建隊列: 可以在http://localhost:8161/admin/queue.jsp中看到
queue=new ActiveMQQueue("jason.queue2");
replyQueue=new ActiveMQQueue("jason.replyQueue");
// 得到消息生成者【發送者】:需要由Session和Destination來創建
producer = session.createProducer(queue);
// 創建消息
TextMessage message = session.createTextMessage("jason學習ActiveMq 發送的消息");
//在消息中設置回覆的目的地,
//對方用MessageProducer sender=session.createProducer(message.getJMSReplyTo());創建回覆者
message.setJMSReplyTo(replyQueue);
// 發送一個non-Persistent的消息
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
producer.send(message);
// 發送一個Persistent的消息
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
producer.send(session.createTextMessage("這是一個Persistent的消息!重啓JMS,仍可獲取"));
System.out.println("發送消息:jason學習ActiveMq 發送的消息");
System.out.println("這是一個Persistent的消息!重啓JMS,仍可獲取");

//用回覆的目的地定義回覆接收者,且設置偵聽
replyer=session.createConsumer(replyQueue);
replyer.setMessageListener
(
new MessageListener()
{
public void onMessage(Message message)
{
try {
TextMessage txtmess = (TextMessage) message;
System.out.println("consumer的回覆內容是: "+txtmess.getText());
} catch (Exception e) {
e.printStackTrace();
}
}
}
);
session.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != connection)
connection.close();
} catch (Throwable ignore) {
}
}
}
}
接收者:
//public class Receiver {
public class QueueConsumer implements MessageListener{
public static void main(String[] args)
{
QueueConsumer re=new QueueConsumer();
//循環只是爲了讓程序每2秒進行一次連接偵聽是否有消息可以獲取.
while(true)
{
re.consumeMessage();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//對於主動接收的,只須直接執行:re.consumeMessage();即可.
//其中的while(true),會一次性將所有的消息獲取過來.
}

public void consumeMessage()
{
ConnectionFactory connectionFactory;
Connection connection = null;
Session session;
Queue queue;
MessageConsumer consumer;

connectionFactory = new ActiveMQConnectionFactory(
ActiveMQConnection.DEFAULT_USER,
ActiveMQConnection.DEFAULT_PASSWORD,
"tcp://192.168.1.191:61616");
try {
connection = connectionFactory.createConnection();
connection.start();
session = connection.createSession(Boolean.FALSE,
Session.AUTO_ACKNOWLEDGE);
queue=new ActiveMQQueue("jason.queue2");
consumer = session.createConsumer(queue);

// 接受消息方式一:主動的去接受消息,用consumer.receive
//只能獲取一條消息 -->不採用
// TextMessage message = (TextMessage) consumer.receive(1000);
// if (null != message) {
// System.out.println("收到消息" + message.getText());
// }
//可以不斷循環,獲取所有的消息.--->關鍵.
// while (true) {
// TextMessage message = (TextMessage) consumer.receive(1000);
// if (null != message) {
// System.out.println("收到消息" + message.getText());
// } else {
// break; //沒有消息時,退出
// }
// }

/*接受消息方式二:基於消息監聽的機制,需要實現MessageListener接口,這個接口有個onMessage方法,當接受到消息的時候會自動調用這個函數對消息進行處理。
*/
consumer.setMessageListener(this);


} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != connection)
connection.close();
} catch (Throwable ignore) {
}
}
}

public void onMessage(Message message)
{
try {
if (message instanceof TextMessage) {
TextMessage txtmess = (TextMessage) message;
System.out.println("收到的消息是:" + txtmess.getText());
//回覆發送者
MessageProducer sender=session.createProducer(message.getJMSReplyTo());
sender.send(session.createTextMessage("已收到你的消息"));
}
else
System.out.println("收到的消息是:" + message);
} catch (Exception e) {
}
}
}
說明:
(2)
VM Transport
VM transport允許在VM內部通信,從而避免了網絡傳輸的開銷。這時候採用的連接不是socket連接,而是直接地方法調用。第一個創建VM 連接的客戶會啓動一個embed VM broker,接下來所有使用相同的broker name的VM連接都會使用這個broker。當這個broker上所有的連接都關閉的時候,這個broker也會自動關閉。
TCP Transport
TCP transport 允許客戶端通過TCP socket連接到遠程的broker。以下是配置語法:
tcp://hostname:port?transportOptions
tcp://localhost:61616
(3)
3.1
啓動activeMQ後,用戶創建的queues會被保存在activeMQ解壓目錄下的\data\kr-store\data中.
3.2
創建queue,可以在代碼中創建,也可以直接進入http://localhost:8161/admin--->點擊queue--->在上面的field中填下你要創建的queue名-->點擊創建即可.
3.3
若用戶創建的queue,不是持久化的,則在重啓activeMQ後,數據文件中的內容會被清空,但文件仍存在.
(4)
messageProducer發送消息後,會在保存在目的地,即上面的queue中,也即就是在\data\kr-store\data目錄下的文件中.
messageReceiver(連接到相同目的地的接收者),不需要立即接收.只要activeMQ的服務端不關閉,當運行接收者,連接到activeMQ的服務端時,就可以獲取activeMQ服務端上已發送的消息.
發送/接收的消息情況及數量及消息的內容與處理(刪除),可以在
http://localhost:8161/admin/queue.jsp中查看,操作.
(5)
可以將activeMQ的服務端放於一PC中,發送者位於另一PC,接收者也位於另一PC中.
只要:tcp://activeMQ的服務端IP:activeMQ的服務端口,進行連接即可.
(6)
queue消息,只被消費一次.

topic的實例(無結合spring)
public class TopicTest {
public static void main(String[] args) throws Exception {
ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("vm://localhost");
Connection connection = factory.createConnection();
connection.start();
//創建一個Topic
Topic topic= new ActiveMQTopic("testTopic");
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//註冊消費者1
MessageConsumer comsumer1 = session.createConsumer(topic);
comsumer1.setMessageListener(new MessageListener(){
public void onMessage(Message m) {
try {
System.out.println("Consumer1 get " + ((TextMessage)m).getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
//註冊消費者2
MessageConsumer comsumer2 = session.createConsumer(topic);
comsumer2.setMessageListener(new MessageListener(){
public void onMessage(Message m) {
try {
System.out.println("Consumer2 get " + ((TextMessage)m).getText());
} catch (JMSException e) {
e.printStackTrace();
}
}
});
//創建一個生產者,然後發送多個消息。
MessageProducer producer = session.createProducer(topic);
for(int i=0; i<10; i++){
producer.send(session.createTextMessage("Message:" + i));
}
}
}
輸出如下:
Consumer1 get Message:0
Consumer2 get Message:0
Consumer1 get Message:1
Consumer2 get Message:1
Consumer1 get Message:2
Consumer2 get Message:2
.............................

實例二:
異步的電子郵件(將topic的應用實例也放在一起)
(4.1)
背景說明:  
以傳統的方式發送電子郵件(作爲同步請求的一部分)會引起一些問題。首先,連接到電子郵件服務器需要一次網絡往返,速度可能會很慢,尤其是在服務器非常繁忙的時候。過載的電子郵件服務器甚至可以使依賴於電子郵件的服務暫時不可用。
xa 事務支持
另一個顯而易見的問題是,電子郵件服務器通常在本質上是非事務性的。當事務被回滾時,這可以導致出現不一致的通知——把一條消息放入隊列之後不能取消它。幸運的是, jms 支持事務,而且可以通過把消息的發送延遲到提交底層事務的時候來解決這個問題。
(4.2)
實現效果:
用戶在更改密碼後,系統會發送郵件通知用戶,爲了避免發送郵件時程序對用戶操作的阻塞,可以用JMS異步發送郵件.
(4.3)
實現流程:
當用戶更改密碼後
--1->調用JMS的發送者,發送者會利用jmsTemplate發送消息到目的地
--2->系統,執行原系統的程序.
--2->當消息發送後,messageListener偵聽到消息,接收後執行相應的方法handleMessage().在此方法中執行發送email.
好處:
從而實現了異步發送郵件,避免用戶等待單一線程完成,使原程序的執行更快,提升用戶體驗
<1>
第一步:
創建broker,jmsFactory,destination,messageConverter,jmsTemplate(用於發送JMS消息).
然後將jmsFactory,Destination,MessageConverter放入jmsTemplate中.
(1)
首先,我們在Spring中加入ActiveMQ Broker的配置:
1.
刪除原來jason-servlet.xml中的
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
改爲:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:amq="http://activemq.org/config/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://activemq.org/config/1.0
http://activemq.apache.org/schema/core/activemq-core-5.0.0.xsd">
同時,要導入相應的jar包即可.
2.
<!-- 在Spring中配置嵌入式的 activemq broker,這樣就不用在使用JMS時,要手動啓動activeMQ broker -->
<amq:broker useJmx="false" persistent="false">
<amq:transportConnectors>
<amq:transportConnector uri="tcp://localhost:61616" />
</amq:transportConnectors>
</amq:broker>
<!-- 消息的存儲機制, 服務器重啓也不會丟失消息.
<amq:broker useJmx="false" persistent="true">
<amq:persistenceAdapter>
<amq:amqPersistenceAdapter directory="d:/amq"/>
</amq:persistenceAdapter>
<amq:transportConnectors>
<amq:transportConnector uri="tcp://localhost:61616" />
<amq:transportConnector uri="vm://localhost:0" />
</amq:transportConnectors>
</amq:broker>
-->
(2)
在Spring中配置JMS Connection Factory。
<bean id="jmsFactory2"
class="org.apache.activemq.ActiveMQConnectionFactory">
<property name="brokerURL" value="tcp://localhost:61616" />
</bean>
也可直接用:
<amq:connectionFactory id="jmsFactory2" brokerURL="tcp://localhost:61616" />
注意其中的borkerURL,應該是你在activemq.xml中transportconnector節點的uri屬性,這表示JMS Server的監聽地址。activeMQ默認端口是61616,由於採用默認方式,所以這裏也是61616.
同時,要運行程序之前,我們要先啓動broker,即啓動解壓目錄下的/bin/activemq.bat.
(3)
配置消息發送目的地:
<bean id="topicDestination"
class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg value="MY.topic" />
</bean>
<bean id="queueDestination"
class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg value="MY.queue" />
</bean>
也可直接用:
<amq:topic name="topicDestination" physicalName="MY.topic"/>
<amq:queue name="queueDestination" physicalName="MY.queue"/>
在 JMS中,目的地有兩種:主題(topic)和隊列(queue)。兩者的區別是:當一個主題目的地中被放入了一個消息後,所有的訂閱者都會收到通知;而 對於隊列,僅有一個“訂閱者”會收到這個消息,隊列中的消息一旦被處理,就不會存在於隊列中。顯然,對於郵件發送程序來說,使用隊列纔是正確的選擇,而使 用主題時,可能會發送多封相同的郵件。
(4)
配置Spring中消息發送的JMS Template:
(與hibernate相似,其配置的是hibernateTemplate.都要將連接工廠放到template中)
<bean id="producerJmsTemplate"
class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory">
<bean class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory"
ref="jmsFactory2" />
</bean>
</property>
<!-- 因爲要實現此模板同時用於queue與topic,所以目的地要放於發送者中.若單獨使用,放於此,更方便!
<property name="defaultDestination" ref="queueDestination" />
-->
<property name="messageConverter" ref="userMessageConverter" />
</bean>
(5)
在實際的消息發送中,郵件內容需要用到User.username, User.password, User.email, User.fullname,我們定義了messageConverter,在發送信息時,將user對象轉換成消息,在接收消息時,會將消息轉換成 User對象.只要在上面的jmstemplate中設置了messageConverter屬性,發送/接收消息時,Spring會自動幫我們進行轉 換,下面是Converter的配置和代碼:
<bean id="userMessageConverter"
class="com.laoer.bbscs.jms.UserMessageConverter" />
代碼如下:
public class UserMessageConverter implements MessageConverter {
private static transient Log logger = LogFactory.getLog(UserMessageConverter.class);
public Object fromMessage(Message message) throws JMSException {
if (logger.isDebugEnabled()) {
logger.debug("Receive JMS message: " + message);
}
if (message instanceof ObjectMessage) {
ObjectMessage oMsg = (ObjectMessage) message;
if (oMsg instanceof ActiveMQObjectMessage) {
ActiveMQObjectMessage aMsg = (ActiveMQObjectMessage) oMsg;
try {
PersonInfo personInfo = (PersonInfo) aMsg.getObject();
return personInfo;
} catch (Exception e) {
logger.error("Message:[" + message + "] is not a instance of personInfo.");
throw new JMSException("Message:[" + message + "] is not a instance of personInfo.");
}
} else {
logger.error("Message:[" + message + "] is not " + "a instance of ActiveMQObjectMessage[personInfo].");
throw new JMSException("Message:[" + message + "] is not " + "a instance of ActiveMQObjectMessage[personInfo].");
}
} else {
logger.error("Message:[" + message + "] is not a instance of ObjectMessage.");
throw new JMSException("Message:[" + message + "] is not a instance of ObjectMessage.");
}
}

public Message toMessage(Object obj, Session session) throws JMSException {
if (logger.isDebugEnabled()) {
logger.debug("Convert User object to JMS message: " + obj);
}
if (obj instanceof PersonInfo) {
ActiveMQObjectMessage msg = (ActiveMQObjectMessage) session.createObjectMessage();
msg.setObject((PersonInfo) obj);
return msg;
} else {
logger.error("Object:[" + obj + "] is not a instance of PersonInfo.");
throw new JMSException("Object:[" + obj + "] is not a instance of PersonInfo.");
}
}
}
此程序實現了MessageConverter接口,並實現其中的fromMessage和toMessage方法,分別實現轉換接收到的消息爲User對象和轉換User對象到消息。
我們在程序中使用的是ActiveMQObjectMessage,它是ActiveMQ中對javax.jms.ObjectMessage的一個實現。
<2>
第二步:
配置發送消息:
(1)
在spring配置文件中配置發送者:
<!-- 發送queue消息 -->
<bean id="userMessageProducer" class="com.laoer.bbscs.jms.UserMessageProducer">
<property name="jmsTemplate" ref="producerJmsTemplate" />
<property name="defaultDestination" ref="queueDestination" />
</bean>
<!-- 發送topic消息 -->
<bean id="topicMessageProducer" class="com.laoer.bbscs.jms.TopicMessageProducer">
<property name="jmsTemplate" ref="producerJmsTemplate" />
<property name="defaultDestination" ref="topicDestination" />
</bean>
由於發送者是用jmsTemplate進行發送的,所以有注入jmsTemplate.
(2)
代碼如下:
package com.laoer.bbscs.jms;
public class UserMessageProducer {
private JmsTemplate jmsTemplate;
//因爲"目的地"屬性放於jmsTemplate中,則不用添加此屬性.
private Queue defaultDestination;
public void sendUserLoginInformationMail(PersonInfo personInfo) {
// 若"目的地"屬性放於jmsTemplate中,則用此方式
// getJmsTemplate().convertAndSend(personInfo);
getJmsTemplate().convertAndSend(this.defaultDestination,personInfo);
}
getter,setter略...
}
其中,sendUserLoginInformationMail方法是唯一我們需要編寫的,調用JMSTemplate的convertAndSend方法,Spring會自己調用我們之前配置的converter來轉換我們發送的User對象,再進行發送。
public class TopicMessageProducer {
private JmsTemplate jmsTemplate;
private Topic defaultDestination;
public void sendTopicMessage(PersonInfo personInfo) {
getJmsTemplate().convertAndSend(this.defaultDestination,personInfo);
}
getter,setter略...
}

<3>
實現消息的接收
我們使用MDP(Message Drive POJO)來實現消息的異步接收。
我們需要實現javax.jms.MessageListener接口的void onMessage(Message message)方法來接收消息。
不過我們可以使用Spring中提供的MessageListenerAdapter來簡化接收消息的代碼。
(0)
<!-- 定義消息消費者,然後直接在messageListener中調用.
消費者,不用加入jmsTemplate屬性,jmsTemplate只用於發送消息 -->
<!-- queue消息消費者,只能一個 -->
<bean id="userMessageConsumer" class="com.laoer.bbscs.jms.UserMessageConsumer">
<property name="mailSender" ref="mailSender" />
</bean>
<!-- topic消息消費者,可以多個 -->
<bean id="topicConsumerA" class="com.laoer.bbscs.jms.TopicConsumerA" />
<bean id="topicConsumerB" class="com.laoer.bbscs.jms.TopicConsumerB" />
(1)
配置messageListener,用它來接收消息.(用jmsTemplate來發送消息)
<!-- 定義queue,topic(A/B consumer)各自的偵聽器 -->
<bean id="messageListener"
class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg ref="userMessageConsumer"/>
<property name="defaultListenerMethod" value="handleMessage" />
<property name="messageConverter" ref="userMessageConverter" />
</bean>
<bean id="topicListenerA"
class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg ref="topicConsumerA"/>
<property name="defaultListenerMethod" value="receiveA" />
<property name="messageConverter" ref="userMessageConverter" />
</bean>
<bean id="topicListenerB"
class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor-arg ref="topicConsumerB"/>
<property name="defaultListenerMethod" value="receiveB" />
<property name="messageConverter" ref="userMessageConverter" />
</bean>
其中的mailSender:即是我們的郵件發送類,此類中的方法send()方法實現了郵件發送的功能。
消息偵聽適配器defaultListenerMethod屬性:指定Spring在收到消息後調用的方法,此處爲handleMessage,Spring會根據收到的消息--轉換爲User對象-->調用handleMessage()方法。
(2)
配置消息偵聽容器,並指定我們定義的消息偵聽器。
<!-- 定義queue,topic(A/B consumer)各自的偵聽容器 -->
<bean id="listenerContainer"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="concurrentConsumers" value="5" />
<property name="connectionFactory" ref="jmsFactory2" />
<property name="destination" ref="queueDestination" />
<property name="messageListener" ref="messageListener" />
</bean>
<bean id="topicListenerContainerA"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="concurrentConsumers" value="5" />
<property name="connectionFactory" ref="jmsFactory2" />
<property name="destination" ref="topicDestination" />
<property name="messageListener" ref="topicListenerA" />
</bean>
<bean id="topicListenerContainerB"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="concurrentConsumers" value="5" />
<property name="connectionFactory" ref="jmsFactory2" />
<property name="destination" ref="topicDestination" />
<property name="messageListener" ref="topicListenerB" />
</bean>
配置了同時偵聽的個數,連接工廠(發送者,接收者都要連接到同一地方),目的地(與上面對應),自定義的偵聽器.
(3)
接收者的代碼:
package com.laoer.bbscs.jms;
public class UserMessageConsumer {
private static transient Log logger = LogFactory.getLog(UserMessageConsumer.class);
private MailSender mailSender;
public void handleMessage(PersonInfo personInfo) throws JMSException {
if (logger.isDebugEnabled()) {
logger.debug("Receive a User object from ActiveMQ: " + personInfo.toString());
}
mailSender.send(personInfo, "h***[email protected]");
}
getter,setter略....
}

public class TopicConsumerA {
public void receiveA(PersonInfo personInfo) throws JMSException
{
System.out.println("TopicConsumerA收到TopicProducer的消息---->personInfo的用戶名是:"+personInfo.getName());
}
}
TopicConsumerB與TopicConsumerA相似.

(4)
發送郵件的相應方法:
public String send(PersonInfo personinfo,String mailAddr)
{
System.out.println("現在的時間是: "+System.currentTimeMillis());
// 不要使用SimpleEmail,會出現亂碼問題
HtmlEmail email = new HtmlEmail();
try{
// 這裏是發送服務器的名字
email.setHostName("smtp.sohu.com");
// 編碼集的設置
email.setCharset("gbk");
// 收件人的郵箱
// email.addTo("h***[email protected]");
email.addTo(mailAddr);
// 發送人的郵箱
email.setFrom("sh****[email protected]", "she**fa");
// 如果需要認證信息的話,設置認證:用戶名-密碼。分別爲發件人在郵件
服務器上的註冊名稱和密碼
email.setAuthentication("she**fa","20***23");
email.setSubject("測試email與JMS--你的密碼修改了");
// 要發送的信息
email.setMsg("你現在的用戶名是:"+personinfo.getName()+" \n密碼
是:"+personinfo.getPassword());
// 發送
email.send();
System.out.println("現在的時間是: "+System.currentTimeMillis());
return "發送成功!";
} catch (EmailException e) {
return "發送失敗!";
}
}
(5)
在jLogin.java中調用即可(也添加相應的producer及setter,getter):
//用ActiveMQ的queue,正常發email
this.getMailSender().send(pi, hw***@126.com");
this.getUserMessageProducer().sendUserLoginInformationMail(pi);
//用ActiveMQ的topic發佈消息
this.getTopicMessageProducer().sendTopicMessage(pi);

這樣,當執行後,會異步發送郵件,同時topicProducer發送一個消息後,topicConsumerA/B都會偵聽接收到,且執行相應的操作.
[6]
實例二:
整合ActiveMQ,Quartz,實現定時發送郵件:
(1)
activeMQ的配置同上;
(2)
如何將ActiveMQ整合到Quartz中去:
1.用非繼承的方法實現quartz有任務類.
將任務類的目標類,目標方法:指定爲消息發送類,及其發送消息的方法即可.
<!-- 利用Quartz定時分email -->
<bean id="mailJobDetailBean"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject">
<ref local="userMessageProducer"/> -->指定發送者
</property>
<property name="targetMethod"> -->指定發送方法,不同於上面
<value>send2</value>
</property>
</bean>
public void send2()
{
PersonInfo personInfo=new PersonInfo();
personInfo.setName("jason");
personInfo.setPassword("123456789");
this.getJmsTemplate().convertAndSend(this.defaultDestination,personInfo);
}
2.創建時間表.
<bean d="mailCronTriggerBean"
class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail">
<ref local="mailJobDetailBean"/>
</property>
<property name="cronExpression">
<value>30 * * * * ?</value>
</property>
</bean>
3.放入任務工廠.
<bean id="schedulerFactoryBean"
class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTriggerBean"/>
<ref bean="jasonGetPersonCronTriggerBean"/>
<ref bean="mailCronTriggerBean"/>
</list>
</property>
</bean>


1 DestinationResolver
DestinationResolver接口的作用是將指定的目的地名解析爲目的地實例。其定義如下:
Java代碼
1. public interface DestinationResolver {
2. Destination resolveDestinationName(Session session, String destinationName,
3. boolean pubSubDomain) throws JMSException;
4. }
參數pubSubDomain用於指定是使用“發佈/訂閱”模式(解析後的目的地是Topic),還是使用“點對點”模式(解析後的目的地是Queue)。

CachingDestinationResolver接口繼承了DestinationResolver,增加了緩存的功能,其接口定義如下:
Java代碼
1. public interface CachingDestinationResolver extends DestinationResolver {
2. void removeFromCache(String destinationName);
3. void clearCache();
4. }
在目的地失效的時候,removeFromCache方法會被調用;在JMS provider失效的時候,clearCache方法會被調用。

1.1 DynamicDestinationResolver
DynamicDestinationResolver實現了DestinationResolver接口。根據指定的目的地名,DynamicDestinationResolver會動態創建目的地實例。針對JMS1.1規範,它採用如下方法創建目的地:
Java代碼
1. session.createTopic(topicName)
2. session.createQueue(queueName);

1.2 JndiDestinationResolver
JndiDestinationResolver繼承自JndiLocatorSupport, 同時實現了CachingDestinationResolver接口。如果在JMS provider中配置了靜態目的地,那麼JndiDestinationResolver通過JNDI查找的方式獲得目的地實例。

JndiDestinationResolver的fallbackToDynamicDestination屬性用於指定在JNDI查找失敗後,是否使 用動態目的地,默認值是false。JndiDestinationResolver的cache屬性用於指定是否對目的地實例進行緩存,默認值是 true。

1.3 BeanFactoryDestinationResolver
BeanFactoryDestinationResolver實現了DestinationResolver接口和BeanFactoryAware接口。它會根據指定的目的地名從BeanFactory中查找目的地實例。以下是相關的代碼:
Java代碼
1. public Destination resolveDestinationName(Session session, String destinationName,
2. boolean pubSubDomain) throws JMSException {
3. Assert.state(this.beanFactory != null, "BeanFactory is required");
4. try {
5. return (Destination) this.beanFactory.getBean(destinationName, Destination.class);
6. }
7. catch (BeansException ex) {
8. throw new DestinationResolutionException(
9. "Failed to look up Destinaton bean with name '" + destinationName + "'", ex);
10. }
11. }

2 JmsAccessor
抽象類JmsAccessor是JmsTemplate、SimpleMessageListenerContainer和 DefaultMessageListenerContainer等concrete class的基類。JmsAccessor定義瞭如下幾個用於訪問JMS服務的共通屬性。
Java代碼
1. private ConnectionFactory connectionFactory;
2. private boolean sessionTransacted = false;
3. private int sessionAcknowledgeMode = Session.AUTO_ACKNOWLEDGE;

JmsAccessor提供了創建Connection和Session的方法,如下:
Java代碼
1. protected Connection createConnection() throws JMSException {
2. return getConnectionFactory().createConnection();
3. }
4.
5. protected Session createSession(Connection con) throws JMSException {
6. return con.createSession(isSessionTransacted(), getSessionAcknowledgeMode());
7. }

2.1 JmsDestinationAccessor
抽象類JmsDestinationAccessor繼承自JmsAccessor,增加了destinationResolver和 pubSubDomain屬性。destinationResolver的默認值是DynamicDestinationResolver的實例,也就是 說默認採用動態目的地解析的方式;pubSubDomain用於指定是使用“發佈/訂閱”模式還是使用“點對點”模式,默認值是false。

JmsDestinationAccessor提供了用於解析目的地的方法,如下:
Java代碼
1. protected Destination resolveDestinationName(Session session, String destinationName)
2. throws JMSException {
3. return getDestinationResolver().resolveDestinationName(session, destinationName,
4. isPubSubDomain());
5. }

2.2 AbstractJmsListeningContainer
AbstractJmsListeningContainer繼承自JmsDestinationAccessor,作爲所有Message Listener Container的公共基類。它主要提供了JMS connection的生命週期管理的功能,但是沒有對消息接收的方式(主動接收方式或者異步接收方式)等做任何假定。該類主要的屬性如下:
Java代碼
1. private String clientId;
2. private Connection sharedConnection;
clientId通常用於持久訂閱;sharedConnection保存了被共享的JMS connection。

該類定義瞭如下的抽象方法,以便子類可以決定是否使用共享的JMS connection。
Java代碼
1. protected abstract boolean sharedConnectionEnabled();

2.3 AbstractMessageListenerContainer
AbstractMessageListenerContainer繼承自AbstractJmsListeningContainer,也是作爲所有Message Listener Container的公共基類。該類主要的屬性如下:
Java代碼
1. private volatile Object destination;
2. private volatile Object messageListener;
3. private boolean exposeListenerSession = true;
destination用於指定接收消息的目的地。
messageListener用於指定處理消息的listener。對於messageListener,它既可以是符合JMS規範的 javax.jms.MessageListener,也可以是Spring特有的 org.springframework.jms.listener.SessionAwareMessageListener。 SessionAwareMessageListener的定義如下:
Java代碼
1. public interface SessionAwareMessageListener {
2. void onMessage(Message message, Session session) throws JMSException;
3. }
跟javax.jms.MessageListener相比,這個接口的onMessage方法增加了一個Session 類型的參數,可以通過這個session發送回覆消息(reply message)。

如果使用了SessionAwareMessageListener 類型的message listener,那麼exposeListenerSession參數指定了傳入onMessage方法的session參數是否是創建了 MessageConsumer的session,默認值是true。如果是false,那麼 AbstractMessageListenerContainer會在connection上新建一個session,並傳入onMessage方法。

2.4 AbstractPollingMessageListenerContainer
AbstractPollingMessageListenerContainer繼承自AbstractMessageListenerContainer,它提供了對於主動接收消息(polling)的支持,以及支持外部的事務管理。
Java代碼
1. private boolean pubSubNoLocal = false;
2. private long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT;
3. private PlatformTransactionManager transactionManager;
如果使用“發佈/訂閱”模式,那麼pubSubNoLocal 屬性指定通過某個連接發送到某個Topic的消息,是否應該被投遞迴這個連接。

receiveTimeout屬性用於指定調用MessageConsumer的receive方法時的超時時間,默認值是1秒。需要注意的是,這個值應該比transactionManager 中指定的事務超時時間略小。

通常情況下,應該爲transactionManager設置一個 org.springframework.transaction.jta.JtaTransactionManager的實例,此外也要設置一個支持 XA的ConnectionFactory。需要注意的是,XA 事務對性能有較大的影響。
如果只是希望使用local JMS transaction,那麼只要設置sessionTransacted爲true或者使用JmsTransactionManager即可。實際上, 如果設置了非JTA的transactionManager,那麼sessionTransacted屬性會自動被設置成true。
由於local JMS transaction無法同其它local transaction(例如local database transaction)進行協調,因此客戶端程序可能需要對重發的消息進行檢查。JMS規範要求:JMS provider應該將重發消息的JMSRedelivered屬性設置爲true。

2.5 SimpleMessageListenerContainer
SimpleMessageListenerContainer繼承自AbstractMessageListenerContainer,使用異步方式 接收消息(也就是通過MessageConsumer上註冊MessageListener的方式接收消息)。該類主要的屬性如下:
Java代碼
1. private boolean pubSubNoLocal = false;
2. private int concurrentConsumers = 1;
3. private Set sessions;
4. private Set consumers;
5. private TaskExecutor taskExecutor;
如果使用“發佈/訂閱”模式,那麼pubSubNoLocal 屬性指定通過某個連接發送到某個Topic的消息,是否應該被投遞迴這個連接。

SimpleMessageListenerContainer允許創建多個Session和MessageConsumer來接收消息。具體的個數由 concurrentConsumers屬性指定。需要注意的是,應該只是在Destination爲Queue的時候才使用多個 MessageConsumer(Queue中的一個消息只能被一個Consumer接收),雖然使用多個MessageConsumer會提高消息處理 的性能,但是消息處理的順序卻得不到保證:消息被接收的順序仍然是消息發送時的順序,但是由於消息可能會被併發處理,因此消息處理的順序可能和消息發送的 順序不同。此外,不應該在Destination爲Topic的時候使用多個MessageConsumer,這是因爲多個 MessageConsumer會接收到同樣的消息。
SimpleMessageListenerContainer創建的Session和MessageConsumer分別保存在sessions和consumers屬性中。

taskExecutor屬性的默認值是null,也就是說,對MessageListener(或者 SessionAwareMessageListener)的回調是在MessageConsumer的內部線程中執行。如果指定了 taskExecutor,那麼回調是在TaskExecutor內部的線程中執行。以下是相關的代碼:
Java代碼
1. protected MessageConsumer createListenerConsumer(final Session session)
2. throws JMSException {
3. Destination destination = getDestination();
4. if (destination == null) {
5. destination = resolveDestinationName(session, getDestinationName());
6. }
7. MessageConsumer consumer = createConsumer(session, destination);
8.
9. if (this.taskExecutor != null) {
10. consumer.setMessageListener(new MessageListener() {
11. public void onMessage(final Message message) {
12. taskExecutor.execute(new Runnable() {
13. public void run() {
14. processMessage(message, session);
15. }
16. });
17. }
18. });
19. }
20. else {
21. consumer.setMessageListener(new MessageListener() {
22. public void onMessage(Message message) {
23. processMessage(message, session);
24. }
25. });
26. }
27.
28. return consumer;
29. }
需要注意的是,如果指定了taskExecutor,那麼消息在被taskExecutor內部的線程處理前,可能已經被確認過了(外層的 onMessage方法可能已經執行結束了)。因此如果使用事務Session或者Session.CLIENT_ACKNOWLEDGE類型的確認模 式,那麼可能會導致問題。

該類的sharedConnectionEnabled方法(在AbstractJmsListeningContainer中定義)總是返回true, 因此SimpleMessageListenerContainer會使用共享的JMS connection。

2.6 DefaultMessageListenerContainer
DefaultMessageListenerContainer繼承自 AbstractPollingMessageListenerContainer,主要使用同步的方式接收消息(也就是通過循環調用 MessageConsumer.receive的方式接收消息)。該類主要的屬性如下:
Java代碼
1. private int concurrentConsumers = 1;
2. private int maxConcurrentConsumers = 1;
3. private int maxMessagesPerTask = Integer.MIN_VALUE;
4. private int idleTaskExecutionLimit = 1;
5. private final Set scheduledInvokers = new HashSet();
6. private TaskExecutor taskExecutor;
7. private int cacheLevel = CACHE_AUTO;
跟SimpleMessageListenerContainer一樣,DefaultMessageListenerContainer也支持創建多個 Session和MessageConsumer來接收消息。跟SimpleMessageListenerContainer不同的 是,DefaultMessageListenerContainer創建了concurrentConsumers所指定個數的 AsyncMessageListenerInvoker(實現了SchedulingAwareRunnable接口),並交給 taskExecutor運行。

maxMessagesPerTask屬性的默認值是Integer.MIN_VALUE,但是如果設置的taskExecutor(默認值是 SimpleAsyncTaskExecutor)實現了SchedulingTaskExecutor接口並且其 prefersShortLivedTasks方法返回true(也就是說該TaskExecutor傾向於短期任務),那麼 maxMessagesPerTask屬性會自動被設置爲10。
如果maxMessagesPerTask屬性的值小於0,那麼AsyncMessageListenerInvoker.run方法會在循環中反覆嘗試 接收消息,並在接收到消息後調用MessageListener(或者SessionAwareMessageListener);如果 maxMessagesPerTask屬性的值不小於0,那麼AsyncMessageListenerInvoker.run方法裏最多會嘗試接收消息 maxMessagesPerTask次,每次接收消息的超時時間由其父類 AbstractPollingMessageListenerContainer的receiveTimeout屬性指定。如果在這些嘗試中都沒有接收 到消息,那麼AsyncMessageListenerInvoker的idleTaskExecutionCount屬性會被累加。在run方法執行完 畢前會對idleTaskExecutionCount進行檢查,如果該值超過了 DefaultMessageListenerContainer.idleTaskExecutionLimit(默認值1),那麼這個 AsyncMessageListenerInvoker可能會被銷燬。

所有AsyncMessageListenerInvoker實例都保存在scheduledInvokers中,實例的個數可以在 concurrentConsumers和maxConcurrentConsumers之間浮動。跟 SimpleMessageListenerContainer一樣,應該只是在Destination爲Queue的時候才使用多個 AsyncMessageListenerInvoker實例。

cacheLevel屬性用於指定是否對JMS資源進行緩存,可選的值是CACHE_NONE = 0、CACHE_CONNECTION = 1、CACHE_SESSION = 2、CACHE_CONSUMER = 3和CACHE_AUTO = 4。默認情況下,如果transactionManager屬性不爲null,那麼cacheLevel被自動設置爲CACHE_NONE(不進行緩 存),否則cacheLevel被自動設置爲CACHE_CONSUMER。

如果cacheLevel屬性值大於等於CACHE_CONNECTION,那麼sharedConnectionEnabled方法(在AbstractJmsListeningContainer中定義)返回true,也就是說使用共享的JMS連接。


3 SingleConnectionFactory
SingleConnectionFactory實現了ConnectionFactory接口,其createConnection方法總是返回相同的 Connection。可以在SingleConnectionFactory的構造函數中傳入Connection對象或者 ConnectionFactory對象,用來創建被代理的連接對象。 SingleConnectionFactory.createConnection方法返回的連接是個代理,它忽略了對stop和close方法的調用 (連接會在SingleConnectionFactory.destroy方法中關閉)。

SingleConnectionFactory的reconnectOnException屬性用來指定是否在連接拋出JMSException的時候,對連接進行重置,重置後如果再調用createConnection方法,那麼會返回一個新的連接。

需要注意的是,AbstractJmsListeningContainer類的抽象方法sharedConnectionEnabled指定了是否在 message listener container內部使用共享的JMS連接。因此通常情況下不需要爲單獨的message listener container設置SingleConnectionFactory(及其子類);如果希望在不同的message listener container之間共享JMS連接,那麼可以考慮使用SingleConnectionFactory。

3.1 CachingConnectionFactory
CachingConnectionFactory繼承自SingleConnectionFactory,增加了對Session和MessageProducer緩存的功能。該類主要的屬性如下:
Java代碼
1. private int sessionCacheSize = 1;
2. private boolean cacheProducers = true;
sessionCacheSize屬性指定了被緩存的Session實例的個數(默認值是1),也就是說,如果同時請求的Session個數大於sessionCacheSize,那麼這些Session不會被緩存,而是正常的被創建和銷燬。

cacheProducers屬性指定了是否對MessageProducer進行緩存,默認值是true。


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