三、JMS P2P编程
在JMS P2P通信方式中,发送程序将消息放入一个队列,根据通信要求,发送程序可以要求一个应答信息(请求-应答模式),也可以不要求立即获得应答(发送-遗忘模式)。如果需要应答信息,发送程序通过消息头的JMSReplayTo域向消息的接收程序声明应答信息应当放入哪一个本地队列。
在请求-应答模式中,发送程序可以按照两种方式操作。一种称为伪同步方式,发送程序在等待应答消息时会被阻塞;另一种是异步方式,发送程序发送消息后不被阻塞,可以照常执行其他处理任务,它可以在以后适当的时机检查队列,查看有没有它希望得到的应答信息。下面的代码片断显示了JMS程序发送消息的过程。
// 发送JMS消息 import javax.jms.Message; import javax.jms.TextMessage; import javax.jms.QueueConnectionFactory; import javax.jms.QueueConnection; import javax.jms.QueueSender; import javax.jms.QueueSession; import javax.jms.Queue; import javax.jms.JMSException; import javax.naming.InitialContext; import javax.naming.Context; public class MyJMSSender { private String requestMessage; private String messageID; private int requestRetCode = 1; private QueueConnectionFactory queueConnectionFactory = null; private Queue requestQueue = null; private Queue responseQueue = null; private QueueConnection queueConnection = null; private QueueSession queueSession = null; private QueueSender queueSender = null; private TextMessage textMsg = null; // 其他代码... public int processOutputMessages(String myMessage) { // 查找管理对象 try { InitialContext initContext = new InitialContext(); Context env = (Context) initContext.lookup ("java:comp/env"); queueConnectionFactory = (QueueConnectionFactory) env.lookup("tlQCF"); requestQueue = (Queue) env.lookup("tlReqQueue"); responseQueue = (Queue) env.lookup("tlResQueue"); queueConnection = queueConnectionFactory. createQueueConnection(); queueConnection.start(); queueSession = queueConnection.createQueueSession (true, 0); queueSender = queueSession.createSender(requestQueue); textMsg = queueSession.createTextMessage(); textMsg.setText(myMessage); textMsg.setJMSReplyTo(responseQueue); // 处理消息的代码逻辑... queueSender.send(textMsg); queueConnection.stop(); queueConnection.close(); queueConnection = null; } catch (Exception e) { // 异常处理代码... } return requestRetCode = 0; } } |
下面来分析一下这段代码。JMS程序的第一个任务是找到JNDI名称上下文的位置。对于WSAD开发环境来说,如果JMS程序属于J2EE项目,则JNDI名称空间的位置由WSAD测试服务器管理,运行时环境能够自动获知这些信息。在这种情况下,我们只要调用InitialContext类默认的构造函数创建一个实例就可以了,即:
InitialContext initContext = new InitialContext(); |
对于WSAD环境之外的程序,或者程序不是使用WSAD JNDI名称空间,例如使用LDAP(轻量级目录访问协议),程序寻找JNDI名称的操作稍微复杂一点,必须在一个Properties或Hashtable对象中设定INITIAL_CONTEXT_FACTORY和PROVIDER_URL,然后将该Properties对象或Hashtable对象作为参数调用InitialContext的构造函数。下面我们来看几个创建InitialContext对象的例子,第一个例子显示了运行在WSAD之外的程序如何找到WSAD InitialContext对象。
// 例一:运行在WSAD之外的程序寻找WSAD InitialContext对象 // 说明:将localhost替换为JNDI服务运行的服务器名称 Properties props = new Properties(); props.put(Context.INITIAL_CONTEXT_FACTORY, "com.ibm.websphere.naming.WsnInitialContextFactory"); props.put(Context.PROVIDER_URL, "iiop://localhost/"); InitialContext initialContext = InitialContext(props); // 例二:下面的例子显示了如何找到基于文件的JNDI InitialContext Hashtable hashTab = new Hashtable (); hashTab.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory"); hashTab.put(Context.PROVIDER_URL, "file://c:/temp"); InitialContext initialContext = InitialContext(hashTab); // 例三:下面的例子显示了如何找到基于LDAP的JNDI InitialContext Hashtable hashTab = new Hashtable (); hashTab.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); hashTab.put(Context.PROVIDER_URL, "file://server.company.com/o=provider_name, c=us"); InitialContext initialContext = InitialContext(hashTab); |
获得InitialContext对象之后,下一步是要查找java:comp/env子上下文(Subcontext),例如:
Context env = (Context) initContext.lookup("java:comp/env"; |
java:comp/env是J2EE规范推荐的保存环境变量的JNDI名称子上下文。在这个子上下文中,JMS程序需要找到几个由JMS管理的对象,包括QueueConnectionFactory对象、Queue对象等。
下面的代码寻找由JMS管理的几个对象,这些对象是JMS程序执行操作所必需的。
queueConnectionFactory = (QueueConnectionFactory) env.lookup("QCF"); requestQueue = (Queue) env.lookup("requestQueue"); |
接下来,用QueueConnectionFactory对象构造出QueueConnection对象。
queueConnection = queueConnectionFactory.createQueueConnection(); |
3.1 使用JMS QueueConnection对象
JMS QueueConnection提供了一个通向底层MOM(就本文而言,它是指WebSphere MQ队列管理器)的连接,按照这种方式创建的连接使用默认的Java绑定传输端口来连接到本地的队列管理器。
对于MQ客户程序来说(运行在不带本地队列管理器的机器上的MQ程序),QueueConnectionFactory对象需要稍作调整,以便使用客户端传输端口:
QueueConn.setTransportType(JMSC.MQJMS_TP_CLIENT_MQ_TCPIP); |
另外,刚刚创建的QueueConnection对象总是处于停止状态,需要调用"queueConnection.start();"将它启动。
建立连接之后,用QueueConnection对象的createQueueSession方法来创建一次会话。必须注意的是,QueueSession对象有一个单线程的上下文,不是线程安全的,因此会话对象本身以及在会话对象基础上创建的对象都不是线程安全的,在多线程环境中必须加以保护。createQueueSession方法有两个参数,构造一个QueueSession对象的语句类如:
queueSession = queueConnection.createQueueSession (false, Session.AUTO_ACKNOWLEDGE); |
createQueueSession方法的第一个参数是boolean类型,它指定了JMS事务类型--也就是说,当前的队列会话是事务化的(true)还是非事务化的(false)。JMS事务类型主要用于控制消息传递机制,不要将它与EJB的事务类型(NotSupported,Required,等等)混淆了,EJB的事务类型设定的是EJB模块本身的事务上下文。createQueueSession方法的第二个参数是一个整数,指定了确认模式,也就是决定了如何向服务器证实消息已经传到。
如果队列会话是事务化的(调用createQueueSession方法的第一个参数是true),第二个参数的值将被忽略,因为提交事务时应答总是自动执行的。如果由于某种原因事务被回退,则不会有应答出现,视同消息尚未递送,JMS服务器将尝试再次发送消息。如果有多个消息参与到同一会话上下文,它们被作为一个组处理,确认最后一个消息也就自动确认了此前所有尚未确认的消息。如果发生回退,情况也相似,整个消息组将被视为尚未递送,JMS服务器将试图再次递送这些消息。
下面说明一下底层的工作机制。当发送程序发出一个消息,JMS服务器接收该消息;如果消息是持久性的,服务器先将消息写入磁盘,然后确认该消息。自此之后,JMS服务器负责把消息发送到目的地,除非它从客户程序收到了确认信息,否则不会从临时存储位置删除消息。对于非持久性的消息,收到消息并保存到内存之后确认信息就立即发出了。
如果队列会话是非事务化的(调用createQueueSession方法的第一个参数是false),则应答模式由第二个参数决定。第二个参数的可能取值包括:AUTO_ACKNOWLEDGE,DUPS_OK_ACKNOWLEDGE,以及CLIENT_ACKNOWLEDGE。
■ 对于非事务化的会话来说,AUTO_ACKNOWLEDGE确认模式是最常用的确认模式。对于事务化的会话,系统总是假定使用AUTO_ACKNOWLEDGE确认模式。
■ DUPS_OK_ACKNOWLEDGE模式是一种"懒惰的"确认方式。可以想到,这种模式可能导致消息提供者传递的一些重复消息出错。这种确认模式只用于程序可以容忍重复消息存在的情况。
■ 在CLIENT_ACKNOWLEDGE模式中,消息的传递通过调用消息对象的acknowledge方法获得确认。
在AUTO_ACKNOWLEDGE模式中,消息的确认通常在事务结束处完成。CLIENT_ACKNOWLEDGE使得应用程序能够加快这一过程,只要处理过程一结束就立即予以确认,恰好在整个事务结束之前。当程序正在处理多个消息时,这种确认模式也是非常有用的,因为在这种模式中程序可以在收到所有必需的消息后立即予以确认。
对于非事务化的会话,一旦把消息放入了队列,所有接收程序立即能够看到该消息,且不能回退。对于事务化的会话,JMS事务化上下文把多个消息组织成一个工作单元,消息的发送和接收都是成组执行。事务化上下文会保存事务执行期间产生的所有消息,但不会把消息立即发送到目的地。
只有当事务化的队列会话执行提交时,保存的消息才会作为一个整体发送给目的地,这时接收程序才可以看到这些消息。如果在事务化队列会话期间出现错误,在错误出现之前已经成功处理的消息也会被撤销。定义成事务化的队列会话总是有一个当前事务,不需要显式地用代码来开始一个事务。当然,事务化的环境总是存在一定的代价--事务化会话总是要比非事务化会话慢。
● 注意:JMS队列会话的事务化是一个概念,实现了JMS逻辑的Java方法的事务属性是另一个概念,不要将两者混淆了。TX_REQUIRED属性表示的是方法在一个事务上下文之内运行,从而确保数据库更新和队列中消息的处理作为一个不可分割的工作单元执行(要么都成功提交,要么都回退)。顺便说明一下,在容器管理的事务环境中,一个全局性的两阶段提交(Two-Phase Commit)事务上下文会被激活(参见本文后面的详细说明),这时参与全局性事务的数据源应当用XA版的数据库驱动程序构建。
相比之下,在createQueueSession方法的第一个参数中指定true建立的是JMS事务上下文:多个消息被视为一个工作单元。在消息的接收方,执行queueSession.commit()之前,即使已经收到了多个消息也不会给出确认信息;一旦queueSession.commit()执行,它就确认收到了在此之前尚未提交的所有消息;消息发送方的情况也相似。
3.2 处理回退事件
如前所述,如果事务异常终止,收到的消息会被发回到原来的队列。接收消息的程序下一次再来处理该队列时,它还会再次得到同一个消息,而且这一次事务很有可能还是要失败,必须再次把消息发送回输入队列--如此不断重复,就形成了无限循环的情况。
为防止这种情况出现,我们可以设置监听端口上的Max_retry计数器,超过Max_retry计数器规定的值,接收程序就不会再收到该消息;或者对于推式的会话,消息不会再被传递。另外,在推式会话中,重新传递的事务会被设置JMSRedelivered标记,程序可以调用消息对象的getJMSRedelivered方法来检查该标记。
消息通过QueueSender JMS对象发送,QueueSender对象利用QueueSession对象的createSender方法创建,每一个队列都要创建一个QueueSender对象:
queueSender = queueSession.createSender(requestQueue); |
接下来创建一个消息(TextMessage类型),根据myMessage字符串的值设置消息的内容,myMessage变量作为输入参数传递给queueSession.createTextMessag方法。
textMsg = queueSession.createTextMessage(myMessage); |
指定接收队列,告诉接收消息的程序要把应答放入哪一个队列:
textMsg.setJMSReplyTo(responseQueue); |
最后,用Sender对象发送消息,然后停止并关闭连接。
queueSender.send(textMsg); queueConnection.stop(); queueConnection.close(); |
发出消息之后,可以调用消息对象的getJMSMessageID方法获得JMS赋予消息的ID(即提取JMSMessageID消息头域),以后就可以通过这一ID寻找应答消息:
String messageID = message.getJMSMessageID(); |
如果有JMS连接池,释放后的会话不会被拆除,而是被返回给连接池以供重用。
3.3 关闭JMS对象
垃圾收集器不一定会及时关闭JMS对象,如果程序要创建大量短期生存的对象,可能会引起问题,至少会浪费大量宝贵的资源,所以显式地释放所有不用的资源是很重要的。
if (queueConn != null) { queueConn.stop(); queueConn.close(); queueConn = null; } |
关闭队列连接对象将自动关闭所有利用该连接对象创建的对象。但个别JMS提供者例外,这时请按照下面代码所示的次序关闭所有JMS对象。
// 关闭JMS对象 if (queueReceiver != null) { queueReceiver.close(); queueReceiver = null; } if (queueSender != null) { queueSender.close(); queueSender = null; } if (queueSession != null) { queueSession.close(); queueSession = null; } if (queueConn != null) { queueConn.stop(); queueConn.close(); queueConn = null; } |
3.4 接收消息
消息接收方的处理逻辑也和发送方的相似。消息由JMS QueueReceiver对象接收,QueueReceiver对象建立在为特定队列创建的QueueSession对象的基础上。差别在于QueueReceiver接收消息的方式--QueueReceiver对象能够按照伪同步或异步方式接收消息。下面的代码显示了如何用伪同步方式接收消息。
// 伪同步方式接收消息 import javax.jms.Message; import javax.jms.TextMessage; import javax.jms.QueueConnectionFactory; import javax.jms.QueueConnection; import javax.jms.QueueSender; import javax.jms.Queue; import javax.jms.Exception; import javax.naming.InitialContext; public class MyJMSReceiver { private String responseMessage; private String messageID; private int replyRetCode = 1; private QueueConnectionFactory queueConnectionFactory = null; private Queue inputQueue = null; private QueueConnection queueConnection = null; private QueueSession queueSession = null; private QueueReceiver queueReceiver = null; private TextMessage textMsg = null; public void processIncomingMessages() { // 查找管理对象 InitialContext initContext = new InitialContext(); Context env = (Context) initContext.lookup("java:comp/env"); queueConnectionFactory = (QueueConnectionFactory) env.lookup("tlQCF"); inputQueue = (Queue) env.lookup("tlQueue"); queueConnection = queueConnectionFactory. createQueueConnection(); queueConnection.start(); queueSession=queueConnection.createQueueSession(true, 0); queueReceiver = queueSession.createReceiver(inputQueue); // 等一秒钟,看看是否有消息到达 TextMessage inputMessage = queueReceiver.receive(1000); // 其他处理代码... queueConnection.stop(); queueConnection.close(); } } |
下面分析一下上面的代码。消息由QueueReceiver对象执行receive方法接收。receive方法有一个参数,它指定了接收消息的等待时间(以毫秒计)。在上面的代码中,QueueReceiver对象在一秒之后超出期限,解除锁定,把控制返回给程序。如果调用receive方法时指定了等待时间,QueueReceiver对象在指定的时间内被锁定并等待消息,如果超出了等待时间仍无消息到达,QueueReceiver对象超时并解除锁定,把控制返回给程序。
接收消息的方法还有一个"不等待"的版本,使用这个方法时QueueReceiver对象检查是否有消息之后立即返回,将控制交还给程序。下面是一个例子:
TextMessage message = queueReceiver.receiveNoWait(); |
如果调用receive方法时不指定参数,QueueReceiver对象会无限期地等待消息。采用这种用法时应当非常小心,因为程序会被无限期地锁定。下面是一个无限期等待消息的例子:
TextMessage message = queueReceiver.receive(); |
不管等待期限的参数设置了多少,这都属于拉式消息传递,如前所述,这种消息传递方式的效率较低。不仅如此,这种方法还不适合于J2EE EJB层,不能用于EJB组件之内,原因稍后就会说明。不过,这种处理方式适合在Servlet、JSP和普通Java JMS应用中使用。
接收消息的第二种办法是异步接收。用异步接收方式时,QueueReceiver对象必须用setMessageListener(class_name)方法注册一个MessageListener类,其中class_name参数可以是任何实现了onMessage接口方法的类。在下面这个例子中,onMessage方法由同一个类实现(为简单计,这里没有给出try/catch块的代码)。
● 注意:接下来的消息接收方式不适用于EJB组件,这些代码仅适用于Servlet、JSP和普通的Java JMS应用程序。
// 消息监听类的例子 import javax.jms.Message; import javax.jms.TextMessage; import javax.jms.QueueConnectionFactory; import javax.jms.QueueConnection; import javax.jms.QueueReceiver; import javax.jms.Queue; import javax.jms.Exception; import javax.naming.InitialContext; public class MyListenerClass implements javax.jms.MessageListener { private int responseRetCode = 1; private boolean loopFlag = true; private QueueConnectionFactory queueConnectionFactory = null; private Queue responseQueue = null; private QueueConnection queueConnection = null; private QueueSession queueSession = null; private QueueSender queueSender = null; public void prepareEnvironment(String myMessage) { // 查找管理对象 InitialContext initContext = new InitialContext(); Context env = (Context) initContext.lookup("java:comp/env"); queueConnectionFactory = (QueueConnectionFactory) env.lookup("tlQCF"); responseQueue = (Queue) env.lookup("tlResQueue"); queueConnection = queueConnectionFactory. createQueueConnection(); queueSession = queueConnection.createQueueSession (true, 0); queueReceiver = queueSession.createReceiver (responseQueue); queueReceiver.setMessageListener(this) queueConnection.start(); } public void onMessage(Message message) { // 希望收到一个文本消息... if (message instanceof TextMessage) { String responseText = "确认消息传递:" + ((TextMessage) message).getText(); // 当收到一个以@字符开头的消息时,循环结束, // MessageListener终止 if (responseText.charAt(0) == '@') { loopFlag = 1; // 结束处理; } else {t // 继续处理消息 // 本例中我们知道应答消息的队列,并非真的要用到 //message.getJMSReplyTo // 这只是一个如何获取应答消息队列的例子 Destination replyToQueue=message.getJMSReplyTo(); // 设置应答消息 TextMessage responseMessage = responseSession.createTextMessage(responseText); // 使CorrelationID等于消息ID, //这样客户程序就能将应答消息和原来的请求 // 消息对应起来 messageID = message.getJMSMessageID(); responseMessage.setJMSCorrelationID(messageID); // 设置消息的目的地 responseMessage.setJMSDestination( replyToQueue) queueSender.send( responseMessage); } } } // 保持监听器活动 while (loopFlag) { // 将控制转移到其他任务(休眠2秒) System.out.println("正处于监听器循环之内..."); Thread.currentThread().sleep(2000); } // 当loopFlag域设置成flase时,结束处理过程 queueConn.stop(); queueConnection.close(); } |
注册一个MessageListener对象时,一个实现了MessageListener逻辑的新线程被创建。我们要保持这个线程处于活动状态,因此使用了一个while循环,首先让线程休眠一定的时间(这里是2秒),将处理器的控制权转让给其他任务。当线程被唤醒时,它检查队列,然后再次进入休眠状态。当一个消息到达当前注册的MessageListener对象所监视的队列时,JMS调用MessageListener对象的onMessage(message)方法,将消息作为参数传递给onMessage方法。
这是一种推式的消息接收方式,程序效率较高,但仍不适合在EJB组件之内使用。下面将探讨为什么这些接收消息的方式都不能用于EJB组件之内,然后给出解决办法。虽然这部分内容放入了P2P通信方式中讨论,但其基本思路同样适用于Pub/Sub通信方式。