利用 JMS 实现异步 Web 服务操作
James M. Snell
软件工程师, IBM
2004 年 11 月
学习如何在 Web 服务中应用设计良好、经过检验的 Web 应用程序设计方法。本系列中的第一个技巧将教您怎样通过 Java 消息服务(JMS)来实现异步请求操作。
当您希望通过 SOAP 实现 Web 服务时,您脑子里想到的很可能就是请求响应模式的同步操作。然而,一个真正的面向服务体系结构(SOA)应当能够包容更多形式的消息模式和设计模式。在本文中,我将通过一个简短的系列来集中说明如何将 Web 应用基本设计模式移植到 Web 服务应用上。设计模式并不是一个新事物,事实上,它已经应用到一些传统的 Web 应用上好多年。但是,许多开发者可能还不知道如何在 Web 服务领域中应用这些设计模式,或者尚未全面了解如何应用这些设计模式。我的目标就是提供一个简单、直接的设计模式的集合,作为请求-响应模型替代者。大体了解在 J2EE 环境中如何实现 Web 服务环境对于理解本文中的示例将很有帮助。
异步请求模式
为中断长时间运行的操作,我研究了异步请求响应操作的实现方法,避免了超时和代码执行的长时间挂起。如果您实现过传统的 HTTP 和 HTML Web 应用程序中的异步请求,您将发现我所实现的模式与它非常相似。
图 1. 异步请求模式
模式的流程非常简单:
- 请求者向服务提供者发出一个请求,服务提供者将请求消息放入队列并返回一个相关的 ID 。请求提供者可以根据此 ID 在以后的时间里检查消息的状态。
- 请求处理者从队列中取出消息并进行处理。一般来说,请求的处理是一个较长时间的过程。一旦请求处理完毕,处理者将向队列中发送一条响应消息。
- 在未来某一时刻,请求者询问服务提供者是否有此请求的响应消息到达。如果队列中有响应消息,则服务提供者将此消息返回给请求者。如果请求消息尚未到达,服务提供者将此消息报告给请求者,请求者据此可以选择或者取消此次请求或者继续等待,并以设定的时间间隔轮询服务提供者,直到响应到达。
Kyle Brown 曾经写过一篇很不错的文章(请参阅 参考资料),就是关于将此模式应用在 J2EE Servlet 应用程序设计上,那个应用程序是为一个名为 Java Ranc 的 Java 资源开发 Web 站点设计的。 在他的文章中,Kyle 深入讨论了与此模式实现相关的设计出发点和设计思想。自然,基于 Web 服务实现此模式相对于原来的实现不需要作太大的改动。
设计服务接口
为了说明异步请求模式,我实现了一个简单的示例 Web 服务,它除了说明这一模式的概念外无任何实际意义。这一服务的唯一功能只是将三个小写的 字符串
输入值在强制 10 秒的间隔后转化为大写的字符串,这一强制时间间隔用来模拟一较长时间处理过程。
为实现此服务,需要提供以下两个 Web 服务操作: submitRequest
和 checkResponse
。 每个操作所实现的功能都是自我描述的。 清单 1 是服务接口的 WSDL 描述。
清单 1. AsyncService.wsdl
关于这一接口有两点需要说明:
- 服务使用 RFC/文本编码方式。
submitRequest
和 checkResponse
操作都返回一个称作 响应
的对象。根据 类型属性区分,有两种类型的 响应
对象。 类型属性值为 0 的称为 刷新响应,类型属性为 1 的称为 请求响应。 刷新响应指的是响应尚未到达,请求者应该在不早于刷新属性(相当于上面引用的 Kyle Brown文章中提到的 HTTP META Refresh
机制)指定的时间间隔下提交一个新的 checkResponse
操作。请求响应包含三个大写输入字符串,并标志着请求处理过程的结束。
- 刷新响应包含一个
相关 ID
,其属性被用来作为 checkResponse
操作的输入值。这一标志只是客户端用来将最初自己的请求与响应进行关联。当然还有其它方式实现这一功能,稍后我将对此进行讨论。
清单 2 显示了服务的一个典型的消息交换。
清单 2. AsyncService 消息交换
原始请求
String
String
String
提交请求的响应
0
1097517621904
10000
初次 checkResponse 尝试,没有响应到达,在 10000 毫秒后提交。
0
1097517621904
10000
第 2 次 checkResponse 尝试,在 10000 毫秒后提交。
1
0
STRING
STRING
STRING
|
实现服务
实现此异步请求模式服务是 Java 消息服务的直接应用。为实现此例程,我使用开源的 JMS 实现 OpenJMS (请参阅 参考资料) 和 IBM® WebSphere ® Application Server V5(应用服务器)。 我使用 OpenJMS 缺省配置,并实现了一个 JSR-109 兼容的J2EE Web 服务。我利用 WebSphere Studio Application Developer (应用开发工具) V5.1来编写示例中的代码,您可以从 developerWorks(请参阅 参考资料)下载它。 如果您没有 Application Developer,我同时还提供了一个 EAR 文件 (请参阅 参考资料) 。
有 2 个服务器端的组件需要实现:请求处理器和 Web 服务实现。请求处理器负责将请求消息放入队列并仿真执行一个 10 秒的“长时间”运转过程。 Web 服务实现负责接收 Web 客户端的请求,把它们排队进行处理,在 checkResponse 操作后将响应传递到客户端。
在一个典型的 J2EE 应用程序中,按照 JMS 定义,请求处理器应该通过一个消息驱动 Bean 来实现。在本例中,我通过一个简单的 HTTP Servlet 来实现它,这个 Servlet 实现了 JMS MessageListener
接口。 Servlet 被配置为在服务器启动时初始化,这使得 Servlet 可以监听到所有发送来的请求。一旦队列中包含请求,它就被发送到监听 Servlet 。
清单 3. JNDIListenerServlet.java
package com.ibm.developerworks.wspattern.one.helper;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.naming.Context;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
public class JNDIListenerServlet
extends HttpServlet
implements Servlet, MessageListener {
private Context context;
private QueueConnection connection;
private QueueSession session;
private Queue queue;
private QueueReceiver receiver;
public void init()
throws ServletException {
super.init();
try {
context = JNDIHelper.getInitialContext();
connection = JNDIHelper.getConnection(context);
session = JNDIHelper.getSession(connection);
queue = JNDIHelper.getQueue(context);
receiver = JNDIHelper.getQueueReceiver(session, queue);
receiver.setMessageListener(this);
System.out.println("Listener servlet is Listening");
} catch (Exception e) {}
}
public void destroy() {
try {
connection.close();
} catch (Exception e) {}
}
public void onMessage(Message message) {
try {
System.out.println("Processing message " + message.getJMSCorrelationID());
Thread.sleep(10 * 1000); // sleep for ten seconds
Queue responseQueue = JNDIHelper.getResponseQueue(context);
QueueSender sender = JNDIHelper.getQueueSender(session,responseQueue);
MapMessage request = (MapMessage)message;
MapMessage response = session.createMapMessage();
response.setJMSCorrelationID(request.getJMSCorrelationID());
for (Enumeration e = request.getMapNames(); e.hasMoreElements();) {
String name = (String) e.nextElement();
try {
response.setString(
name,
request.getString(name).toUpperCase());
} catch (Exception ex) {}
}
sender.send(response);
} catch (Exception e) {
System.out.println("==================");
try {
System.out.println(
"THERE WAS AN ERROR PROCESSING THE MESSAGE! " +
message.getJMSCorrelationID());
} catch (Exception ex) {}
e.printStackTrace(System.out);
System.out.println("==================");
}
}
}
|
JNDIListenerServlet
和服务实现都使用了一个为本应用程序而创建的简单的助手类,助手类隐藏了初始化 JMS 连接和会话的过程。
清单 4. JNDIHelper.java
package com.ibm.developerworks.wspattern.one.helper;
import java.util.Hashtable;
import javax.jms.JMSException;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIHelper {
private static Context context;
public static Context getInitialContext()
throws NamingException {
if (context == null) {
Hashtable properties = new Hashtable();
properties.put(
Context.INITIAL_CONTEXT_FACTORY,
"org.exolab.jms.jndi.InitialContextFactory");
properties.put(
Context.PROVIDER_URL,
"rmi://localhost:1099");
context = new InitialContext(properties);
}
return context;
}
public static QueueConnection getConnection(
Context context)
throws NamingException,
JMSException {
QueueConnectionFactory factory =
(QueueConnectionFactory) context.lookup(
"JmsQueueConnectionFactory");
QueueConnection connection = factory.createQueueConnection();
connection.start();
return connection;
}
public static QueueSession getSession(QueueConnection connection)
throws JMSException {
QueueSession session =
connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
return session;
}
public static Queue getQueue(Context context)
throws NamingException {
Queue queue = (Queue) context.lookup("queue1");
return queue;
}
public static Queue getResponseQueue(Context context)
throws NamingException {
Queue queue = (Queue) context.lookup("queue2");
return queue;
}
public static QueueSender getQueueSender(
QueueSession session,
Queue queue)
throws JMSException {
QueueSender sender = session.createSender(queue);
return sender;
}
public static QueueReceiver getQueueReceiver(
QueueSession session,
Queue queue)
throws JMSException {
QueueReceiver receiver = session.createReceiver(queue);
return receiver;
}
public static QueueReceiver getQueueReceiver(
QueueSession session,
Queue queue,
String selector)
throws JMSException {
QueueReceiver receiver = session.createReceiver(queue, selector);
return receiver;
}
}
|
Servlet 创建完成后,编辑 Web 应用程序的配置文件 web.xml 使得 Servlet 被配置为在服务器启动时初始化。Servlet 初始化后,它打开 JMS 连接,并将自己注册为 OpenJMS 缺省消息队列的监听者。
第二步是创建服务实现。这里利用 Application Developer 来开发将变得轻而易举,因为实现一个 JSR-109 兼容的 Web 服务所需要的各种文件都可以生成。 这里我主要讲述服务实现类。您可以通过点击本文顶部或底部的 Code 图标来下载代码,并进一步查看应用服务器所需的各种 Java 文件和 XML 配置文件。
清单 5. AsyncService.java
package com.ibm.developerworks.wspattern.one;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.naming.Context;
import com.ibm.developerworks.wspattern.one.helper.JNDIHelper;
public class AsyncService {
public Response submitRequest(Request request) {
Response response = null;
try {
Context context = JNDIHelper.getInitialContext();
QueueConnection connection = JNDIHelper.getConnection(context);
QueueSession session = JNDIHelper.getSession(connection);
Queue queue = JNDIHelper.getQueue(context);
QueueSender sender = JNDIHelper.getQueueSender(session,queue);
MapMessage message = session.createMapMessage();
String corrID = Long.toString(System.currentTimeMillis());
message.setJMSCorrelationID(corrID);
message.setString("one", request.getA());
message.setString("two", request.getB());
message.setString("three", request.getC());
sender.send(message);
response = new Response();
response.setType(Response.TYPE_REFRESH);
response.setCorrelationID(corrID);
response.setRefresh(10 * 1000);
return response;
} catch (Exception e) {
response = new Response();
response.setType(Response.TYPE_RESPONSE);
response.setA(e.getMessage());
}
return response;
}
public Response checkResponse(ResponseCheck check) {
String corrID = check.getCorrelationID();
Response response = null;
try {
Context context = JNDIHelper.getInitialContext();
QueueConnection connection = JNDIHelper.getConnection(context);
QueueSession session = JNDIHelper.getSession(connection);
Queue queue = JNDIHelper.getResponseQueue(context);
String selector = "JMSCorrelationID = '" + corrID + "'";
QueueReceiver receiver = JNDIHelper.getQueueReceiver(session, queue, selector);
Message message = receiver.receiveNoWait();
if (message == null) {
response = new Response();
response.setType(Response.TYPE_REFRESH);
response.setRefresh(10 * 1000);
response.setCorrelationID(corrID);
} else {
MapMessage resp = (MapMessage)message;
response = new Response();
response.setType(Response.TYPE_RESPONSE);
response.setA(resp.getString("one"));
response.setB(resp.getString("two"));
response.setC(resp.getString("three"));
}
} catch (Exception e) {}
return response;
}
}
|
这里没什么特别之处。 submitRequest
方法基于输入参数实现了一个 JMS 的 MapMessage
消息。这一 map message 包含 3 个字符串的值。此消息随后被发送到队列,一个包含相关 ID 的刷新响应已经组装完毕并被发送到客户端。
checkResponse
操作从输入参数中提取相关 ID ,打开到响应消息队列的连接,要求队列发送与此相关 ID 匹配的消息。 如果符合条件的消息不存在,操作并不会等待消息的出现。它只是简单地用一个新的刷新间隔周期来组装一个刷新响应并将它返回给调用者。如果存在发送来的符合条件的消息,操作将组装并返回一个适当的请求响应。
发布此 Web 服务,启动您的 OpenJMS 和 WebSphere 服务器,您的异步请求模式 Web 服务将启动并开始运行。
结束语
异步请求模式成功的关键在于 Web 服务客户与服务提供者之间协调请求和响应的能力。 在本文的示例中,您可以创造一种简单的、但特定与这一示例应用程序的关联 ID 和刷新时间机制。 然而,完全有可能通过组合一些 Web 服务相关的规范来实现同样的效果。 WS-Addressing 终点参考和 WS-Transaction 协调上下文可以很容易地集成到相关 ID 和刷新时间值的实现中。无论怎样,在应用此模式时,不同的应用程序都有其特殊的地方,但无论如何,不管您使用标准的 SOAP 头元素还是各种各种 Web 服务定义,您的每个操作实现的行为必须是良好定义的且被规范地文档化。
进一步深入分析,示例是通过传统的 SOAP 请求-响应消息模式来发送请求和接收响应的。同时还有另外一种实现方式,即可以通过 Rest-style 模式来实现,通过 HTTP POST 方法发送请求到 Servlet ,通过 HTTP GET 方法获取响应。 任一实现方法都同样有效而且相互之间都有其相对长处和缺点,主要根据您的应用程序的特定的需求来选择。 例如是否您的 checkResponse
操作需要基于 WS-Security 的认证,而基于 REST-style 的交互模式将根本无需考虑。
最后,您很容易想到扩展此示例的范围,允许请求者发送详细状态请求到长时间操作甚至允许取消进行中的操作。详细的研究这些可能性超出了本文的范围,因此,我把它作为一个练习留给您自己去探讨。
参考资料
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
- Kyle Brown 发表在 Java Ranch 的文章,“ J2EE 中的异步请求”提供了本文中所讨论的异步请求模式的设计基础
- 下载 WebSphere Studio Application Developer,我利用它来编写示例程序。
- 您可以下载 OpenJMS provider 并获取这一开源程序的更多信息。
- 参考我的 developerWorks weblog 中关于(但并不限于)新技术话题的更多讨论。
- 使用 快速启动 Web 服务访问 Web 服务知识、工具和技能,它提供了最新的基于 Java 的软件开发工具和来自 IBM 的中间件(试用版),以及在线教程和文章和在线技术论坛。
- 请访问 Developer Bookstore 以获得技术书籍的完整列表,其中包括数百篇 Web 服务相关主题 的图书。
- 想了解更多信息?developerWorks 的 SOA 与 Web 服务专区包含了数以百计的技术文章以及关于如何开发 Web 服务应用程序的入门级、中级和高级教程。
下载
|
描述 |
|
|
|
Name |
|
|
|
Size |
|
|
|
Download method |
|
|
|
WebSphere deployable EAR file |
|
|
|
ws-tip-altdesign1ear.ear |
|
|
|
701 KB |
|
|
|
FTP |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Example program source files |
|
|
|
ws-tip-altdesign1code.zip |
|
|
|
719 KB |
|
|
|
FTP |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
关于下载方法的信息
|
关于作者 James Snell 是 IBM 新技术工具小组成员之一,在过去的几年里主要研究新兴的 Web 服务技术和标准。他维护着一个 developerWorks 中关注新兴技术的 weblog 栏目。 |