所有EJB3.0开发商都必须提供一个JMS provider的实现,JMS provider对于message-driven bean而言绝对是必须的。JMS是一套用于访问企业消息系统的开发商中立的API。JMS在其中扮演的角色与JDBC很相似:JDBC提供一套用于访问各种不同关系数据库的公共API,JMS也提供了独立于特定厂商的企业消息系统访问方式。JMS使用消息服务(messaging service)来帮助enterprise bean发送信息,消息服务有时也称为消息代理。JMS是专门为不同java应用系统之间传递各类信息而设计的。
使用JMS重新实现TravelAgentEJB
修改11章开发的TravelAgentEJB,使其能在预订完成时利用JMS来通知其他java应用程序。
以下相关类(如Connection)属于JSM API
@Resource(mappedName=”connectionFactoryNameGoesHere”)
private ConnectionFactory connectionFactory;//注入
@Resource(mappedName=”TicketTopic”)//注入
private Topic topic;
@Remove
public TicketDO bookPassage(CreditCardDO card,double price){
………..
Connection connect= connectionFactory.createConnection();//创建JMS provider连接
Session session=connect.createSession(true,0);//创建Session对象
// createSession(boolean transacted,int acknowledgeMode);
//这两个参数会被容器忽略,EJB规范建议分别设置为true和0
MessageProducer producer=session.createProducer(topic);//创建消息生产者
TextMessage textMsg=session.createTextMessage();//创建文本消息
textMsg.setText(ticketDescription);
producer.send(textMsg);
connect.close();
………..
}
ConnectionFactory和Topic
为了发送JMS,我们需要一个指向JMS provider的连接和一个消息目标地址。使用JMS连接工厂(JMS connection factory)可以获得JMS provider连接;而消息目标地址则可以由一个Topic(主题)对象来表示,也可以由queue(队列)来表示。
消息的类型
在JMS中,消息是一个java对象,它包含两个部分:消息头和消息正文。JMS API定义了多种不同的消息类型(TextMessage、MapMessage、ObjectMessage、StreamMessage和BytesMessage),并提供了不发送和接收这些消息的方法。
使用MapMessage的例子:
MapMessage mapMsg=session.createMapMessage();
mapMsg.setInt(“CustomerID”,ticket.customerID.intValue());
mapMsg.setInt(“Price”,ticket.price);
producer.send(mapMsg);
使用ObjectMessage的例子:
ObjectMessage objectMsg=session.createObjectMessage();
objctMsg.setObject(ticket);
producer.send(objectMsg);
JMS应用客户端
用于接收和处理乘客的预订信息,它在收到消息之后将每和票据的信息输出到屏幕
public class JmsClient_TicketConsumer implements javax.jms.MessageListener{//监听消息
public static void main(String [] args) throws Exception{
new JmsClient_TicketConsumer();
while(true) { Thread.sleep (10000); }
}
public JmsClient_TicketConsumer () throws Exception{
Context jndiContext = getInitialContext ();
ConnectionFactory factory = (ConnectionFactory)
jndiContext.lookup ("ConnectionFactory");
Queue ticketQueue = (Queue)
jndiContext.lookup ("queue/titan-TicketQueue");
Connection connect = factory.createConnection();
Session session = connect.createSession(false,Session.AUTO_ACKNOWLEDGE);
MessageConsumer receiver = session.createConsumer(ticketQueue); //消息消费者
receiver.setMessageListener(this);
connect.start ();//开始监听是否有信息到达
}
public void onMessage (Message message){//每当有新消息送达,该方法被调用
try{
ObjectMessage objMsg = (ObjectMessage)message;//强制转换成发送信息时的信息类型
TicketDO ticket = (TicketDO)objMsg.getObject ();
System.out.println ("********************************");
System.out.println (ticket);
System.out.println ("********************************");
} catch (JMSException displayed){
displayed.printStackTrace ();
}
}
public static Context getInitialContext ()throws javax.naming.NamingException{
Properties env=new Properties();
env.put(…….);//使用开发商专有属性来配置InitialContext,也可以在jndi.properties文件中设置
return new InitialContext (env);
}
}
JMS的异步性
JMS消息处理的一个主要优势它的异步性。当JMS客户端发送消息时,它并不等待回应,只是将消息发送到消息路由,由消息路由来负责将消息传递给其他客户端。而使用java RMI和JAX-RPC时,客户端以同步方式调用bean方法,当前线程会被阻塞直到方法执行完毕为止。当需要与其他应用程序时行通信时,RMI的种种不足使JMS成为一种极富吸引力的替代方案。同时发送方的事务与安全上下文是不会传播到接收方的,不过JMS客户端和JMS provider可以共同拥有一个分布式事务。
JMS的消息传递模型
JMS provider提供了两种消息传递模型:发布-订阅模型(publicsh-and-subscribe,pub/sub)和点对点模型(point-to-point,p2p),JMS规范将其称为消息传递域(messaging domain)。通过情况下,发布-订阅主要用于一对多的消息广播,而点对点则用于一对一的消息传递。
发布-订阅
在发布-订阅消息传递模型中,生产者可以通过一个被称为主题的虚拟通道,将消息发送给多个消费者。任何发送到某一主题的消息都会被转送给订阅了该主题的所有消费者。此模型基本上是一个推模型(push-based model),消息会自动广播,消费者无须通过主动请求或轮询主题方式获得新的消息。
点对点
点对点的消息传递模型允许JMS客户端通过一个被称为队列的虚拟通道,以同步或异步的方式收发消息。它是一个拉模型(pull-based model)或轮询模型,在此类模型中,消息浊自动推送给客户端的,而是由客户端从队列中获得。一个队列可以有多个接收者,但是每条消息只能由一个接收者接收。
应该选用哪一种消息传递模型
两者实际上都可以使用“推”或“拉”的方式。JMS提供了一套同时适用于这两种模型的API。采用pub/sub模型所能完成的工作几乎都可以采用点对点来完成,反之亦然。某些情况下,这取决于个人偏好,或熟悉的程度。利用pub/sub模式,JMS通过使用专门的主题,可以先对消息进行过滤,而后再分发给消费者;而p2p保证每条消息只由一个消费者处理,当消息需要被分别处理,同时又要保证消息的先后顺序时,这一能力尤为重要。
Session Bean不应该接收消息
Session Bean响应来自EJB客户端的调用,但是它们不能像message-driven bean那样响应JMS消息(消息驱动,也就是接收到消息时,一些方法自动执行),我们可以编写一个业务方法中消费JMS消息的session bean,但是这个方法必须由EJB客户端来调用。如
Connection connect = factory.createConnection();
Session session = connect.createSession(true,0);
MessageConsumer receiver = session.createConsumer(queue); //消息消费者
TextMessage textMsg=( TextMessage) receiver.receive();
connect.close();
可见使用session bean编写代码来接收信息是不明智的。
了解更多有关JMS的内容
JMS代表了分布式计算环境下一种功能强大的范式,以上对其的简介的目的是为讨论message-driven bean作铺垫的。
基于JMS的Message-Driven Bean
MDB是无状态的,事务感知的服务器组件,用于处理通过JMS进行递送的异步消息。MDB负责处理消息,而容器则负责管理包括事务、安全、资源、并发和消息确认在内的组件外环境。MDB是用来响应异步消息的,它不有远程或本地接口(方法自动调用,而非由客户端调用)
ReservationProcessor EJB
ReservationProcessor EJB是一个message-driven bean,其作用是接收代表新到预订通知的JMS消息,当它收到消息时,会创建一个新的Reservatin EJB,然后使用ProcessPayment EJB处理支付,并送出一张票据,它是TravelAgent EJB的一个自动化版本。源码如下:
@MessageDriven(activationConfig={//含义见下一节
@ActivationConfigProperty(propertyName="destinationType",
propertyValue="javax.jms.Queue"),
@ActivationConfigProperty(propertyName="acknowledgeMode",
propertyValue="Auto-acknowledge")})
public class ReservationProcessorBean implements javax.jms.MessageListener {
//MessageListener 监听器接口只定义了onMessage()方法
@PersistenceContext(unitName= "titanDB")//注入
private EntityManager em;
@EJB
private ProcessPaymentLocal process;
@Resource(mappedName="ConnectionFactory")
private ConnectionFactory connectionFactory;
public void onMessage(Message message) {
//类似于TravelAgentEJB的bookPassage()方法,此方法会自动执行,所以称为自动化
System.out.println("Received Message");
try {
//从收到的消息中取出数据,类似于得到请求参数
MapMessage reservationMsg = (MapMessage)message;
int customerPk = reservationMsg.getInt("CustomerID");
int cruisePk = reservationMsg.getInt("CruiseID");
int cabinPk = reservationMsg.getInt("CabinID");
double price = reservationMsg.getDouble("Price");
// 获取信用卡
Date expirationDate = new Date(reservationMsg.getLong("CreditCardExpDate"));
String cardNumber = reservationMsg.getString("CreditCardNum");
String cardType = reservationMsg.getString("CreditCardType");
CreditCardDO card = new CreditCardDO(cardNumber, expirationDate, cardType);
Customer customer = em.find(Customer.class, customerPk);
Cruise cruise = em.find(Cruise.class, cruisePk);
Cabin cabin = em.find(Cabin.class, cabinPk);
//创建代表舱位预订记录的Reservation
Reservation reservation = new Reservation(customer, cruise, cabin, price,
new Date());
em.persist(reservation);
//处理信用卡支付
process.byCredit(customer, card, price);
TicketDO ticket = new TicketDO(customer,cruise,cabin,price);
deliverTicket(reservationMsg, ticket);//发送信息给客户端,见下文
}
catch(Exception e) {
throw new EJBException(e);
}
}
//MDB也可以使用JMS来发送信息
public void deliverTicket (MapMessage reservationMsg, TicketDO ticket) throws JMSException, NamingException{
Queue queue = (Queue)reservationMsg.getJMSReplyTo();//从接收的信息中得到回复消息目标地址
Connection connect = connectionFactory.createConnection();
Session session = connect.createSession(true,0);
MessageProducer sender = session.createProducer(queue);
ObjectMessage message = session.createObjectMessage();
message.setObject(ticket);//把ticket发送到客户端
sender.send(message);
connect.close();
}
}
@MessageDriven
MDB使用@javax.ejb.MessageDriven注解。
@ActivationConfigProperty
@Message-Driven.activationConfig()属性接受一组@ ActivationConfigProperty注解作为参数,用一组简单的“名/值”对来描述MDB配置.EJB3.0为基于JMS的message-driven bean定义了一组固定属性,它们是:acknowledgeMode、messageSelector、destinationType和subscriptionDurability
消息选择器(message Selector)
消息选择器让MDB选择性地接收来自特定主题或队列的消息,它以SQL-92条件表达式语法(SQL中的where子句)的一个子集为基础的,如:
@ActivationConfigProperty(propertyName="messageSelector", propertyValue="MessageFormat=’Versin 3.4’")
JMS消息生产者通过以下代码设置Message的MessageFormat属性
Message message=session.createMapMessage();
message.setStringProperty(“MessageFormat”,”Versin 3.4”);
确认模式
JMS确认是指JMS客户端通知JMS provider确认消息已经到达的一种机制。在EJB中,收到信息后发送确认是MDB容器的职责。
@ActivationConfigProperty(propertyName="acknowledgeMode", propertyValue="Auto-acknowledge")
确认模式有两个取值:Auto-acknowledge和Dups-ok-acknowledge。前者告诉容器,在消息交给MDB实例处理后,容器应该立即向JMS provider发送确认。后者容器任何时间发送确认都是可接受的,由于MDB容器可能延迟很长时间才发送确认,JMS provider可能认为容器没有收到消息而重发信息,所以使用这种模式时,MDB必须能够正确处理重复消息。在实际上大多数确认模式都会被忽略,如果事务由容器来管理,确认行为是在事务上下文中执行:如果事务成功,消息就会被确认;如果事务失败,消息就不会被确认
订阅的持续性
当基于JMS的MDB使用javax.jms.Topic时,我们必须在部署描述文件中声明:这一订阅是Durable或NonDurable。如果EJB容器由于某些原因失去与JMS provider的连接,Durable表示,在此期间Bean本应收到的消息是不会丢失的,当容器重新获得连接后,这些消息会由容器发送给MDB,这种行为称为保存-转发消息传递机制。声明为NonDurable的MDB,会丢失连接期间本应收到的消息,所以它可以改善JMS provier的性能,但降低MDB的可靠性。声明方式:
@ActivationConfigProperty(propertyName="subscriptionDurability", propertyValue="Durable")
ReservationProcessor EJB的部署描述文件
<?xml version="1.0"?>
<ejb-jar …...>
<enterprise-beans>
<message-driven>
<ejb-name>ReservationProcessorBean</ejb-name>
<ejb-class>com.titan.reservationprocessor. ReservationProcessorBean </ejb-class>
<message -type>javax.jms.MessageListener</ message -type>
<transacton-type>Container</transacton-type >
<message-destination-type>javax.jms.Queue</message-destination-type>
<activation-config>
<activation-property>
<activation-config-property-name>destinationType</activation-config-property-name>
<activation-config-property-value>javax.jms.Queue</activation-config-property-value>
<activation-property>
…………
<activation-config>
<ejb-local-ref>
<ejb-ref-name>ejb/PaymentProcessor</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local>com.titan.processpayment.ProcessPaymentLocal</local>
<injection-target>
<injection-target-class>
com.titan.reservationprocessor. ReservationProcessorBean
</injection-target-class>
<injection-target-name>process</injection-target-name>
</injection-target>
</ejb-local-ref>
<persistence-context-ref>
<persistence-context-ref-name>persistence/titan</persistence-context-ref-name>
<persistence-unit-name>titan</persistence-unit-name>
<injection-target>
<injection-target-class>
com.titan.reservationprocessor. ReservationProcessorBean
</injection-target-class>
<injection-target-name>em</injection-target-name>
</injection-target>
</persistence-context-ref>
<resource-ref>
<res-ref-name>jms/ConnectionFactory</res-ref-name>
<res-type>javax.jms.ConnectonFactory </res-type>
<res-auth>Container</res-auth>
<mapped-name>ConnectionFactory</mapped-name>
<injection-target>
<injection-target-class>
com.titan.reservationprocessor. ReservationProcessorBean
</injection-target-class>
<injection-target-name>connectionFactory</injection-target-name>
</injection-target>
</resource-ref>
</message-driven>
</enterprise-beans>
</ejb-jar>
ReservationProcessor客户端
发送预订消息的客户端
代码实现见前面的章节“使用JMS重新实现TravelAgentEJB”
接收票据信息的客户端
代码实现见前面的章节“JMS应用客户端”
Message-driven bean的生命週期
MDB实例的生命周期有两个状态:Does Not Exist和Method-Ready Pool.
Does Not Exist状态
MDB没有被实例化。
Method-Ready Pool状态
和stateless session bean对应状态相似,也使用了实例池。EJB服务器首次启动时,它可能创建一些MDB实例,当MDB实例不足以处理发来的消息时,容器会调用bean class的Class.newInstance()方法创建一个新的bean实例,然后根据bean的元数据或XML声明注入相关资源,如果bean定义了PostConstruction回调函数,容器最后会调用该方法。当有消息送递MDB时,容器会将这条消息委派给任何一个处于MRP状态的bean实例。当服务器移除一部分内存对象以减少处于MRP状态的bean总数时,MDB将到达Does Not Exist状态,这时可能触发@PreDestroy回调事件。
基于连接器的Message-Driven Bean
基于JMS的MDB非常有用,但它也存在局限:EJB开发商往往只支持少量的JMS provider(消息收发双方都使用相同的JMS API)。它只支持JMS编程模型而不支持其他类型的消息系统。EJB3.0还支持一个可扩展的message-driven bean定义,使之可以对任何开发商的任何消息系统提供服务,即基于JCA的MDB,JCA定义了EJB容器和异步连接器之间的规则,从而使用MDB可以自动处理来自EIS的消息。JCA连接器可以从开发商X处购得,使用JCA连接器的MDB,可以自动处理开发商X开发的EIS系统发送过来的消息。例子,一个处理email的MDB
package com.vendorx.email;//来自开发商X的JCA
public inteface EmilListener{
public void onMessage(javax.mail.Message message);
//这个方法可以有不同形式的方法名称和签名,并允许拥有返回值,由JCA开发商决定
}
@MessageDriven(activationConfig={//属性依赖于连接器的类型及具体要求
@ActivationConfigProperty(propertyName="mailServer",
propertyValue="mail.ispx.com"),
@ActivationConfigProperty(propertyName="serverType",
propertyValue="POP3")})
public class EmailBean implements com.vendorx.email.EmilListener{//实现JCA开发商的接口
public void onMessage(javax.mail.Message message){
javax.mial.internet.MimeMessage msg=( javax.mial.internet.MimeMessage)message;
Address[] addressed=msg.getFrom();//来自那里的email.
……..
}
}
消息连接
允许由任一enterprise bean发出的消息,能够最终路由到位于 同一部署环境中的指定message-driven bean处,通过使用消息连接,可以在同一应用程序内的不同组件之间控制消息流转。可以定义一个TicketDistributor EJB分发由TravelAgent EJB发送到同一个JMS主题的消息,专门定义一个MDB来处理分发工作可以获得列好的灵活性和系统性能。EJB规范没有为消息连接提供注解,必须使用XML部署描述文件来使用这个功能。
<session>
…..
<message-destination-link>Distributor</message-destination-link>
…..
</session>
<message-driven>
<ejb-name>TicketDistributorEJB</ejb-name>
<message-destination-link>Distributor</message-destination-link>
</ message-driven >
<assembly-descriptor>
<message-destination>
<message-destination-name>Distributor</message-destination-name>
</message-destination>
<assembly-descriptor>
这时由session bean 发送到目标地址的消息会由MDB TicketDistributorEJB收到转发。session bean可以发送消息,但不要接收消息。
转载:http://zscomehuyue.iteye.com/blog/627218