技术开发 频道

JCA 1.5: 消息输入流

  【IT168 技术文章】

         EJB 2.0 消息驱动 bean

  在使用 J2EE 1.3 和 EJB 2.0 时,消息驱动 bean(MDB)只能起到相当有限的作用。它们让应用程序异步地接收传递 JMS 目标的消息。而 MDB 需要实现 javax.jms.MessageListener,如清单 1 所示:

  清单 1. javax.jms.MessageListener 接口

1 public interface MessageListener {
2     void onMessage(Message message);
3 }

  MDB 还需要实现 MessageDrivenBean 接口。结果,典型的类看起来可能类似清单 2 中的 ExampleMdb :

  清单 2. 示例 EJB 2.0 MDB

1 public class ExampleMdb implements MessageDrivenBean, MessageListener {
2     public void ejbCreate() throws CreateException { ... }
3     public void ejbRemove() { ... }
4     public void setMessageDrivenContext(MessageDrivenContext ctx) { ... }
5     public void onMessage(Message message) {
6         if (msg instanceof TextMessage) {
7             String text = ((TextMessage) message).getText();
8             InitialContext context = new InitialContext();
9             ExampleHome home = (ExampleHome)context.lookup("java:comp/env/ejb/Example");
10             Example bean = home.create();
11             bean.operation(text);
12         }
13     }
14 }

  如果像清单 2 那样遵照 EJB 2.0 规范的建议,那么 onMessage 方法负责对消息的内容进行解包,然后调用其他 EJB 执行实际的业务逻辑。

  但是别误会 —— EJB 2.0 MDB 当然有它们的好处。对于初学者来说,因为 MDB 不是由客户机调用的,所以不需要编写 home、remote 或 local 接口。而且 —— 最主要的一个好处就是,多个实例可以并行操作。在没有 MDB 时,应用程序可能已经确定了一个异步接收消息的目标,但是由于没有将工作复制到另外一个线程中的能力,所以应用程序须先处理第一个消息才能处理下一个消息。

  就像其他 EJB 那样,MDB 能够使用 bean 托管事务或者容器托管事务。后者对 MDB 有一些扭曲。首先,只允许使用两个事务属性:Required 和 NotSupported。这是完全合理的。因为没有客户机调用 MDB,没有需要继承的事务,所以要做的就是能够指出 bean 是否应当在事务中运行。第二,也是更重要的,如果指定了事务属性 Required,那么传递到 MDB 的消息就作为消息的一部分接收。如果有什么事情出错,事务将回滚,那么消息将被重新放在目标上,再次进行处理。

  正如在清单 3 中可以看到的,MDB 只用于 JMS 的这个事实也反映在部署描述符中:

  清单 3. EJB 2.0 部署描述符

1 <ejb-jar>
2   <enterprise-beans>
3     <message-driven>
4       <ejb-name>Example MDB</ejb-name>
5       <ejb-class>example.ExampleMDB</ejb-class>
6       <transaction-type>Bean</transaction-type>
7       <acknowledge-mode>Auto-acknowledge</acknowledge-mode>
8       <message-driven-destination>
9         <destination-type>javax.jms.Topic</destination-type>
10         <subscription-durability>Durable</subscription-durability>
11       </message-driven-destination>
12       <message-selector>
13         JMSType = 'car' AND color = 'red'
14       </message-selector>
15     </message-driven>
16   </enterprise-beans>
17 </ejb-jar>
18

 

  在清单 3 中可以看到,部署描述符中的一些元素对应着 JMS 概念中的应答模式、订阅、持久性和消息选择器。另外一个元素给出了消息目标的类型,它必须是 javax.jms.Queue 或者是 javax.jms.Topic。

  

         EJB 2.1 消息驱动 bean

  那么在 J2EE 1.4 的 EJB 2.1 中发生了什么变化呢?有一个重大的区别:MDB 现在可以实现任何接口。没错 —— 任何 接口。例如,JCA 公共客户端接口(CCI)定义了清单 4 所示的接口,希望由 CCI 连接器异步驱动的 MDB 应当实现这些接口:

  清单 4. javax.resource.cci.MessageListener 接口

1 public interface MessageListener {
2     Record onMessage(Record inputData)
3             throws ResourceException;
4 }

  CCI 接口展示了新的灵活性的一些好处。首先,不需要把 MDB 传递给 JMS 消息。在清单 4 的接口中,传入一个 Record 对象。JMS 消息到达目标时也无需触发方法调用。对于 MDB 会在什么情况下驱动,JCA 规范没有做任何限制。它可以是资源适配器接收到后端系统的一些外部刺激之后的结果,也可以仅仅是一些内部事件,甚至可能是由计时器驱动的事件。第二,如清单 4 所示,调用的方法也可以拥有返回参数。在这种情况下,就可以传递回第二个 Record 对象,正如清单 5 的 ExampleListener 接口中的第一个方法所示,类型无需相同:

  清单 5. listener 接口示例

1 public interface ExampleListener {
2     ExampleOutput process(ExampleInput input);
3     String getData();
4     void request(int id, ExampleRequest request);
5 }

  通常应当用方法的返回参数向造成方法调用事件的发起者提供响应,不论它是资源适配器还是其他什么系统。但是,就像 ExampleListener 接口的第二个方法所表现的那样,实际上可以不用任何参数调用 MDB,只用它检索一些值。第三个方法表明:方法可以使用的参数个数没有限制。这意味着资源适配器可以把初始事件解析为将传递给 MDB 的多个值。可以看到,方法的参数可以是原生类型或是对象。最后,即使 ExampleListener 接口有多个方法,使用它也是正确的。这样就可以让资源适配器根据事件判断要调用哪个方法。

  既然 MDB 接口中消除了与 JMS 有关的限制,那么将部署描述符放在哪呢?清单 6 显示了一个 EJB 2.1 部署描述符示例,针对的是实现 ExampleListener 接口的 MDB:

  清单 6. EJB 2.1 部署描述符

1 <ejb-jar>
2   <enterprise-beans>
3     <message-driven>
4       <ejb-name>Example MDB</ejb-name>
5       <ejb-class>example.ExampleMdb</ejb-class>
6       <messaging-type>example.ExampleListener</messaging-type>
7       <transaction-type>Bean</transaction-type>
8       <activation-config>
9         <activation-config-property>
10           <activation-config-property-name>
11             HostName
12           </activation-config-property-name>
13           <activation-config-property-value>
14             example.com
15           </activation-config-property-value>
16         </activation-config-property>
17         <activation-config-property>
18           <activation-config-property-name>
19             DefaultId
20           </activation-config-property-name>
21           <activation-config-property-value>
22             2001
23           </activation-config-property-value>
24         </activation-config-property>
25       </activation-config>
26     </message-driven>
27   </enterprise-beans>
28 </ejb-jar>
29

  在清单 6 中可以看到,messaging-type 元素包含 MDB 实现的那些接口的类名。如果遗漏了这个元素,那么可以采用 javax.jms.MessageListener 的旧值。特定于 JMS 的元素已经被一组通用的名称-值对替代,这个名称-值对叫作 激活配置属性(activation-configuration properties)。在清单 6 中可以看到,为 HostName 和 DefaultId 属性分别指定了值 example.com 和 2001。

  可以编写实现任意接口的 MDB,这听起来太棒了,感觉不像是真的——可是它确实是真的。接下来的事可能就不太让人惊讶了:需要找到一个准备调用这个接口的资源适配器。资源适配器用它的部署描述符中的 messagelistener-type 元素表示它准备支持的接口(可能不止一个接口),如清单 7 所示:

  清单 7. 输入资源适配器的部署描述符

1 <connector>
2 <display-name>Example Inbound Resource Adapter</display-name>
3 <vendor-name>Example COM</vendor-name>
4 <eis-type>Example EIS</eis-type>
5 <resourceadapter-version>1.0</resourceadapter-version>
6 <resourceadapter>
7   <resourceadapter-class>
8    example.ExampleRaImpl
9   </resourceadapter-class>
10   <inbound-resourceadapter>
11    <messageadapter>
12     <messagelistener>
13      <messagelistener-type>example.ExampleListener</messagelistener-type>
14      <activationspec>
15       <activationspec-class>example.ExampleActivationSpecImpl</activationspec-class>
16       <required-config-property>
17        <config-property-name>HostName</config-property-name>
18       </required-config-property>
19       <required-config-property>
20        <config-property-name>PortNumber</config-property-name>
21       </required-config-property>
22      </activationspec>
23     </messagelistener>
24    </messageadapter>
25   </inbound-resourceadapter>
26 </resourceadapter>
27 </connector>
28

  详细看看这个示例部署描述符是值得的。它从一些传统的东西开始:显示名称、供应商名称、资源适配器连接的企业信息系统(EIS)的名称,以及适配器的版本。这些内容后面跟着一个 resourceadapter 元素,它的第一个子元素包含 ResourceAdapter 接口的适配器实现的名称。后面跟着的是 inbound-resourceadapter 元素,它可以包含多个 messageadapter 实例。每个 messageadapter 实例都与一个 messagelistener-type 相关联。所以通过拥有多个 messageadapter 元素,可以编写一个输入资源适配器,支持多个接口。但是,必须要保证每个 messageadapter 指定一个不同的接口。

  每个 messageadapter 还包含实现 ActivationSpec 接口的类的名称。在下一节中,我将介绍如何用这个类包含每个部署的 MDB 的配置信息。

        部署 MDB

  正如在前面一节看到的,资源适配器支持的每个 MDB 接口都给出实现 ActivationSpec 接口的类的名称,如清单 8 所示:

  清单 8. javax.resource.spi.endpoint.ActivationSpec 接口

1 public interface ActivationSpec extends ResourceAdapterAssociation {
2     void validate() throws InvalidPropertyException;
3 }

  这个实现类遵循 JavaBean 标准,可以通过适当指定的 getter 和 setter 方法来定义大量属性。在将 MDB 部署到应用服务器中时,将使用这个类,如下所示:

  应用服务器通过查看 MDB 部署描述符中 messaging-type 元素的内容,判断 MDB 实现的接口。

  然后应用服务器找到支持这个接口的已经部署的资源适配器,选择一个想在上面部署 MDB 的适配器。

  应用服务器创建 ActivationSpec 类的实例,这个类对应于选中的资源适配器和接口,然后使用内省(introspection)功能确定类的属性以及默认值。

  用 ResourceAdapter 的父类上同名配置的属性覆盖默认值。

  可以选择覆盖这些属性值中的任何值。

  然后这些属性被 MDB 部署描述符中的 activation-config 属性的值再次覆盖。

  应用服务器检测要求在资源适配器部署描述中指定的所有 bean 属性(在这个示例中,是 HostName 和 PortNumber)都有提供的值。

  可以看到,部署的 MDB 最终使用的属性可以来自许多地方,重要的是记住它们的先后顺序。特别是要注意 MDB 的部署描述符不需要包含资源适配器要求的所有属性。

  一旦所有属性设置就绪,应用服务器就可以选择调用 ActivationSpec 上的 validate 方法,或者把这个方法的调用推迟到应用程序启动时。资源适配器可能用这个方法检查算术属性是否在可以接受的范围之内,或者确保表示枚举值的字符串有效。资源适配器还应当检查最后的属性集是否一致;例如,可能要根据其他属性的值来判定某个属性的值是否受限或是必需的。InvalidPropertyException 包含一项或多项检测失败时需要抛出的一组无效属性。

  成功的验证并不意味着部署的 MDB 肯定能成功启动。资源适配器只能执行静态检测,也就是说,执行那些不需要连接到后台系统的检测。只有当应用程序启动时,资源适配器才能验证类似于主机名称和端口号这样的属性是否正确。

  MDB 生命周期管理

  一旦成功地把包含 MDB 的应用程序部署到应用服务器中,下一步就是启动应用程序。应用服务器用清单 9 中的 ResourceAdapter 接口中的两个方法,向资源适配器通知关于某个具体 MDB 生命周期的事件(在规范中称之为 端点):

  清单 9. ResourceAdapter 接口上的端点生命周期方法

1 public interface ResourceAdapter {
2     void endpointActivation(MessageEndpointFactory endpointFactory,
3             ActivationSpec spec) throws ResourceException;
4     void endpointDeactivation(MessageEndpointFactory endpointFactory,
5             ActivationSpec spec);
6     ...
7 }

  在启动应用程序时,要为每个部署的 MDB 调用一次 endpointActivation 方法。第一个参数(我将在下一节详细介绍)是一个创建端点实例的工厂类。方法的第二个参数是在前一节中配置的 ActivationSpec。如果以前没有做过,那么现在可以这样做,此刻,资源适配器通常使用来自 ActivationSpec 的信息与后端系统建立某种形式的远程连接。如果在做这项工作的过程中,资源适配器判定配置的信息不正确,那么它将抛出 NotSupportedException。

  注意,资源适配器不应当阻塞在这个方法中。一旦它确定配置是正确的,则应当立即返回。如果需要进行进一步处理(例如为新的事件定期申请连接),那么资源适配器应当使用 WorkManager 接口,把工作安排到另外一个线程上。

  在包含 MB 的应用程序被停止时,或者处在应用服务器的正常停机期间,会调用对应的 endpointDeactivation 方法。传递给这个方法的参数就是在 endpointActivation 上传递的同一个对象。实际上,因为 JCA 规范强制要求,对于每个端点激活都要创建 MessageEndpointFactory 的一个新实例,所以在不活动(on deactivation)期间传递的对象可以用作在两者之间进行相互关联的键。例如,在激活时创建的资源可能放在一个切断与 MessageEndpointFactory 的联系的映射中,然后在服务器不活动的时候,再检索出这些资源,对它们进行清理。

  调用 MDB

  现在所有工作都已经就绪,到了资源适配器实际调用 MDB 的时候了。在使用 J2EE 1.3 时,这一步可能将使用一个相当麻烦的应用服务器工具,该工具是 JMS 规范的一个组成部分。交互很复杂,而且在某些地方,交互的指定也很糟,从而造成不同的厂商用不同的方式对需求进行解释。幸运的是,JCA 规范不仅让 MDB 能够实现任何接口,还极大地简化了这一过程中的事物。

  清单 10 显示了传递给 endpointActivation 方法的对象实现的 MessageEndpointFactory:

  清单 10. javax.resource.spi.endpoint.MessageEndpointFactory 接口

1 public interface MessageEndpointFactory {
2     boolean isDeliveryTransacted(Method method) throws NoSuchMethodException
3     MessageEndpoint createEndpoint(XAResource xaResource) throws UnavailableException;
4 }

  isDeliveryTransacted 方法让资源适配器判断关联的 MDB 是否运行在事务中调用指定方法。如果 MDB 正在使用容器托管的事务,而且要处理的方法具有 Required 事务属性,那么 isDeliveryTransacted 会返回 true 。如果事务属性采用 NotSupported 允许的其他值,或者 MDB 正在使用 bean 托管的事务,则返回 false。

  我将在下一节中介绍事务。此刻,假设 isDeliveryTransacted 返回了 false (或者假设资源适配器不支持事务)。这意味着在调用 createEndpoint 方法时,可以把 null 作为 XAResource 的参数传入。这个方法返回一个实现了 MessageEndpoint 接口的对象,如清单 11 所示:

  清单 11. javax.resource.spi.endpoint.MessageEndpoint 接口

1 public interface MessageEndpoint {
2     void release();
3     void beforeDelivery(Method method)
4             throws NoSuchMethodException, ResourceException
5     void afterDelivery() throws ResourceException;
6 }

  返回的对象还实现了 MDB 在它的部署描述符中声明的接口。在最简单的情况下,资源适配器就可以把 MessageEndpoint 的类型转换成必要的接口,然后调用预期方法。

  createEndpoint 返回的对象显然不是应用程序开发人员实现的类的实例,因为这个类没有实现 MessageEndpoint 接口。相反,它是应用服务器创建的一个代理。应用服务器可能使用了 Java 1.4 引入的动态代理支持,即时创建对象,它也可能在部署应用程序时就创建了必要的类。容器使用代理来包装实际的端点,以便提供事务和安全性这样的服务。这里比较一下代理和对无状态 bean 本地接口的引用。在使用会话 bean 的情况下,只能调用在本地接口中声明的方法。类似地,对于代理,也只能调用在 MDB 的部署描述符中声明的接口上的方法。

  资源适配器可以选择保持在一个端点上,以便在后续调用中使用它。或者,资源适配器在完成任务并为下一调用创建另一个端点时,也可以调用 release。通常,应用服务器保持一个端点池,所以第二个选项或许是为了在多个线程之间提供更好的重用而提供的。

  我说过这代表的是简单情况。JCA 规范把这称作 Option A 消息传递。作为何时需要可供替代的 Option B 消息传递的一个例子,可以将资源适配器想像为代表应用程序传输序列化的 Java 对象的消息传递系统的一部分。希望调用的 MDB 方法采用反序列化之后的对象作为参数。糟糕的是,与序列化对象对应的类是 J2EE 程序程序的一部分,因此该类位于应用程序的类路径中,而不是位于资源适配器的类路径中。Option B 用 beforeDelivery 方法和 afterDelivery 方法圆满地解决了这个问题。调用 beforeDelivery 会提前执行一些容器操作(这些操作正常情况下可能是在调用代理和调用实际端点方法之间发生的)。这些操作包括把应用程序的类加载器和线程关联起来。调用 beforeDelivery 之后,资源适配器就可以用线程适配器将对象反序列化,并把对象传递给 MDB 接口上要求的方法。

  容器知道自己已经执行了一些在这个端点代理上要求的操作,所以会在调用实际的 MDB 方法时跳过这些方法。但是,被调用的方法必须和 beforeDelivery 上传递的 Method 对象匹配,否则就会抛出 RuntimeException。在调用该方法之后,资源适配器必须调用 afterDelivery 来执行方法调用之后应当发生的对应的容器操作。例如,这样就允许适配器将方法返回的对象序列化,不过仍然是通过使用应用程序的类加载器来实现的。

  图 1 显示了两个消息传递选项的并列式比较。

  图 1. 消息传递选项

  对于 Option A,可以看到前调用逻辑和后调用逻辑是作为一个方法调用的部分执行的,而对于 Option B,它们被分开,分别由 beforeDelivery 和 afterDelivery 驱动。

  MDB 和事务

  如果资源适配器支持 XA (全局) 事务,而且 isDeliveryTransacted 表明 MDB 期望容器启动一个事务,那么资源适配器就应当在调用 createEndpoint 时传入一个 XAResource 接口的实现。XA 事务是个复杂的主题,与 JCA 无关的问题超出本文的范围。对于更多细节,请参考 Java 事务 API 规范 。

  对于如何进行处理,资源适配器有两个选项。它可以选择把要执行的事务性工作直接与它的 XAResource 对象关联起来。资源适配器就可以使用 Option A 消息传递。当调用代理时,容器会调用 start,列举出事务中的 XAResource。这个时候,资源适配器可以把指定的 Xid 与要执行的工作关联起来。代理继续调用端点实例。在端点方法返回之后,容器就完成了事务。当 XAResource 接收到对 prepare 的调用时,它应当保证自己能够在返回 XA_OK 之前成功完成要求的工作。最后,XAResource 接收 commit 或 rollback,这时它应当提交或取消已经执行的工作。

  或者,资源适配器也可以使用 Option B 消息传递,在这种情况下,事务是作为 beforeDelivery 的一部分启动的。这样就允许资源适配器在调用 MDB 的方法之前从 XAResource 检索 Xid。例如,如果在自己能够构建传递给方法的参数之前,资源需要把 Xid 传递给后端系统,那么这可能是必需的。可以想像,在这种情况下,事务是在调用 afterDelivery 的时候完成的。

  如果应用服务器在调用 prepare 和 commit、rollback 之间失败,那么在服务器重启时需要进行事务恢复。为了执行恢复,应用程序服务器需要为每个资源管理器获取一个 XAResource。应用服务器实现这一目标的方式是:在调用 prepare 之前,把与端点关联的 ActivationSpec 写入永久性存储器。在恢复的时候,服务器读出表示每个资源适配器的 ActivationSpec 对象列表,这些适配器对应着它认为没有完成的事务。这个对象数组将传递给 ResourceAdapter 接口上的 getXAResources 方法,如清单 12 所示:

  清单 12. ResourceAdapter 接口上的事务恢复方法

1 public interface ResourceAdapter {
2     XAResource[] getXAResources(ActivationSpec[] specs)
3             throws ResourceException;
4     ...
5 }

  应用服务器调用每个 XAResource 对象上的 recover 方法来确定资源管理器准备好的事务。然后根据情况为每个事务调用 commit 或 rollback。

  在前文中,介绍了 ExecutionContext 如何与 WorkManager 结合,把事务导入应用服务器。如果用导入的事务调用具有事务属性 Required 的端点方法,那么这个方法将继承该事务。在这种情况下,任何 XAResource passed on createEndpoint 上传递的方法都不会列在该事务中。

  托管对象

  在最后一节中,我将介绍的一个主题是:可以平等地用于输入和输出 资源适配器,虽然这一主题在 JCA 规范的消息输入流一章中层介绍过。规范的 1.5 版本的目标之一就是允许 JMS 提供者作为 JCA 资源适配器实现。如果了解 JMS,那么就会注意到它包含两类可以管理的对象:连接工厂和目标。通过托管连接工厂和它们在资源适配器的部署描述符中定义的属性,JCA 提供了一种机制,使管理员可以定义连接工厂,以便部署的程序能够通过 Java 名称与目录服务接口(JNDI)查找它们。但是 JMS 目标不是工厂,而是拥有不同属性(描述它们表示的队列或主题)的对象。如果这样,那么怎么才能创建 JMS 目标呢?

  JCA 1.5 规范定义了托管对象 来弥补这个空白。资源适配器的部署描述符可以包含一个或多个 adminobject 元素,如清单 13 所示:

  清单 13. 显示托管对象的部署描述符代码段

1 <adminobject>
2    <adminobject-interface>example.ExampleAdminObject</adminobject-interface>
3    <adminobject-class>example.ExampleAdminObjectImpl</adminobject-class>
4    <config-property>
5     <config-property-name>Color</config-property-name>
6     <config-property-type>java.lang.String</config-property-type>
7      <config-property-value>Red</config-property-value>        
8    </config-property>
9    <config-property>
10     <config-property-name>Depth</config-property-name>
11     <config-property-type>java.lang.Integer</config-property-type>
12    </config-property>
13   </adminobject>
14

  在这种情况下,应用程序期望从 JNDI (通过资源环境引用)查找实现了 example.ExampleAdminObject 接口的对象。部署描述符提供了能提供这个对象实现的 JavaBean 类的名称。它还提供了许多命名属性,每个属性都有一个类型和一个可选的默认值,这种方式非常类似于托管连接工厂。当管理员希望定义一个对象时,应用程序服务器的工具应当允许管理员选择资源适配器和托管对象接口,然后提示管理员输入每个属性的值。应用服务器随后将包含这个信息的引用绑定到 JNDI。当应用程序执行查找时,就创建一个托管对象的实例,而且在把对象返回应用程序之前设置好属性。

  现在应当能够看出 JCA 资源适配器如何通过使用托管对象来提供 JMS 目标对象了。但是请不要忘记,您可以对任何想让管理员能够配置的 JavaBean(不仅仅是连接工厂)使用托管对象,这些 JavaBean 可以通过 JNDI 访问。

  结束语

  在本文中,我解释了消息驱动 bean 在 J2EE 1.3 和 J2EE 1.4 之间的变化,特别是目前如何实现任何接口。您已经看到如何用 JCA 消息输入流合约来配置资源适配器,以便用它来检索 MDB 实例以及调用 MDB 上的方法。我还详细介绍了调用 bean 方法的选项,以及调用事务性方法的意义。最后,我介绍了托管对象如何使用资源适配器定义将要配置的 JavaBean,并使其能够通过 JNDI 访问。

  这一文章系列介绍了 JCA 1.5 的一些新特性,从优化和生命周期管理开始,到工作管理和事务,最后以消息输入流合约告终。像过去一样,规范提供了信息的确定来源,而且肯定会在未来的版本中继续发展。我希望您觉得这个系列有用,不管您是打算升级现有的资源适配器,还是打算从头编写一个新的资源适配器,或者仅仅把资源适配器用作应用程序的一部分。

0
相关文章