44.3. Message-driven Bean

44.3.1. Message-driven Bean

Message-driven Bean是JMS消息驱动的Java EE™平台服务器端组件,具备无状态、支持事务的特点。当从JMS队列(Queue)或主题(Topic)中接收到JMS消息后,由容器对组件进行调用。一般,可理解为消息的监听器(Listener)及接收者(Consumer)。

有关JMS消息系统,请参阅第 47.2 节 “消息服务开发”及???。

Message-driven Bean组件对于客户端是不可见的。客户端如希望调用封装在组件中的业务逻辑,只能通过向组件监听的JMS队列(Queue)或主题(Topic)发送消息,然后容器以事件的形式向组件实例发送消息,组件实例根据消息的内容调用相应的业务逻辑或其他组件。

因此,任何向组件监听的特定JMS队列(Queue)或主题(Topic)发送JMS消息的客户端,即可视为Message-driven Bean的客户端。

Message-driven Bean组件模型不具备会话状态,也就是说,当组件实例在没有对客户端的JMS消息提供处理的时候,所有的实例间没有差别。

Message-driven Bean的运行和客户端的运行是异步的。同时,对于客户端不可见。其实例由容器创建,其生存周期由容器控制。

44.3.1.1. Message-driven Bean与EJB容器、客户端、消息系统

下图是EJB容器、客户端、消息系统与EJB之间的关系:

EJB容器、客户端、消息系统与EJB之间的关系

客户端发送消息到JMS消息系统中的队列或主题。Message-driven Bean在部署到容器中时,指定的队列或主题,容器对其进行监听;当容器从队列或主题中接收到消息之后,将消息作为事件的一部分通知容器中相应的Message-driven Bean实例。

44.3.2. 组件模型单元

Message-driven Bean由容器控制其生存周期,容器提供安全、并发、事务等等服务,对于客户端来说,Message-driven Bean是不可见的。因此,Message-driven Bean不同于Session Bean和Entity Bean,不具有组件接口和Home接口。

一般,在Apusic应用服务器启动之后,通过开发者在部署描述中指定的消息或队列,容器即创建Message-driven Bean的实例,对队列或主题进行监听并接受消息。

Message-driven Bean组件模型包含两个单元,即组件类和部署描述。下面分别对开发这些单元时,涉及的普遍过程、规则及注意事项进行描述。

44.3.2.1. 组件类

44.3.2.1.1. javax.ejb.MessageDrivenBean接口

EJB2.1规范中的Message-driven Bean组件中的组件类必须实现MessageDrivenBean接口。EJB3.0规范不强制Message-driven Bean实现该接口,而通过@MessageDriven注解进行标记并通过依赖注入与注解实现类似功能。

MessageDrivenBean接口中定义了两个容器管理回调的方法:

  • setMessageDrivenContext方法,容器创建Bean实例后,容器将调用该方法将由容器维护的Bean实例的上下文(context)与Bean实例进行关联。 在EJB3.0规范中,可使用@Resource注解通知容器注入MessageDrivenContext实例。

  • ejbRemove方法,在实例被容器清除时,容器将调用此方法。一般,实例会在此方法中对实例占用的资源进行释放。在EJB3.0规范中,可使用@PreDestroy注解标记此方法。

44.3.2.1.2. javax.jms.MessageListener接口

Message-driven Bean组件中的组件类必须实现MessageListener接口。

在消息到达Message-driven Bean指定的监听队列或主题时,容器将调用javax.jms.MessageListener接口中定义的onMessage方法。开发者在此方法中提供对消息进行处理的业务逻辑。

Session Bean和Entity Bean不可实现javax.jms.MessageListener接口。

44.3.2.1.3. javax.ejb.MessageDrivenContext接口

容器将提供一个MessageDrivenContext对象,使实例可以访问由容器维护的实例的上下文环境。在此接口中,定义了如下方法:

  • getEJBHome、getEJBLocalHome方法,从EJBContext接口继承的方法,Message-driven Bean实例不可调用此方法;

  • getCallerPrincipal方法,从EJBContext接口继承的方法,Message-driven Bean实例不可调用此方法;

  • isCallerInRole方法,从EJBContext接口继承的方法,Message-driven Bean实例不可调用此方法;

  • setRollbackOnly方法,当前事务将被永久标记为回滚,不会被提交。只有容器管理事务的Message-driven Bean可被允许使用此方法;

  • getRollbackOnly方法,检查当前事务是否已被标记为回滚。例如,EJB实例可以通过此方法,决定是否继续在当前事务边界内继续进行计算。只有容器管理事务的Message-driven Bean可被允许使用此方法;

  • getUserTransaction方法,返回javax.transaction.UserTransaction接口。EJB实例可通过此接口对事务边界进行划分,并取得事务的状态。只有容器管理事务的Message-driven Bean可被允许使用此方法;

44.3.2.1.4. 串行化的调用

Apusic应用服务器中的EJB容器支持Message-driven Bean的多个实例的并发运行,但是每个实例只“看到”一个串行的方法调用过程,因此开发Message-driven Bean时,不需要将其以可重入(reentrant)的方式进行编写。

44.3.2.1.5. 消息处理的并发

Apusic应用服务器允许Message-driven Bean的多个实例并发执行,提供对流(Stream)消息并发处理。

44.3.2.1.6. Message-driven Bean方法的事务上下文

onMessage方法在何种事务范围内被调用,取决于部署描述中指定的事物属性,如Bean被指定使用容器管理的事务的方式,则必须将事务属性设置为“Required”或“NotSupported”。

当Bean采用Bean管理的事务的方式,即使用javax.transaction.UserTransaction接口进行事务划分时,导致Bean实例被调用的消息接收操作并非是事务中的一部分。如果希望消息接收操作是事务中的一部分,则Bean必须使用容器管理事务的方式,并且设置事务属性为“Required”。

44.3.2.1.7. 消息接收确认(Message Acknowledgement)

Message-driven Bean不能使用JMS API中提供的消息接收确认操作。消息接收确认操作由容器自动完成。如Bean采用了容器管理事务的方式,则消息接收确认操作作为事务提交的一部分自动进行。如使用了Bean管理事务的方式,消息接收确认操作不能作为事务提交的一部分,开发者可通过在部署描述中的acknowledge-mode元素指定消息接收确认操作的方式为AUTO_ACKNOWLEDGE或DUPS_OK_ACKNOWLEDGE,如未指定acknowledge-mode元素,容器将使用AUTO_ACKNOWLEDGE方式进行消息接收确认操作。

44.3.2.1.8. 指定队列(Queue)或主题(Topic)

当Message-driven Bean被部署到容器时,必须关联到某个消息队列(Queue)或主题(Topic),以便容器对此队列或主题进行监听。

开发者可通过@MessageDriven注解的mappedName属性或部署描述文件中的message-driven-destination元素指定关联的队列或主题。

如Bean关联的是一个消息主题,则通过部署描述中的subscription-durability元素指定对队列进行的是持久还是非持久订阅,如此元素未指定,则使用非持久订阅的方式。

44.3.2.1.9. 异常处理

Message-driven Bean中的onMessage方法不能声明抛出java.rmi.RemoteException异常。

一般来说,Message-driven Bean在运行期间不应向容器抛出RuntimeException异常。RuntimeException异常是指会导致Message-driven Bean进入“不存在”状态的非应用级异常。如果Bean使用了Bean-managed事务并抛出了RuntimeException异常,容器不应确认收到了这条消息。

从发信端看来,收信端一直存在,若发信端继续对该目的地发送消息,容器会自动把消息转向到其他Message-driven Bean实例。

44.3.2.1.10. 遗漏的PreDestroy调用

在系统发生异常的情况下,不能保证容器总会调用Bean的PreDestroy方法,因此,如果Bean在PostConstruct方法中打开了一些资源,并在PreDestroy方法中释放这些资源,在这种情况下,则这些资源不能被释放。

鉴于以上原因,使用Message-driven Bean的应用需要提供一种机制,以便周期性的清除这些未释放的资源占用。

44.3.2.2. 必须遵守的规则

在开发Message-driven Bean时,开发者必须遵守如下规则:

44.3.2.2.1. 组件类
  • 使用EJB2.1规范时,必须间接或直接实现javax.ejb.MessageDrivenBean接口; 使用EJB3.0规范时,可改为使用@MessageDriven注解对组件类进行标记。

  • 必须间接或直接实现javax.jms.MessageListener接口;

  • 类必须声明为public,不可被声明为final或abstract类;

  • 必须拥有一个无参数的public构造函数(constructor);

  • 类不能定义finalize()方法;

  • 在原EJB2.1规范中,类必须实现ejbCreate()方法用来创建组件实例,在EJB3.0中,这一要求已被移除了。EJB3.0的兼容规则规定,如果Message-driven Bean类实现了ejbCreate()方法,将看作被@PostConstruct注解标记的方法处理。此时若同时使用@PostConstruct注解,则只能标记ejbCreate()方法。

44.3.2.2.2. onMessage方法
  • 方法必须被声明为public;

  • 方法不能被声明为final或static;

  • 返回值必须为void;

  • 方法只能有一个javax.jms.Message类型的参数;

  • 不能抛出java.rmi.RemoteException异常

  • 运行期间一般来说不应抛出RuntimeException。请参考:第 44.3.2.1.9 节 “异常处理”

44.3.2.2.3. ejbRemove方法
  • 方法名必须是ejbRemove;

  • 方法必须被声明为public;

  • 方法不能被声明为final或static;

  • 返回值必须为void;

  • 方法不能有参数;

  • 不能抛出java.rmi.RemoteException异常。

  • 在EJB3.0规范中,可使用@PreDestroy注解实现同样效果。若实现javax.ejb.MessageDrivenBean接口同时使用注解,则只能把ejbRemove()方法注解为@PreDestroy

44.3.3. 生存周期

下图表示Message-driven Bean的生存周期。

Message-driven Bean的生存周期