JMS基础知识(转)
http://www.ibm.com/developerworks/cn/java/j-jmsvendor/#resources
而 JMS 就不是这样。只要最少的工作并遵循我在本文中所推荐的过程,您就可以使您的 JMS 客户机代码顺畅地运行,而丝毫觉察不到所使用的供应商实现的差异。虽然我假设您对 JMS 消息处理有基本的了解,但我们还是将对基本概念和术语进行简短回顾,来开始这次讨论。
发送和接收消息的基础是 连接,它负责分配 JVM 之外的资源。JMS 供应商通常至少为 P2P 事务实现一个 QueueConnection
,至少为 P/S 事务实现一个 TopicConnection
。这些连接提供一个 Session
,它是管理消息发送和接收的结构。
P2P 事务管理的基本结构是 QueueSender
和 QueueReceiver
。用于 P/S 事务管理的基本结构是 TopicSubscriber
和 TopicPublisher
。Topic 和 Queue 对象封装了导向每条消息的目标和源的特定信息。这一层次结构如图 1 所示:
其它特定于应用程序服务器的结构(如请求/响应支持类)和特性可以在 JMS 标准中找到(请参阅 参考资料)。
因为连接是和 JMS 服务器交互的入口点,因此连接接口的每个实现必须知道如何与它自己的 JMS 服务器的一个实例连接。因为底层连接协议的详细信息往往因供应商不同而有所不同,所以设置活动连接所需的信息也会因供应商而异。
大多数供应商允许动态地设置连接。也就是说,他们将连接类的构造器定义成公共的(public),允许程序员定义所需连接信息。大多数供应商提供在调用后可以返回一个连接的工厂类。
就连接工厂而言,工厂类可以返回一个已预先装入专有连接信息的连接。供应商定义的工厂类将提供方法以允许程序员设置连接参数。这些连接参数指示工厂返回的连接的性质。
为了使所有这些更加具体,让我们看看
QueueConnection
和
QueueConnectionFactory
的几个实现的构造器、连接工厂和设置方法。(请注意,有些情况下会有许多重载的构造器;对每种情况我只举例说明一个构造器。)
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 通过合并来自下列两个源的值来确定每个特性的值,其先后顺序如下:
- 构造器环境参数、(对于适当的特性)applet 参数和系统特性中首次出现的特性
- 应用程序资源文件(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 所示:
清单 3. PropertiesManagement.properties
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()); } |
查询只是调用 Context
的 lookup(String name)
方法,然后传入我们希望绑定对象的位置的名称。返回对象必须被强制转换为正确的类,在该示例中它将是标准 javax.jms
接口之一。
javax.jms.Destination
是一个接口,它封装消息将被发送到的特定目标。 Queue
和 Topic
接口都继承 Destination
接口。因为 Destination
是 JMS 受管的对象,所以 Queue
和 Topic
也是。
您将注意到 JMS API 在 Topic
和 Queue
会话类中包含两个方法:
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 所示:
清单 7. 包含主题字符的 PropertiesManagement.properties
#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 专有扩展。
本文概括的方法并不算详尽。我的目的是帮助您开始实现独立于供应商的 JMS 解决方案。而且,在您采用新的 JMS 实现时,这些技术应该能提高您集成更改的能力。
您可能已经注意到了,这里所有的技术在很大程度上都依靠在 JNDI 中存储 JMS 受管的对象这一策略。以另外的方式使 JMS 与 JNDI 一起工作,以及将 J2EE 应用程序服务器和 JNDI 服务集成的进一步研究,将留待今后的文章讨论。
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
- 单击本文顶部或底部的
论坛参与有关本文的
论坛。
- 下载
PropertiesManagement.zip源文件。
- 在“
Should you use JMS in your next enterprise application?”一文(
developerWorks,2002 年 2 月)中,专栏作家 Brian Goetz 概括了在 Java 应用程序中使用消息排队的的几点好处,并研究了最能从 MQ 技术中受益的问题的类型。
- Daniel Drasin 在他的文章“
Get the message?”(
developerWorks,2002 年 2 月)中,讨论了成功地将 IBM MQSeries(现在称作 WebSphere MQ)设置为 JMS 服务器的好处、一些潜在的缺陷和实际操作指南。
- 要了解更多有关 Java 消息服务 API 的知识,请浏览 Sun Microsystems 的
JMS 主页。
- 深入研究
JMS javadoc。
- 同样请参阅
JNDI javadoc。
- 如果您在寻找 JMS 编程技巧,JGuru 的
JMS FAQ提供了许多信息。
- 深入研究为 WebSphere Application Server 4.0
安装和配置 MQSeries。
- 撰写本文时用到了以下 JMS 实现:
- 您将在 IBM developerWorksJava 技术专区找到上百篇有关 Java 编程各个方面的文章。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述