對jms中Durable Subscription的一種理解

1. Durable Subscription釋義(What's Durable Subscription?)
2. 如何進行Durable Subscription(Durable Subscription How to)
2.1. Client Id
2.2. Subscriber Name
2.3. 小結
過了近乎2個星期的倒班生活,現在都有點兒“恍如隔世”的感覺了,再加上老李臨時給了個東西做,更是嚴重的“顛倒黑白”, 否則,這篇文字早就應該出來了,趁着今天可以偷懶,就在工作時間了結它吧!(別告我狀啊,呵呵,偷偷di進村,打槍di不要...)

1. Durable Subscription[1]釋義(What's Durable Subscription?)
對於什麼是Point-to-Point(P2P)和Publish/Subscribe(Pub/Sub),我就不用廢話了,大家應該比我都清楚, 我就直接說Durable Subscription了。

對於通常的消息訂閱來說, JMS Provider會對這類消息訂閱者“一視同仁”,你來了,我就給你消息,你走了,我就不管你了。 當消息到達指定Topic之後,JMS Provider只會爲已經連接並且訂閱了該指定Topic的消息訂閱者發送消息, 如果消息到達之後你恰好不在,那不好意思,你將接收不到這一消息。這就好像現在的商場促銷活動,禮品(消息)有限,雖然你(相當於消息訂閱者)也想獲得相應的禮品, 但當發送禮品的時候你不在禮品派發現場的話,你將失去這一獲得禮品(消息)的機會,因爲商場可不會管你是何方神聖,對於JMS Provider來說, 也是同樣道理,只要我(JMS Provider)派發消息的時候你不在,你收不到消息是你自己找的,跟我沒有關係。 也就是說,JMS Provider不會“耗費腦筋”去記下誰還沒有來接收消息,就跟商場不會紀錄到底誰的禮品還沒有來領取一樣, 因爲對於這種情況來說,耗費資源去這些不確定的client, 完全就是non-sense的,不是嘛? JMS Provider或者說商場,根本就不會知道誰會來領取消息或者禮品。

當我們轉到Durable Subscription的時候,情況就完全不同了。如果消息訂閱者通過Durable Subscription的方式來訂閱消息, 那麼JMS Provider將會費點兒腦筋來記下這個Durable Subscription的消息訂閱者是誰,即使當消息到達之後,該Durable Subscription消息訂閱者不在, JMS Provider也會保證, 該Durable Subscription消息訂閱者重新回來之後,之前到達而該Durable Subscription消息訂閱者還沒有處理的消息,將被一個不少的發送給它。

Let's take something for example. 假設你經營一家旅遊勝地的旅館,對於來住宿的顧客來說,你可以將他們劃分爲兩類:

散客,不事先預定你的旅店
對於這類顧客來說,你也不會知道他來自哪裏,姓甚名誰,只會在他們入住後臨時爲期分配房間並提供相應服務,一旦他們退房離開,旅館方將不再保留與之相關的任何信息。

這是不是與通常的消息訂閱者很像?

常客,或許都持有你旅館的VIP卡
對於這類顧客,一旦他們擁有了VIP卡,則意味着他們之前已經登記過,當他們再次光臨的時候,根據VIP卡這一標誌,你就可以確定他們上次入住的房間等信息, 這樣就可以爲他們提供相同的房間,相同的服務。直到他們主動註銷VIP卡或者VIP卡期限到期, 你的旅館將一直保留這類顧客的相關信息。

而這恰好與Durable Subscription場景下,JMS Provider與Durable Subscription消息訂閱者之間的關係很相似。

簡單來說,區分Durable Subscription和Nondurable Subscription最明顯的一個標誌就是,JMS Provider是否會爲消息訂閱者保存相應的狀態。 對於Durable Subscription來說,JMS Provider會根據消息訂閱者提供的某種標誌來爲其保留相應狀態, 就類似於那張VIP卡或者身份證,在使用JMS API進行Durable Subscription的編程的時候,消息訂閱者必須通過某種方式來提供這種標誌性信息,這是最需要我們關注的一點。

在瞭解了Durable Subscription的語義之後,我們馬上來看一下如何使用JMS API進行實際的Durable Subscription編程, 並詳細瞭解在JMS API中,我們可以通過什麼途徑爲JMS Provider提供Durable Subscription消息訂閱者的標誌信息...

2. 如何進行Durable Subscription(Durable Subscription How to)
我們以一個簡化功能的類似Spring的SimpleMessageListenerContainer爲例,來說明進行Durable Subscription的過程中應該注意的幾個問題,下面是該類的代碼:

public class GenericSimpleMessageListenerContainer extends ServiceWithLifecycle {
   
    private static final transient Log logger = LogFactory.getLog(GenericSimpleMessageListenerContainer .class);
   
    private JndiTemplate      jndiTemplate;
    private String            connectionFactoryJndiName;
    private String            destinationJndiName;
    private ConnectionFactory connectionFactory;
    private Destination       destination;
   
    private MessageListener   messageListener;
   
    private Connection        sharedConnection;
    private Session           session;
    private MessageConsumer   messageConsumer;
   
    /*
     * set a non-empty durableSubscriptionName to perform durable subscription
     */
    private String            durableSubscriptionName;
    /*
     * to identify durable subscriber plus durableSubscriptionName
     */
    private String            clientId;
   
    public GenericSimpleMessageListenerContainer()
    {

    }
    public GenericSimpleMessageListenerContainer(JndiTemplate jt)
    {
this.jndiTemplate = jt;
    }
   
    protected void doStart()
    {
Validate.notNull(getMessageListener());

setupConnectionFactoryIfNecessary(jndiTemplate);
setupDestinationIfNecessary(jndiTemplate);

try {
    setupSharedConnectionIfNecessary();
    session = getSharedConnection().createSession(false, Session.AUTO_ACKNOWLEDGE);
    if(StringUtils.isNotEmpty(getDurableSubscriptionName()))
    {
if(logger.isInfoEnabled())
{
    logger.info("create durable subsriber with name:"+getDurableSubscriptionName());
}
messageConsumer = session.createDurableSubscriber((Topic)getDestination(), getDurableSubscriptionName());
    }
    else
    {
if(logger.isInfoEnabled())
{
    logger.info("create generic Message Consumer.");
}
messageConsumer = session.createConsumer(getDestination());
    }
    messageConsumer.setMessageListener(getMessageListener());
    getSharedConnection().start();
   
    if(logger.isInfoEnabled())
    {
logger.info("The Connection to deliver messages is Started now!");
    }
   
} catch (JMSException e) {
    logger.error("failed to start Message listener container!!!\n");                    JmsUtils.closeMessageConsumer(getMessageConsumer());
    JmsUtils.closeSession(getSession());
    JmsUtils.closeConnection(getSharedConnection());
    throw JmsUtils.convertJmsAccessException(e);
}

    }
   
    protected void doStop()
    {
try {
    getSharedConnection().stop();
} catch (JMSException e) {
    logger.warn("failed to stop connection of delivering jms message.\n");
    logger.warn(ExceptionUtils.getFullStackTrace(e));
}
JmsUtils.closeMessageConsumer(getMessageConsumer());
JmsUtils.closeSession(getSession());
JmsUtils.closeConnection(getSharedConnection());
    }
    /**
     * After creating connection from ConnectionFactory,
     * we will check whether we can set clientId for the created connection,
     * If a pre-configured client Id exists, we will not try to set our clientId;
     * otherwise we will set our custom client Id if it's not empty.<br>
     *
     * The try-catch(IllegalStateException) is also for checking whether the jms provider has pre-configured a client Id for the connections it creates.<br>
     * If a pre-configured client id does exist, we will let it be, so after catching such exception, we just log it in WARN level to notify us.<br>
     * 
     * @throws JMSException
     */
    private void setupSharedConnectionIfNecessary() throws JMSException {
if (getSharedConnection() == null)
{
    setSharedConnection(getConnectionFactory().createConnection());
   
    String preConfiguredClientId = getSharedConnection().getClientID();
    if(StringUtils.isEmpty(preConfiguredClientId) && StringUtils.isNotEmpty(clientId))
    {
try
{
    getSharedConnection().setClientID(clientId);
    if(logger.isInfoEnabled())
    {
logger.info("set up JMS Connection with Client Id:"+clientId);
    }
}
catch(IllegalStateException e)
{
    logger.warn("A pre-configured client id exists, durable subscriber will use this client id and ignore external setted client id.");
    logger.warn("pre-configured client id:"+preConfiguredClientId);
    logger.warn("external setted client id:"+clientId);
}
    }
}
    }
    private void setupDestinationIfNecessary(JndiTemplate jndiTemplate) {
if(getDestination() == null)
{
    Validate.notEmpty(getDestinationJndiName());
   
    try {
setDestination((Destination)jndiTemplate.lookup(getDestinationJndiName()));
    } catch (NamingException e) {
throw new RuntimeException("failed to lookup destination via JNDI with jndiName:"+getDestinationJndiName());
    }
}
    }
    private void setupConnectionFactoryIfNecessary(JndiTemplate jndiTemplate) {
if(getConnectionFactory() == null)
{
    Validate.notEmpty(getConnectionFactoryJndiName());
   
    try {
setConnectionFactory((ConnectionFactory)jndiTemplate.lookup(getConnectionFactoryJndiName()));
    } catch (NamingException e) {
throw new RuntimeException("failed to lookup ConnectionFactory via JNDI with jndiName:"+getConnectionFactoryJndiName());
    }
}
    }

// getters and setters...
}
如果你在使用Spring 2.x之前的版本而又不能升級,那麼這個類可以“湊合”用一下(因爲它的功能並不完備,比如沒有添加MessageSelector以及多線程等功能支持), 如果可能,還是建議你使用Spring 2.x之後引入的SimpleMessageListenerContainer或者DefaultMessageListenerContainer,當然了,這些屬於題外話,

使用JMS API進行Durable Subscription編程與通常的方式沒有太多差異,只要搞清楚一下兩點,剩下的基本就不會有太大問題了。

2.1. Client Id
JMS規定了兩種Administered Object,即ConnnectionFactory和Destination,所以,“萬物伊始”,我們得先將這兩個東西從JNDI上拿下來, GenericSimpleMessageListenerContainer提供了兩種方式,要麼你在外面獲取到這兩個東西, 然後直接注入給他;要麼你就傳一個JndiTemplate, 然後注入這兩個東西對應的Jndi名稱。

有了ConnectionFactory,我們可以通過它創建到相應JMS Provider的連接;有了Destination,我們才知道該去哪裏接收消息,我想這個很容易理解, 這裏需要着重說明的是ConnectionFactory。

我們已經說過, 要進行Durable Subscription,客戶端必須提供某種類似VIP卡或者身份證之類的標誌,在JMS中,Client Id的存在即是因爲如此。 將Client Id稱作Connection Id或許更好理解,它與JMS的Connection相“掛鉤”,當一個JMS Connection被創建之後, 它有兩種方式獲得它的Client Id:

•通過ConnectionFactory自動獲得.  既然ConnectionFactory屬於Administered Object, 那麼在各個JMS Provider中部署相應ConnectionFactory的時候, 我們就可以設定通過ConnectionFactory創建Connection的時候,是否要爲創建的Connection設定Client Id, 以及該設定什麼樣的Client Id, 而具體設定方式可能需要參考各個JMS Provider各自的文檔。

•客戶端程序自定義設定.  在Connection被創建之後,並且沒有進行任何其他操作之前,客戶端程序可以爲其設定自定義的Client Id,不過,如果該Connection已經被ConnectionFactory預先設定了Client Id的話, connection..setClientID(clientId)將會拋出JMS的IllegalStateException。

所以,在setupSharedConnectionIfNecessary()方法中,你會發現,我們會事先檢查ConnectinFactory是否已經預先設定過Client Id,如果沒有並且客戶端程序持有注入的非空的Client Id, 那麼我們纔會爲Connection設定自定義的Client Id。

Caution
連接到JMS Provider進行Durable Subscription的多個Connection不可以擁有相同的Client Id,否則也會被IllegalStateException伺候!


2.2. Subscriber Name
單憑Client Id還不足以唯一標誌某一個Durable Subscription,就跟我憑一個身份證,可以預定多個房間一樣。 同一個連接裏,你可以創建多個MessageConsumer去訂閱不同Topic的消息,如果下回回來,你只想繼續接受某一個Topic消息的話,JMS Provider如何知道是哪一個? 所以,爲了區分同一個Connection中不同的Durable Subscription,我們還需要進一步的標誌物,這就是Subscriber Name!



messageConsumer = session.createDurableSubscriber((Topic)getDestination(), getDurableSubscriptionName());通過Session創建DurableSubscriber的時候,我們要爲其提供一個Durable Subscriber Name,這是與普通訂閱最基本的區別:

messageConsumer = session.createConsumer(getDestination());有了SubscriberName之後,下回,當我們重新連接然後使用相同的SubscriberName創建消息訂閱的時候,JMS Provider就會知道將哪一個Durable Subscription使用的Topic中的消息進行傳送了。

Note
創建MessageConsumer的時候可以同時設定相應的Message Selector, 另外進行異步消息接收的時候,需要爲MessageConsumer設定相應的MessageListener, 最後,調用connection.start()方法告知JMS Provider開始進行消息傳送,這裏只是簡單提及一下,我向大家比我更清楚。


2.3. 小結
Connection級別的Client Id和創建MessageConsumer時候的Subscriber Name唯一標誌一個Durable Subscription,這是在JMS中成功進行Durable Subscription的前提(當然,要是JMS Provider過於“山寨”,或許也不成)。

基本上,個人覺得在Durable Subscription中要提的就這些了。 有關JMS更多信息,可以參考JMS規範以及各個JMS Provider提供的文檔。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章