項目的後台要求在更改密碼後發送郵件通知使用者,為了避免發送郵件時程式對使用者操作的阻塞,之前中文版中使用了線程來發送郵件,而在英文版中,我決定使用
JMS來非同步發送郵件,讓使用者更改密碼的操作和發送郵件的操作更進一步解耦,也在實際環境中試試JMS。
我們的環境是Spring 2.5, Tomcat 5.5,使用ActiveMQ
來
實現JMS傳送和接收。
首先,我們在Spring中加入ActiveMQ
Broker的配置:
<bean id="broker"
class="org.apache.activemq
.xbean.BrokerFactoryBean">
<property name="config"
value="classpath:activemq
.xml"
/>
<property name="start"
value="true" />
</bean>
我們在此處配置了BrokerFactoryBean,此Bean實現在Spring中配置嵌入式Broker,並且支援XBean方式的配
置。Broker的設定檔由config屬性指定,此處定義設定檔位於classpath中的activemq
.xml。
接下來我們需要建立Broker的設定檔activemq
.xml。其實我們
不需要從頭配置,展開ActiveMQ
的jar包,在org.apache.activemq
.xbean下,就有一個activemq
.xml,
我們將其拷貝到WEB-INF/classes/目錄下,並進行修改。
下面是activemq
.xml的內容:
<beans>
<bean
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
/>
<broker useJmx="false"
persistent="false"
xmlns="http://activemq
.apache.org/schema/core">
<transportconnectors>
<transportconnector uri="tcp://localhost:61636" />
</transportconnectors>
<networkconnectors></networkconnectors>
</broker>
</beans>
在broker中,我們指定了不開啟JMX,並且不使用持久化(persistent=”false”)。
如果不對訊息進行持久化儲存,在容器或者JVM關閉、重啟,或者崩潰後,所有的訊息都將丟失,在我們的業務中,對於發送密碼更改通知訊息,並
非是重要的功能,所以我們選擇不使用持久化儲存,但對於不同商務邏輯,可能會需要進行持久化儲存。ActiveMQ
提
供的持久化儲存方案可以將訊息儲存到檔案系統、資料庫等。
要在Broker中開啟持久化儲存,需要設定persistent為true,並且對其子節點persistenceAdapter,
journaledJDBC進行配置。ActiveMQ
jar包中的activemq
.xml有被注釋掉的樣本,可以參考。
接著我們在Spring中配置JMS Connection Factory。
<bean id="jmsFactory"
class="org.apache.activemq
.ActiveMQConnectionFactory">
<property name="brokerURL"
value="tcp://localhost:61636" />
</bean>
注意其中的borkerURL,應該是你在activemq
.xml中
transportconnector節點的uri屬性,這表示JMS Server的監聽地址。
配置訊息發送目的地:
<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>
在JMS中,目的地有兩種:主題(topic)和隊列(queue)。兩者的區別是:當一個主題目的地中被放入了一個訊息後,所有的訂閱者都
會收到通知;而對於隊列,僅有一個“訂閱者”會收到這個訊息,隊列中的訊息一旦被處理,就不會存在於隊列中。顯然,對於郵件發送程式來說,使用隊列才是正
確的選擇,而使用主題時,可能會發送多封相同的郵件。
Topic和Queue只是JMS內部以及其處理方式的不同,對於訊息發送方和接收方來說,完全沒有代碼上的區別。
配置Spring中訊息發送的JMS Template:
<bean id="producerJmsTemplate"
class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory">
<bean
class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory"
ref="jmsFactory" />
</bean>
</property>
<property name="defaultDestination"
ref="queueDestination" />
<property name="messageConverter"
ref="userMessageConverter" />
</bean>
注意此處的defaultDestination使用的是基於Queue的目的地。
在實際的訊息發送中,郵件內容需要用到User.username, User.password, User.email,
User.fullname,顯示如果我們直接發送User對象到訊息佇列,接收的時候也能直接取出User對象,那麼在郵件發送程式中操作就會方便許
多,所以在些處,我們定義了messageConverter屬性,他指定了發送訊息時使用的訊息轉換bean,這樣,在直接發送User到JMS隊列
時,Spring會自動幫我們進行轉換,下面是Converter的配置和代碼:
<bean id="userMessageConverter"
class="com.tiandinet.jms.sample.UserMessageConverter" />
此轉換器同樣也會使用在訊息接收中,將接收到的訊息轉換為User對象。
package com.tiandinet.jms.sample;<br />import javax.jms.JMSException;<br />import javax.jms.Message;<br />import javax.jms.ObjectMessage;<br />import javax.jms.Session;<br />import org.apache.activemq.command.ActiveMQObjectMessage;<br />import org.apache.commons.logging.Log;<br />import org.apache.commons.logging.LogFactory;<br />import org.springframework.jms.support.converter.MessageConverter;<br />import com.tiandinet.jms.sample.User;<br />/**<br />* Converte User message.<br />*<br />* @author Yangtze<br />*/<br />public class UserMessageConverter implements MessageConverter {<br /> private static transient Log logger = LogFactory.getLog(UserMessageConverter.class);<br /> /**<br /> * {@inheritDoc}<br /> *<br /> * @see org.springframework.jms.support.converter.MessageConverter<br /> * #fromMessage(javax.jms.Message)<br /> */<br /> public Object fromMessage(Message message) throws JMSException {<br /> if (logger.isDebugEnabled()) {<br /> logger.debug("Receive JMS message: " + message);<br /> }<br /> if (message instanceof ObjectMessage) {<br /> ObjectMessage oMsg = (ObjectMessage) message;<br /> if (oMsg instanceof ActiveMQObjectMessage) {<br /> ActiveMQObjectMessage aMsg = (ActiveMQObjectMessage) oMsg;<br /> try {<br /> User user = (User) aMsg.getObject();<br /> return user;<br /> } catch (Exception e) {<br /> logger.error("Message:[" + message + "] is not a instance of User.");<br /> throw new JMSException("Message:[" + message + "] is not a instance of User.");<br /> }<br /> } else {<br /> logger.error("Message:[" + message + "] is not "<br /> + "a instance of ActiveMQObjectMessage[User].");<br /> throw new JMSException("Message:[" + message + "] is not "<br /> + "a instance of ActiveMQObjectMessage[User].");<br /> }<br /> } else {<br /> logger.error("Message:[" + message + "] is not a instance of ObjectMessage.");<br /> throw new JMSException("Message:[" + message + "] is not a instance of ObjectMessage.");<br /> }<br /> }<br /> /**<br /> * {@inheritDoc}<br /> *<br /> * @see org.springframework.jms.support.converter.MessageConverter#toMessage(java.lang.Object,<br /> * javax.jms.Session)<br /> */<br /> public Message toMessage(Object obj, Session session) throws JMSException {<br /> if (logger.isDebugEnabled()) {<br /> logger.debug("Convert User object to JMS message: " + obj);<br /> }<br /> if (obj instanceof User) {<br /> ActiveMQObjectMessage msg = (ActiveMQObjectMessage) session.createObjectMessage();<br /> msg.setObject((User) obj);<br /> return msg;<br /> } else {<br /> logger.error("Object:[" + obj + "] is not a instance of User.");<br /> throw new JMSException("Object:[" + obj + "] is not a instance of User.");<br /> }<br /> }<br />}
此程式實現了MessageConverter介面,並實現其中的fromMessage和toMessage方法,分別實現轉換接收到的消
息為User對象和轉換User對象到訊息。
我們在程式中使用的是ActiveMQObjectMessage,它是ActiveMQ
中
對javax.jms.ObjectMessage的一個實現。
此時,我們已經完成了JMS Connection Factory和用於發送JMS訊息的JMS
Template配置,接下來,應該編寫發送訊息的Bean了,代碼如下:
package com.tiandinet.jms.sample;<br />import org.springframework.jms.core.JmsTemplate;<br />import com.tiandinet.jms.sample.User;<br />/**<br />* Send user's login information mail via JMS.<br />*<br />* @author Yangtze<br />*/<br />public class UserMessageProducerImpl implements IUserMessageProducer {<br /> private JmsTemplate jmsTemplate;<br /> /**<br /> * {@inheritDoc}<br /> *<br /> * @see com.tiandinet.jms.sample.IUserMessageProducer<br /> * #sendUserLoginInformationMail(com.tiandinet.jms.sample.User)<br /> */<br /> public void sendUserLoginInformationMail(User user) {<br /> getJmsTemplate().convertAndSend(user);<br /> }<br /> /**<br /> * Return the jmsTemplate.<br /> *<br /> * @return the jmsTemplate<br /> */<br /> public final JmsTemplate getJmsTemplate() {<br /> return jmsTemplate;<br /> }<br /> /**<br /> * Set the jmsTemplate.<br /> *<br /> * @param jmsTemplate<br /> * the jmsTemplate to set<br /> */<br /> public final void setJmsTemplate(JmsTemplate jmsTemplate) {<br /> this.jmsTemplate = jmsTemplate;<br /> }<br />}
代碼很簡單,sendUserLoginInformationMail方法是唯一我們需要編寫的,調用JMSTemplate的
convertAndSend方法,Spring會自己調用我們之前配置的converter來轉換我們發送的User對象。
將此Java在Spring中進行配置,然後在Controller中進行調用即可實現發送User對象到JMS。
到此為止,我們已經實現了訊息的發送,現在我們來實現訊息的接收。
相對於發送,訊息的接收的配置要相對簡短些,我們使用MDP(Message Drive
POJO)來實現訊息的非同步接收。我們需要實現javax.jms.MessageListener介面的void onMessage(Message
message)方法來接收訊息。不過我們可以使用Spring中提供的MessageListenerAdapter來簡化接收訊息的代碼。
我們先寫處理訊息的介面和實作類別:
package com.tiandinet.jms.sample;<br />import javax.jms.JMSException;<br />import javax.jms.ObjectMessage;<br />import com.tiandinet.jms.sample.User;<br />/**<br />* JMS message handler.<br />*<br />* Yangtze<br />*/<br />public interface IMessageConsumer {<br /> /**<br /> * Handle the user message.<br /> *<br /> * @param user<br /> * User<br /> * @throws JMSException<br /> * exception<br /> */<br /> void handleMessage(User user) throws JMSException;<br />}<br />package com.tiandinet.jms.sample;<br />import javax.jms.JMSException;<br />import org.apache.commons.logging.Log;<br />import org.apache.commons.logging.LogFactory;<br />import com.tiandinet.jms.sample.User;<br />import com.tiandinet.jms.sample.IMailService;<br />/**<br />* JMS message handler - for User Message.<br />*<br />* Yangtze<br />*/<br />public class UserMessageConsumerImpl implements IMessageConsumer {<br /> private static transient Log logger = LogFactory.getLog(UserMessageConsumerImpl.class);<br /> private IMailService mailService;<br /> /**<br /> * {@inheritDoc}<br /> *<br /> * @see com.tiandinet.jms.sample.IMessageConsumer<br />* #handleMessage(com.tiandinet.jms.sample.User)<br /> */<br /> public void handleMessage(User user) throws JMSException {<br /> if (logger.isDebugEnabled()) {<br /> logger.debug("Receive a User object from ActiveMQ: " + user.toString());<br /> }<br /> mailService.sendUserLoginInforMail(user);<br /> }<br />}
配置message listener
<bean id="messageListener"
class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
<constructor -arg>
<bean
class="com.tiandinet.jms.sample.UserMessageConsumerImpl">
<property name="mailService"
ref="mailService" />
</bean>
</constructor>
<property name="defaultListenerMethod"
value="handleMessage" />
<property name="messageConverter"
ref="userMessageConverter" />
</bean>
其中的mailService即是我們的郵件發送類,其sendUserLoginInforMail方法實現了郵件發送的功能。
訊息偵聽適配器defaultListenerMethod屬性指定Spring在收到訊息後調用的方法,此處為
handleMessage,Spring會根據收到的訊息User對象,調用handleMessage(User user)方法。
配置訊息偵聽容器,並指定我們定義的訊息接聽程式。
<bean id="listenerContainer"
class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="concurrentConsumers"
value="5" />
<property name="connectionFactory"
ref="jmsFactory" />
<property name="destination"
ref="queueDestination" />
<property name="messageListener"
ref="messageListener" />
</bean>