一、概述
消息服务是一种在分布式应用之间提供消息传递服务的软件,具有可靠、异步、宽松结
合、语言中立、平台中立的特点,而且通常是可配置的。它的实现原理是:对发送者和
接收者之间传递的消息进行封装,并在分布式消息客户程序结合的位置加上一个软件处
理层。消息服务为消息的客户程序提供了一个接口,这个接口隔离了底层的消息服务,
使得各种不同的客户程序能够通过一个友好的编程接口方便地通信。
Java消息服务(Java Message Service,JMS)是一个Java API,它定义了消息的客户程
序如何以一种标准化的形式与底层的消息服务提供者交互。JMS提供了一种接口,底层消
息服务提供者通过该接口向客户程序提供JMS消息服务。JMS提供了点对点消息模式(Po
int-to-Point)和发布-订阅消息模式(Publish-Subscribe)。点对点消息模式通过一
个消息队列实现,消息的生产者向队列写入消息,消息的消费者从队列提取消息。发布
-订阅消息模式通过一个话题(Topic)节点构成的层次结构实现,消息的生产者向这个
层次结构发布消息,消息的消费者向这个结构订阅消息。
点对点消息模式具有如下特点:
每一个消息只有一个消费者。
消息的接收者和发送者之间不存在时间上的依赖关系。不论发送者发送消息时接收者是
否在运行,接收者都可以提取信息。
接收者对于成功处理的消息给出回执。
发布-订阅消息模式具有如下特点:
每一个消息可以有多个消费者。
向某个话题订阅的客户程序只能收到那些在它订阅之后发布的消息。为了接收到消息,
订阅者必须保持活动状态。因此,发布者和订阅者之间存在时间上的依赖关系。
JMS API在一定程度上放宽了对这种依赖关系的要求,允许创建持久性订阅(Durable S
ubscription)。有了持久性订阅,当订阅者不活动时发送的消息也能接收到。
EJB 2.0规范定义了一种新的EJB类型,即消息驱动的EJB(Message-Driven EJB,简称M
DB),它能够以EJB的形式实现JMS消息的接收者。消息驱动的EJB实现一组新的接口,这
组接口使得EJB能够异步地接收和处理JMS消息生产者发送到队列或话题的消息。EJB客户
程序的构造方式与普通JMS消息生产者的构造方式完全一样,也就是说,JMS消息生产者
不必知道消息的消费者是一个EJB。
相对于会话Bean和实体Bean而言,消息驱动的Bean最大的特点是客户程序不通过接口访
问Bean。与会话Bean和实体Bean不同,消息驱动的Bean只有一个Bean类。从某些方面看
,消息驱动的Bean类似于无状态会话Bean:
消息驱动的Bean不为特定的客户保留数据或对话状态。
一个消息驱动Bean的所有的实例都是等价的,这使得容器能够把消息指派给任意一个消
息驱动Bean的实例。容器能够建立消息驱动Bean的缓冲池,实现消息的并发处理。
一个消息驱动的Bean能够处理来自多个客户程序的消息。
消息驱动Bean的实例变量可以在处理客户消息期间包含一些状态信息,例如JMS连接、打
开的数据库连接,或者是对EJB对象的引用。当一个消息到达,容器调用消息驱动Bean的
onMessage()方法处理消息。onMessage()方法通常把消息定型(cast)成为五种JMS消息
类型之一,然后按照应用的业务逻辑的要求处理消息。
传递给消息驱动Bean的消息可能处于一个事务之内,这时,onMessage()方法内的所有操
作都属于该事务的一部分。如果消息处理结果被回退,则系统将再次投递该消息。
哪些时候应该使用消息驱动的Bean呢?会话Bean和实体Bean能够发送JMS消息,能够同步
接收消息,但不能异步接收。一些时候,为防止过多地占用服务器资源,在服务器端的
组件中,我们想要避免阻塞,这时,我们可以用消息驱动的Bean异步接收消息。
二、MDB体系结构
图一描述了消息驱动的Bean组件的基本体系结构。
在图一中,位于顶端的是javax.ejb.EnterpriseBean接口,它是所有EJB的基础接口。E
nterpriseBean接口派生出了javax.ejb.MessageDrivenBean接口,所有消息驱动的EJB类
必须实现javax.ejb.MessageDrivenBean接口。此外,消息驱动的Bean必须实现javax.j
ms.MessageListener接口。公用的、非最终的、非抽象的消息驱动的EJB,比如图一显示
的MyMessageDrivenEJBean,必须同时实现MessageListener接口和MessageDrivenBean接
口。消息驱动的EJB与其他类型的EJB不同,它们不把业务方法导出给客户程序,它们关
心的只是遵从EJB容器的接口要求。由于这个原因,消息驱动的Bean必须有一个不需要参
数的公用构造方法(ejbCreate()方法),而且不应该实现finalize()方法。
2.1 MDB接口
在消息驱动的Bean中,setMessageDrivenContext()方法用来把一个MessageDrivenCont
ext的对象实例传递给EJB,它是MessageDrivenBean接口定义中容器调用的第一个方法。
MessageDrivenContext对象封装了一个EJB消息驱动容器上下文的接口,支持消息驱动的
EJB实例访问容器提供的运行时消息驱动上下文
对于消息驱动的EJB来说,关键之一是要实现一个没有参数的ejbCreate()方法。当EJB容
器准备创建消息驱动EJB的实例时,它将调用这个方法。容器之所以决定创建某个EJB的
实例,可能是因为它要构造一个Bean实例的缓冲池,也可能是因为它接收到了客户的请
求。这个ejbCreate()方法和其他Bean上的EJB构造方法类似,属于EJB实现的一种特殊的
构造函数或初始化方法。
当EJB容器准备不让Bean实例继续处理客户程序的请求时,它就会调用消息驱动Bean的e
jbRemove()方法。何时在消息驱动的Bean上调用ejbRemove()方法由EJB容器单独决定,
不受EJB客户程序的任何约束。应当注意的是,容器并不保证一定调用ejbRemove()方法
。在正常操作时,容器会调用ejbRemove()方法;但是,当消息驱动的Bean向容器抛出了
系统异常时,不能保证ejbRemove()方法一定会被调用。由于这个原因,Bean开发者必须
按时检查和清除Bean分配的所有资源。
对于Bean开发者来说,最重要的任务也许是实现onMessage()方法。当一个异步消息必须
由Bean实例处理时,容器将调用onMessage()方法。onMessage()方法的参数是一个普通
的JMS javax.jms.Message的实例,消息驱动的EJB实例从这个Message的实例提取待处理
的数据完成消息处理。
2.2 JMS消息接口
那么,在onMessage()方法调用传入的 JMS消息中,消息驱动的Bean如何提取信息,可以
提取哪些信息呢?图二描述了基本JMS消息类型的核心接口和概念。在一个以JMS为基础
的消息系统中,Message接口是在系统中传递的所有消息的最基本的接口(或称之为根接
口,Root Interface)。Destination接口描述了消息传递的一个终端;类似地,由于消
息有一个传递模式,所以图二还显示了Message接口与DeliveryMode接口的概念上的关系
。
JMS消息的头信息可以通过一组标准的方法设置或提取,这组标准方法的名字为getJMSX
XX()或setJMSXXX()形式(下面我们分别称之为get方法和set方法),其中XXX是消息头
信息中的属性名字,例如getJMSDeliveryMode()方法。在Message接口中,通过get方法
和set方法操作的标准头信息属性包括:唯一的消息ID,时标(Timestamp),答复和目
标地址,消息传递模式,消息类型,以及消息的优先级。
在JMS消息中,JMS容器提供者特有的属性可以通过getXXXProperty()方法提取,或通过
setXXXProperty()方法设置,其中XXX表示属性的类型,例如byte getByteProperty(ja
va.lang.String name)。每一个属性有一个通过String对象指定的名字和相应的值。名
字以JMSX前缀开头的属性作为标准JMS属性保留。
与消息正文数据(或称之为消息体,与消息头相对而言)的五种类型对应,五种消息类
型扩展了Message接口,如图三所示。Byte数据由BytesMessage封装,Serializable对象
由ObjectMessage封装,String消息由TextMessage封装,键-值对由MapMessage封装,I
/O流由StreamMessage封装。
这些派生消息类型上的方法为特定类型的消息正文定义了get和set操作,而在Message基
础接口内,有一个通用的clearBody()方法,这个方法清除消息的正文,并把它置入只写
模式。clearBody()方法只清除消息正文,不清除消息头或属性。如果消息正文是只读的
,调用该消息后,消息正文的状态将和新消息的空白正文状态一样。
2.3 MDB客户程序接口
消息驱动Bean的客户程序并不知道接收端实际处理消息的将是一个EJB。事实上,消息驱
动Bean的客户程序的构造方法与普通JMS客户程序的构造方法完全一样。
JMS的核心体系如图四所示。从图中我们可以看出,JMS ConnectionFactory(连接工厂
)初始上下文通过Java名称和目录接口(Java Naming and Directory Interface,JND
I)创建。随后,连接工厂将用来创建与JMS服务提供者的连接。有了JMS连接,我们就可
以获得创建消息生产者和消息消费者的会话(Session)。实际上,消息驱动Bean的客户
程序就是消息的生产者,它发送的消息将由消息驱动的Bean(即消息消费者)接收。
三、点对点消息队列模式
图五显示了在一个支持点对点消息队列的系统中JMS的基本体系结构。消息队列体系实际
上是核心JMS体系的一个扩展,特别地,它加入了对消息队列功能的支持。连接工厂、连
接、会话、消息生产者、消息消费者等都用点对点消息队列形式的接口进行了扩展。
JMS客户程序利用JNDI获得一个QueueConnectionFactory对象的引用。随后,我们用Que
ueConnectionFactory.createQueueConnection()方法之一创建一个QueueConnection对
象的实例。调用createQueueConnection()方法时可以指定一个用户名字和密码,或者,
我们也可以使用该方法不带参数的版本,此时假定使用默认用户身份。
QueueConnection接口是Connection接口的一种子类型,它代表着一个与JMS点对点消息
队列服务的连接。JMS客户程序调用createQueueSession()方法创建QueueSession的实例
,createQueueSession()方法调用中一个boolean类型的参数指定了QueueSession对象是
否要提供事务支持。另外,回执的模式也在createQueueSession()调用中通过参数指定
,这个参数的值可以是三个静态的标识符之一:AUTO_ACKNOWLEDGE,CLIENT_ACKNOWLED
GE,DUPS_OK_ACKNOWLEDGE。
QueueSession.createQueue()方法返回一个Queue对象的实例,调用Queue.getQueueNam
e()方法可以返回队列的名字。
QueueSession.createSender()方法创建一个QueueSender消息生产者,利用QueueSende
r可以把消息发送到Queue。消息可以通过各种不同的QueueSender.send()方法发送到Qu
eue,这些不同的send()方法能够把消息发送给QueueSender对象关联的Queue对象,或者
发送给send()方法调用中指定的Queue对象。消息递送模式、优先级、消息的有效时间都
可以在调用QueueSender.send()方法时指定。
发送给Queue的消息可以用Session接口中定义的各种消息构造方法创建。
四、发布-订阅消息模式
图六显示了在一个支持发布-订阅消息模式的系统中JMS的基本体系结构。发布-订阅消息
机制也是核心JMS机制的一种扩展,增加了一些适合发布-订阅消息模式的功能。连接工
厂、连接、会话、消息生产者、消息消费者等都用发布-订阅形式的接口进行了扩展。
JMS客户程序通过JNDI获得一个TopicConnectionFactory对象的引用。TopicConnection
Factory.createTopicConnection()方法用来创建TopicConnection对象的实例。调用cr
eateTopicConnection()方法时可以指定一个用户名字和密码,或者,我们也可以使用该
方法不带参数的版本,此时假定使用默认用户身份。
TopicConnection接口是Connection接口的一种子类型,它代表着一个与JMS发布-订阅消
息服务的连接。JMS客户程序调用TopicConnection.createTopicSession()方法创建Top
icSession的实例。会话的事务支持和回执模式也在创建TopicSession时指定。
TopicSession.createTopic()方法返回一个Topic对象的实例。Topic接口封装了一个话
题目的地,发布者向该目的地发送消息,订阅者从该目的地接收消息。不同的服务提供
者按照不同的方式实现话题名称的层次结构,调用Topic.getTopicName()方法可以获得
话题的String形式的描述。
TopicSession.createPublisher()方法创建一个TopicPublisher消息生产者,它用来把
消息发布到Topic。消息可以通过各种不同的TopicPublisher.publish()方法发布到Top
ic,这些不同的publish()方法能够把消息发送给TopicPublisher对象关联的Topic对象
,或者发送给publish()方法调用中指定的Topic对象。消息递送模式、优先级、消息的
有效时间都可以在调用TopicPublisher.publish()方法时指定。
发送给Topic的消息可以用Session接口中定义的各种消息构造方法创建。
五、实例
本示例应用是一个消息驱动Bean应用的简单例子,由以下两部分构成:
SimpleMessageClient:J2EE应用客户程序,向队列发送消息。
SimpleMessageEJB:一个消息驱动的Bean,异步地接收和处理由客户程序发送到队列的
消息。
图七描述了这个应用的结构。客户端应用把消息发送到队列,队列由管理员通过j2eead
min命令创建。JMS提供者(这里是J2EE服务器)把消息传递给消息驱动Bean的实例,由
Bean的实例处理消息。
该示例应用的完整代码可以从本文最后下载。
5.1 客户端
SimpleMessageClient把消息发送到SimpleMessageBean监听的队列。客户程序首先确定
连接工厂和队列:
queueConnectionFactory = (QueueConnectionFactory) jndiContext.lookup
("java:comp/env/jms/MyQueueConnectionFactory");
queue = (Queue) jndiContext.lookup("java:comp/env/jms/QueueName");
接下来,客户程序创建队列连接、会话和一个消息发送器:
queueConnection = queueConnectionFactory.createQueueConnection();
queueSession = queueConnection.createQueueSession(false,
Session.AUTO_ACKNOWLEDGE);
queueSender = queueSession.createSender(queue);
最后,客户程序把几个消息发送到队列:
message = queueSession.createTextMessage();
for (int i = 0; i < NUM_MSGS; i++) {
message.setText("我是" + msgArray[i] );
System.out.println("Sending message: " +
message.getText());
queueSender.send(message);
}
5.2 MDB组件
SimpleMessageEJB类阐明了编写消息驱动Bean类的要求:
实现MessageDrivenBean接口和MessageListener接口。
类定义为public类型。
类不能定义成abstract或final。
实现一个onMessage()方法。
实现一个ejbCreate()方法和一个ejbRemove()方法。
包含一个public类型的不需要参数的构造方法。
不能定义finalize()方法。
与会话Bean和实体Bean不同,消息驱动的Bean不定义客户程序访问的接口。客户程序不
是先定位消息驱动的Bean,再调用这些Bean上的方法。虽然消息驱动的Bean没有业务方
法,但它们可以包含由onMessasge()方法内部调用的辅助方法。
当队列接收到一个消息,EJB容器将调用消息驱动Bean的onMessage()方法。在SimpleMe
ssageBean类中,onMessage()方法把接收到的消息定型(cast)成为TextMessage类型,
然后显示出文本信息:
public void onMessage(Message inMessage) {
TextMessage msg = null;
try {
if (inMessage instanceof TextMessage) {
msg = (TextMessage) inMessage;
System.out.println("MESSAGE BEAN:收到消息: "
+ msg.getText());
} else {
System.out.println("消息类型错误: "
+ inMessage.getClass().getName());
}
} catch (JMSException e) {
e.printStackTrace();
mdc.setRollbackOnly();
} catch (Throwable te) {
te.printStackTrace();
}
}
消息驱动Bean的ejbCreate()方法和ejbRemove()方法必须符合以下要求:
访问控制修饰符必须是public。
返回值类型必须是void。
不能有static和final修饰符。
throws子句不能定义任何应用自定义的异常。
不能带有参数。
在SimpleMessageBean类中,ejbCreate()方法和ejbRemove()方法都是空的,不执行任何
有实际意义的操作。
5.3 打包
接下来我们要把上面的应用打包成一个J2EE EAR文件。首先要把SimpleMessageEJB打包
成Jar文件。通常,打包过程可以通过工具完成,但理解模块部署描述器仍是必要的。在
EJB应用模块部署描述器中,顶级元素下面包含元素。下面可以包含一组元素(按照EJB
2.0新规范),每一个元素描述一个消息驱动Bean的配置和部署。
SimpleMessageEJB的ejb-jar.XML文件如下所示。元素内定义了消息驱动Bean的配置和部
署信息,例如唯一的Bean名字、Bean类的名字、配置参数、安全信息、事务信息、消息
目的地类型等。
......
SimpleMessageJAR
SimpleMessageEJB
SimpleMessageEJB
SimpleMessageBean
Container
javax.jms.Queue
SimpleMessageEJB
Bean
onMessage
javax.jms.Message
Required
除了ejb-jar.xml部署描述器之外,通常还要有面向特定平台和环境的部署描述器。大多
数时候,这种描述器可以用GUI工具编写。请参见下载代码中提供的例子。
打包好各个模块之后,接着还要把J2EE应用打包成EAR文件。有关这一步骤的详细说明,
请参见开发平台的相关文档。本文以后有关部署和运行的说明,就以打包后的EAR文件为
基础。
5.4 部署和运行
假设我们在Sun的J2EE参考实现上部署和测试这个示例应用。为便于查看消息驱动Bean的
输出,我们必须以-verbose模式启动服务器:
j2ee -verbose
用下面的j2eeadmin命令创建队列:
j2eeadmin -addJmsDestination jms/MyQueue queue
验证队列已经创建成功:
j2eeadmin -listJmsDestination
启动deploytool,选择菜单“File-->Open”,打开SimpleMessageApp.ear文件。接着,
选择菜单“Tools --> Deploy”,部署应用。出现部署提示时,选中“Return Client
JAR”检查框。
在一个命令窗口中,进入EAR文件(SimpleMessageAppClient.jar文件)所在目录,把环
境变量APPCPATH设置为SimpleMessageAppClient.jar。然后,执行下面的命令:
runclient -client SimpleMessageApp.ear -name SimpleMessageClient -textauth
在登录提示中,输入用户名字j2ee,输入密码j2ee。此时,客户程序将输出以下内容:
Sending message: 我是老大孙悟空
Sending message: 我是老二猪八戒
Sending message: 我是老三沙和尚
在启动j2ee服务器的命令窗口,我们可以看到如下输出:
六、参考资料
下载本文代码:J2EEMessageDrivenEJB_code.zip
Enterprise JavaBean Specifications
(出处:http://www.knowsky.com)