Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3471829
  • 博文数量: 1450
  • 博客积分: 11163
  • 博客等级: 上将
  • 技术积分: 11101
  • 用 户 组: 普通用户
  • 注册时间: 2005-07-25 14:40
文章分类

全部博文(1450)

文章存档

2017年(5)

2014年(2)

2013年(3)

2012年(35)

2011年(39)

2010年(88)

2009年(395)

2008年(382)

2007年(241)

2006年(246)

2005年(14)

分类: Java

2006-06-20 17:19:00

 

我是如何学会使代码与任一 JMS 实现一起工作的

developerWorks

 


级别: 初级

Nicholas Whitehead, Java 架构设计师, finetix LLC

2002 年 2 月 26 日

可能是由于实时消息传递领域几个巨头的支持,Java 消息服务(Java Message Service)近来日渐普及。由于不断有新的供应商加入 JMS 的潮流之中,因此确保您的 JMS 代码可以不加修改运行于多个专有实现是很有意义的。Java 架构设计师 Nicholas Whitehead 用几个简单的步骤向您演示如何将 JMS、Java 命名和目录接口(Java Naming and Directory Interface)和精心设计的特性文件结合在一起以构建独立于供应商的 JMS 解决方案。请单击本文顶部或底部的 论坛与作者和其他读者在 中就本文交流思想。

Java 消息服务(JMS)规范制定了基于 Java 点到点(P2P)和发布/订阅(P/S)消息传递的标准。Sun 目前列出了 12 个经过许可的 JMS 实现者和 16 个未经许可的实现者。就体系结构而言,JMS 与 Java 数据库互连(Java Database Connectivity (JDBC))API 相似,因为它们都只定义了少数几个类而定义了很大集合的接口。这些接口有待实现,而符合这些接口的实现的行为是一样的。

对大多数数据库而言,这种行为的相似性因 JDBC 接口实现而丧失。与 SQL 符合性级别的差异和专有过程型 SQL 扩展(如 Oracle 的 PL/SQL 和 Sybase 的 Transact-SQL)的使用会使得为访问和使用不同的数据库服务而编写的代码有很大的差异。

而 JMS 就不是这样。只要最少的工作并遵循我在本文中所推荐的过程,您就可以使您的 JMS 客户机代码顺畅地运行,而丝毫觉察不到所使用的供应商实现的差异。虽然我假设您对 JMS 消息处理有基本的了解,但我们还是将对基本概念和术语进行简短回顾,来开始这次讨论。

发送和接收消息的基础是 连接,它负责分配 JVM 之外的资源。JMS 供应商通常至少为 P2P 事务实现一个 QueueConnection ,至少为 P/S 事务实现一个 TopicConnection 。这些连接提供一个 Session ,它是管理消息发送和接收的结构。

P2P 事务管理的基本结构是 QueueSenderQueueReceiver 。用于 P/S 事务管理的基本结构是 TopicSubscriberTopicPublisher 。Topic 和 Queue 对象封装了导向每条消息的目标和源的特定信息。这一层次结构如图 1 所示:



JMS 类层次结构图

其它特定于应用程序服务器的结构(如请求/响应支持类)和特性可以在 JMS 标准中找到(请参阅 参考资料)。

因为连接是和 JMS 服务器交互的入口点,因此连接接口的每个实现必须知道如何与它自己的 JMS 服务器的一个实例连接。因为底层连接协议的详细信息往往因供应商不同而有所不同,所以设置活动连接所需的信息也会因供应商而异。

大多数供应商允许动态地设置连接。也就是说,他们将连接类的构造器定义成公共的(public),允许程序员定义所需连接信息。大多数供应商提供在调用后可以返回一个连接的工厂类。

就连接工厂而言,工厂类可以返回一个已预先装入专有连接信息的连接。供应商定义的工厂类将提供方法以允许程序员设置连接参数。这些连接参数指示工厂返回的连接的性质。





回页首


为了使所有这些更加具体,让我们看看 QueueConnectionQueueConnectionFactory 的几个实现的构造器、连接工厂和设置方法。(请注意,有些情况下会有许多重载的构造器;对每种情况我只举例说明一个构造器。)

IIT SwiftMQ 2.1.3 QueueConnectionFactory 构造器参数

  • java.lang.String socketFactory :套接字工厂的类名称
  • java.lang.String hostname :JMS 服务器的主机名
  • int port :JMS 服务器端口
  • long keepalive :保持活动的间隔

下面的代码显示如何创建 SwiftMQ QueueConnectionFactory 对象:

 
QueueConnectionFactory qcf = (QueueConnectionFactory) new 
com.swiftmq.jms.ConnectionFactoryImpl
  ("com.swiftmq.net.PlainSocketFactory", "myhost",4001,60000);

Progress SonicMQ 3.5 QueueConnection 构造器参数

  • java.lang.String brokerURL :URL(格式为 [protocol://]hostname[:port])
  • java.lang.String connectID :标识连接的标识字符串
  • java.lang.String username :缺省用户名
  • java.lang.String password :缺省密码

下面是创建 Progress SonicMQ QueueConnectionFactory 对象的样本代码:


progress.message.jclient.QueueConnection queueConnection = new
progress.message.jclient.QueueConnection("tcp://myhost:2506", 
    "ServiceRequest", "username", "password");

MQSeries (MA88)

我们要查看的最后一个示例是 IBM MQSeries 实现。MQSeries 不使用连接构造器。取而代之的是,要动态创建连接,必须构造一个连接工厂,然后该工厂再提供产生连接的方法。创建无参数构造器的代码如下:


MQQueueConnectionFactory = new
MQQueueConnectionFactory();

连接工厂的构造器是无参数的,因此工厂有变异方法可以被调用,以控制工厂将提供的连接的特性。

  • setTransportType(int x) :将传送类型设置为下列选项之一:
    • JMSC.MQJMS_TP_BINDINGS_MQ :当 MQSeries 服务器和客户机在同一主机上时使用
    • JMSC.MQJMS_TP_CLIENT_MQ_TCPIP :当 MQSeries 服务器和客户机不在同一主机上时使用
  • setQueueManager(String x) :设置队列管理器名称
  • setHostName(String hostname) :仅用于客户机,设置主机名称
  • setPort(int port) :设置客户机连接端口
  • setChannel(String x) :仅用于客户机,设置要使用的通道

下面是用于创建 MQseries QueueConnectionFactory 和获取特定队列管理器连接的样本代码:


com.ibm.mq.jms.MQQueueConnectionFactory factory = new 
com.ibm.mq.jms.MQQueueConnectionFactory();
factory.setQueueManager("QMGR");
com.ibm.mq.jms.MQQueueConnection connection = 
  factory.createQueueConnection();





回页首


正如我们的简短回顾所示,每个供应商都使用自己独特的连接参数集。那么,如何在代码中透明地支持所有这些参数集呢?标准的解决方案是使用命名服务以持久存储预先配置的 ConnectionFactory 。在运行时,代码可以检索 ConnectionFactory ,而从中返回的连接将能够透明地连接到 JMS 服务器。您无需维护和重建代码,只需简单地在命名服务中维护正确配置的连接工厂即可。

Java 命名和目录接口(JNDI)是与命名服务进行相互操作的最常见方式。JNDI 与 JMS 的相似之处在于它只是定义了一组有待实现的接口。可以用一个标准的 API 访问实现 JNDI 的所有命名服务。

JNDI 是编写与供应商无关的代码的关键,因为它提供了访问命名服务的独立于供应商的方式。这样,我们就只需关注编写从命名服务检索正确对象的代码,无需担心前面一节中所概括的任何专有实现。





回页首


通过创建连接工厂,预先配置它然后将它绑定到命名服务,您可以在消息传递服务中隐藏特定于供应商的连接参数。就代码而言,您正在使用通用的 javax.jms.Connection 对象。供应商实现隐藏在该接口背后。

JMS 规范将那些由管理员创建并且包含由 JMS 客户机使用的配置信息的对象称为 JMS 受管的对象。受管的对象并不依赖于 JNDI,但暗示它们可以绑定到 JNDI 名称空间并可以在其中查询它们。

清单 1 和 2 显示连接到 JMS 服务器的两种不同方法(本例中为 SwiftMQ):一个使用依赖于供应商的代码而另一个使用独立于供应商的代码。




1.QueueConnectionFactory queueConnectionFactory = 
(QueueConnectionFactory) new 
com.swiftmq.jms.ConnectionFactoryImpl
  ("com.swiftmq.net.PlainSocketFactory", "localhost",4001,60000);
2.QueueConnection queueConnection = 
queueConnectionFactory.createQueueConnection();




1.Properties p = new Properties();
2.p.put(Context.INITIAL_CONTEXT_FACTORY,
    "com.swiftmq.jndi.InitialContextFactoryImpl");
3.p.put(Context.PROVIDER_URL,"smqp://localhost:4001");
4.ctx = new InitialContext(p);
5.qcf = (QueueConnectionFactory)ctx.lookup("MyQCF");
6.oQueueConnection queueConnection = 
queueConnectionFactory.createQueueConnection();

首先您会注意到独立于供应商的代码稍稍多几行。这是因为我们必须连接到命名服务。然而请记住,在整个程序中您可能只要连接到命名服务一次即可,因此额外多几行是值得的。(只要确保在每次需要建立连接时,重用命名服务而不是实例化远程上下文。)

与命名服务的交互和对命名服务的准备是编写独立于供应商的消息传递代码的关键。在依赖于供应商的代码示例中,我们使用了 SwiftMQ 实现的 QueueConnectionFactory 构造器,来创建将为我们提供连接的工厂。对于这一实现,我们不仅必须包含供应商专有类,还必须向 QueueConnectionFactory 构造器传递特定于供应商的参数,如清单 1 第一行所示。

在独立于供应商的示例中没有特定于供应商的代码,但我们必须知道初始的上下文工厂和命名服务的供应商 URL,以及 QueueConnectionFactory 的绑定名称。对于绑定名称,适当的命名服务维护将允许您将对象从任一供应商绑定到您的 JNDI 树,因而尽管供应商可能改变,但绑定名称却不必改变。至于 JNDI 上下文,通常的做法是将参数字符串(清单 2 中第二和第三行)存储在特性文件中然后在需要时读取。用这种方法,改变 JMS 供应商只需改变特性文件即可。

还有一件有趣的事情要注意:这种技术给予您关于命名服务的灵活性和可移植性。许多 JMS 供应商(如 Fiorano 和 SwiftMQ)提供他们自己的 JNDI 服务,但是您可能想将命名服务与 JMS 服务分开。(例如,您可能想把连接工厂存储在集中式 LDAP 服务器中。)

以下是可以产生不同 JNDI 连接的特性文件项示例。

SwiftMQ JNDI 服务

  • java.naming.provider.url=smqp://myhost:4001
  • java.naming.factory.initial=com.swiftmq.jndi.InitialContextFactoryImpl

IBM WebSphere JNDI 服务

  • java.naming.provider.url=iiop://myhost:9001
  • java.naming.factory.initial= com.ibm.websphere.naming.WsnInitialContextFactory

iPlanet 目录服务器(LDAP)

  • java.naming.provider.url=ldap://myhost:389
  • java.naming.factory.initial=com.sun.jndi.ldap.LdapCtxFactory

BEA WebLogic JNDI 服务

  • java.naming.provider.url=t3://myhost:7001
  • java.naming.factory.initial=weblogic.jndi.WLInitialContextFactory

文件系统 JNDI 服务

  • java.naming.provider.url=file:/tmp/stuff
  • java.naming.factory.initial=com.sun.jndi.fscontext.RefFSContextFactory

请注意,尽管您的代码可能没有直接引用供应商类,但供应商类也按名称被动态装入 JVM,因此在运行时它们一定要在您程序的类路径(classpath)中。这对 JNDI 和 JMS 类都适用。





回页首


那么,至此我们已经知道如何连接到 JNDI 服务以及如何从不同的 JNDI 和 JMS 实现获取连接而不必重编译我们的代码。让我们把到目前为止所知道的东西集中在一起,来看看如何设置特性文件以及它在启用 JNDI 连接中所起的作用。

进行 JNDI 连接的基类是 javax.naming.InitialContext 。尽管有一些特定于目录操作的 InitialContext 子类(如 InitialDirContext ),但通用类将完成此任务。构造 InitialContext 后,它可以从环境(系统特性或 applet 参数)派生 JNDI 参数值或查找特定 jndi.properties 文件。

下面是 J2SE 1.3.1 javadoc 中对该操作的解释:

JNDI 通过合并来自下列两个源的值来确定每个特性的值,其先后顺序如下:

  1. 构造器环境参数、(对于适当的特性)applet 参数和系统特性中首次出现的特性
  2. 应用程序资源文件(jndi.properties)

迄今为止我们只考虑了两个参数:供应商 URL 和 InitialContext 工厂名。实际上可能要提供更多的属性。除了我们已经考虑的两个之外,最常用的是用户名和密码,它们验证您对可能受保护的 JNDI 存储的访问权限。这些参数是:

  • java.naming.security.principal (用户名)
  • java.naming.security.credentials (密码)

建议您将应用程序所有的运行时配置参数都放在一个应用程序特性文件中并将 JNDI 参数包含在该文件中。将所有参数放在一个地方可以消除不确定性。然后您有几个选项来装入应用程序特性文件。我列出了两个作为示例。可以将该文件作为资源束装入,或者您可以将属性文件的名称和位置作为命令行参数传入。这两种方法有不同的好处。

将文件位置作为命令行参数传入是配置代码最简单的方法。简单的修改一下应用程序的启动就可以改变参数。

将文件作为资源束装入有两个好处:

  • 可以根据 JVM 的语言环境装入不同的资源束。例如,application_en_US.properties 文件可能指向位于纽约的 JNDI 服务,而 application_fr.properties 文件指向位于巴黎的 JNDI 服务。
  • 从资源束装入特性是一种与体系结构和平台无关的装入特性文件的方式。因为资源束从类路径装入,所以代码并不依赖于能够读取 JVM 命令行参数的能力。此外,有些组件(如 EJB 组件)不能直接使用文件 I/O,因此资源束或许提供了一种更方便的装入特性文件内容的方法。

为避免与环境设置的混淆,我始终用从特性文件读出的 JNDI 值来设置特性实例。





回页首


这一节的代码清单演示了特性初始化的两种类型(命令行参数和资源束)以及通用 JNDI 查询。首先,我们看看名为 PropertiesManagement.properties 的样本配置文件,如清单 3 所示:




java.naming.provider.url=smqp://localhost:4001
java.naming.factory.initial=com.swiftmq.jndi.InitialContextFactoryImpl
java.naming.security.principal=admin
java.naming.security.credentials=secret
com.nickman.neutraljms.QueueConnectionFactory=myQueueConnectionFactory
com.nickman.neutraljms.TopicConnectionFactory=myQueueConnectionFactory
com.nickman.neutraljms.Queue=testqueue@router1
com.nickman.neutraljms.Topic=testtopic

文件前四项是 JNDI 环境特性。为清楚起见,我增加了认证特性。后四项是命名服务名称,即 JMS 对象被绑定的地方。我们将使用这些名称来检索连接工厂、队列和主题。如果您正在使用 LDAP,那么名称可能不会这么简单。您可能会看到这样的信息:

  • java.naming.provider.url = ldap://myhost:389/o=nickman.com
  • com.nickman.neutraljms.QueueConnectionFactory = cn=myQueueConnectionFactory,ou=jmsTree

现在让我们看一下读取特性文件的代码。正如前面讨论的,对于如何确定 JNDI 连接参数,您有两种选择。清单 4 是检索 JNDI 特性的样本代码:




package com.nickman.jndi;
import javax.naming.*;   // For JNDI Interfaces
import java.util.*;
import java.io.*;
import javax.jms.*;
public class PropertiesManagement {
   Properties jndiProperties = null;
   Context ctx = null;
   public static void main(String[] args) {
     PropertiesManagement pm = new PropertiesManagement(args);
.
.
   public PropertiesManagement(String[] args) {
     jndiProperties = new Properties();
     if(args.length>0) {
       try {
         loadFromFile(args[0]);
.
.
     } else {
       try {
         loadFromResourceBundle();
.
.
   private void loadFromFile(String fileName) throws Exception {
     FileInputStream fis = null;
     try { 
       fis = new FileInputStream(fileName);
       jndiProperties.load(fis);
     } finally {
       try { fis.close(); } catch (Exception erx){}
     }
   }
   private void loadFromResourceBundle() throws Exception {
     String key = null;
     String value = null;
     ResourceBundle rb = 
       ResourceBundle.getBundle("PropertiesManagement");
     Enumeration enum = rb.getKeys();
     while(enum.hasMoreElements()) {
       key = enum.nextElement().toString();
       value = rb.getString(key);
       jndiProperties.put(key, value);
     }
   }

您可以下载我们在这里介绍的代码的整个 源代码文件作为参考。

清单 4 显示了用两种不同方法装入特性文件的代码。如果传入命令行参数,则该代码假设它是全限定特性文件名,并且用 loadFromFile(String fileName) 方法将特性装入。

可能按以下方式调用类:


java com.nickman.jndi.PropertiesManagement 
c:\config\PropertiesManagement.properties

如果没有传入命令行参数,则代码将调用 loadFromResourceBundle() 方法。这个方法将在 CLASSPATH 上查找特性文件,因此有必要将包含该文件的目录放到类路径中。不管用哪种方法,特性都被装入到特性变量 jndiProperties 中。

清单 5 演示到 JNDI 服务的连接:




   public void connectToJNDI() throws javax.naming.NamingException {
     
     // jndiProperties was loaded from PropertiesManagement.properties
     ctx = new InitialContext(jndiProperties); 

     System.out.println("Connected to " + 
       ctx.getEnvironment().get(Context.PROVIDER_URL));
   }

以上连接代码相当简单。 jndiProperties 变量被传递到 InitialContext 构造器中,而产生的 Context 是 JNDI 服务的“句柄”。要注意的是接口 javax.naming.Context 包含一组常量以表示所有可用的环境特性。

建立了 Context 后,我们可以继续查询 JMS 对象,如清单 6 所示:




   public QueueConnectionFactory lookupQueueConnectionFactory() 
        throws javax.naming.NamingException {
     return 
     (QueueConnectionFactory)ctx.lookup(jndiProperties.get
         ("com.nickman.neutraljms.QueueConnectionFactory").toString());
   }
   public Queue lookupQueue() throws javax.naming.NamingException {
     return 
     (Queue)ctx.lookup(jndiProperties.get
         ("com.nickman.neutraljms.Queue").toString());
   }

查询只是调用 Contextlookup(String name) 方法,然后传入我们希望绑定对象的位置的名称。返回对象必须被强制转换为正确的类,在该示例中它将是标准 javax.jms 接口之一。





回页首


javax.jms.Destination 是一个接口,它封装消息将被发送到的特定目标。 QueueTopic 接口都继承 Destination 接口。因为 Destination 是 JMS 受管的对象,所以 QueueTopic 也是。

您将注意到 JMS API 在 TopicQueue 会话类中包含两个方法:

  • Topic TopicSession.createTopic(java.lang.String topicName)
  • Queue QueueSession.createQueue(java.lang.String topicName)

那么,问题出现了:既然可以简单地通过单个字符串引用队列或主题,为什么还要大动干戈地在 JNDI 保存它们?原因是微妙的;要了解原因,我直接从 javadoc 中引述:

Destination 对象封装特定于供应商的地址。JMS API 没有定义标准的地址语法。尽管在考虑标准的地址语法,但现有的面向消息中间件(MOM)产品之间的地址语义差别太大,很难用单一语法进行统一。

因为 Destination 是一个受管的对象,因此除了它的地址以外,它还可以包含特定于供应商的配置信息。

简而言之,这意味着我们可以把特定的 JMS 供应商的详细信息隐藏于我们用来在 JNDI 中查询名称空间的简单名称之后。我还发现在 JMS 客户机与实际的 JMS 目的地之间设置一个间接层可以增加体系结构的灵活性。客户机代码可以引用 JNDI 中名为 myQueue的名称空间,而管理员可以把该名称空间的对象设置为来自任一供应商的任一队列目的地。图 2 阐明了这种思想:



目的地图




回页首


JMS 中的发布-然后-订阅框架定义的一些功能性可能会妨碍我们为保持供应商独立而做的努力。许多 JMS 服务器支持分层名称空间的思想。这允许 P/S 消息按层次分类。当订阅某个主题的客户机连接到 JMS 服务器时,它可以请求适合层次结构特定部分的消息。可以将图 3 中列出的层次结构作为示例研究:



样本层次结构

为了更好地理解,我们将使用一个样本方案。假设某个订户客户机想在某次服务中订阅所有的“美国证券价格”。如果是静态订阅,客户机只需检索 JMS 管理的对象,该对象表示预先配置用以订阅美国证券的主题。这是理想的,因为不同 JMS 供应商对于描述分层订阅有不同的语法,通过将这种不同隐藏在 JNDI 检索的对象之后,客户机可以不考虑底层的 JMS 实现。

但是请考虑一个包含 50 个不同层次且提供数百个可以订阅的不同“单元”的层次结构。另外,还要考虑这个层次结构或许是动态的,管理员可以不断地向它添加单元。在这种情况下,要 JMS 管理员创建所有必需的 JMS 管理的对象来表示所有可能的订阅是不切实际的。而且,层次结构选项可能需要是非常灵活的和动态的以支持客户机应用程序。

在此情形下,在运行时定义主题名称会更有意义。麻烦在于:用于表示层次结构的语法可能因供应商不同而不同。以下代码片段举例说明了三个不同供应商所使用的订阅语法。

MQSeries JMS


Topic topicEqUs = topicSession.createTopic("topic://Prices/Equity/US");
Topic topicEqAll = topicSession.createTopic("topic://Prices/Equity/*");

SonicMQ 3.5


Topic topicEqUs = topicSession.createTopic("Prices.Equity.US");
Topic topicEqAll = topicSession.createTopic("Prices.Equity.*");

SwiftMQ 2.1.3


Topic topicEqUs = topicSession.createTopic("Prices.Equity.US");
Topic topicEqAll = topicSession.createTopic("Prices.Equity.%");

请注意 MQSeries JMS 实现使用了与另外两个实现不同的分隔符(它使用正斜杠而几乎所有其它供应商都使用点)。

除了以上列出的字符以外,在主题名称中还可能出现其它特定于供应商的字符串。例如,SonicMQ 使用英镑标记来分隔较高的层次结构,而 MQSeries 使用主题名称后缀表示通常为 API 保留的选项。几乎所有的 JMS 供应商都使用不同的通配符,这使得在运行时统一定义主题名称变得很困难。幸运的是,针对此问题有一个变通方法:我们将所有的特殊字符存储在引用源中,然后从那个源中装入,并在运行时使用它们。

这个引用源可以是应用程序特性文件或 JNDI 服务。为了举例说明这个变通方法,我们将把主题字符添加到 PropertiesManagement.properties 文件中,如清单 7 所示:




#Topic Delimiter For  Sonic and Swift
com.nickman.neutraljms.TopicDelemiter=.
#Topic Delimiter for MQSeries
#com.nickman.neutraljms.TopicDelemiter=/
#Topic Wild Card For Sonic and MQSeries
com.nickman.neutraljms.TopicWildCard=*
#Topic Wild Card For Swift
#com.nickman.neutraljms.TopicWildCard=%
#Topic Prefix For MQSeries
#com.nickman.neutraljms.TopicPrefix=topic://
#Topic Prefix For All Others
com.nickman.neutraljms.TopicPrefix=

添加这些项之后,如果如下所示,那么主题订阅代码就是与供应商无关的:




String delim = 
jndiProperties.get("com.nickman.neutraljms.TopicDelemiter").toString();
String wildcard = 
jndiProperties.get("com.nickman.neutraljms.TopicWildCard").toString();
Strung prefix = 
jndiProperties.get("com.nickman.neutraljms.TopicPrefix").toString();
Topic topicEqUs = topicSession.createTopic("Prices" + delim + 
"Equity" + delim + "US");
Topic topicEqAll = topicSession.createTopic("Prices" + delim + 
"Equity" + delim + wildcard);

一旦掌握了清单 7 所示的外部配置,就可以举一反三地用它覆盖其它供应商特定的选项。例如,JMS 规范定义了会话可以实现的两种传递方式,但 Sonic MQ 支持其它三种传递方式。通过在外部定义传递方式,您可以在保持代码供应商独立性的同时实现 Sonic MQ 专有扩展。

阅读(648) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~