级别: 初级 James M. Snell (jasnell@us.ibm.com), 软件工程师, IBM
2004 年 12 月 01 日 本文继续介绍简短系列中的第 3 个技巧,该系列重在讨论应用问题,即通过尝试使用 Router 模式将已明确定义好并验证过的 Web 应用程序设计策略用于整个 Web 服务。
该系列中的少部分已经介绍了一些通过使用已知的并被验证过的设计模式来创建 Web 服务的简单直接的技术。迄今为止,我已经观察到可以使用消息队列来实现异步的请求或响应,以及使用 Command 模式来实现商业逻辑封装。这次,我们来研究一种作为简单
服务器的服务的建立过程,向依赖于个人输入的参数值的特定商业逻辑组件发送请求。当您使用这个技巧的时候会注意到其代码是相当精简的,这正是要点。其目的不是为了介绍任何革命性的设计理念或者编程技术。而是我想通过此系列试着说明如下观点:您可以通过一些基本的步骤创建能够解决各种问题的非常灵活的服务。
Router 模式
Router 模式是向基于既定标准的具体商业逻辑片断发送请求这个简单概念的核心内容。在程序中,通过构造诸如
switch-case 和
if-then-else 之类的程序块来阐明这种模式是如何实现。要点是我没有谈论任何难于理解的内容。如
图 1 所示。
图 1. Router 模式
从客户端的角度上看路由器本质上是一个透明的组件。它负责接收请求并且计算出它将要被处理的地点。某些既定的标准决定路由器将把信息发送到哪里。这种发送标准可能是静态的或动态的。静态的路由是指不能更改路由规则。无论当前的状态如何,相同的信息每次都被发向同一商业组件。动态的路由是指当前的某些条件影响了它所携带的具体的路由信息。同样的信息如果被发送多次,可能被发到不同的商业组件中进行处理。
需要记住的最重要的一点是客户端应当完全不知道您所使用的路由模式。事实上,在理想情况下,客户端从来不必知道路由过程是否发生——这需要对会话或状态管理进行深入的研究。
如果客户发出一系列消息或调用一组面向 Web 服务的操作,那么您不能确保基于路由的实现方法通过使用单一的商业组件来处理每个消息或请求。此外,您也不应认为路由组件知道客户端没有直接访问它们,或者知道它们被发送到了商业组件收集装置。最简单的方法是不允许 Web 服务维持任何内部的状态,但这种方法并不总是可行的或者令人满意的。更复杂一点的解决方法是设计某种装置使商业组件能够存储外部的会话状态,这通过使用所有商业组件都能访问的某种形式的共享会话服务来实现。对于简单的情况,基于 Web 的应用程序,将所有的商业组件都封装进单一的运行在独立机器上的应用程序,这是容易实现的。但是,这对于遍布于多机中的分布式的商业组件来说可能会出现问题。一些应用程序服务器向您提供了可以利用的分布式的会话服务,但您需要事先规划并设计应用程序使之能够适合于使用这样的服务。
图 2. 在共享会话环境下的 Router 模式
上面的
图 2 说明了在大多数 Web 应用程序服务器中的 HTTP 会话是如何实现的。请求的监听器收到了来自 HTTP 的消息,并将它们转发到了具有某些有标识的商业组件中(Java™ servlet、JSP 页面等等),而提供共享会话环境是用来维持请求发送到的特定组件的独立性。
Router 模式的另一个重要的方面是应注意到请求发向的商业组件的部署是独立于路由器的部署的。换句话说,商业组件是明显分离的应用程序块,它能被很好地部署在:
- 完全不同的服务器上
- 同一服务器上
- 同样的网络环境下
- 穿过某些防火墙的界限
如
图 3 所示。
图 3. Router 模式使用中级队列
在开始阅读代码之前,请注意路由器可能是单向或者双向的。也就是说,路由器负责将请求发送到适当的商业组件中去,也可能负责将那些请求相应的响应发回客户端。例如,假设您使用异步通信模式,那么您可以轻松地使用该技巧系列中第一部分介绍的装置使客户端能够直接地从等待队列中获取响应信息。另一种方法依赖于向特定的组件发送请求来实现,路由器会在响应返回给客户端之前暂停并等待它。这种行为完全依赖于开发人员所试图完成的工作。
提供 Web 服务的路由器的实现过程
为了说明 Router 模式的简单应用,您应当先建立一种需要单一操作支持的 Web 服务,这种操作依赖于输入参数的具体值,它将把已获得的文本字符串转换为大写或小写字母。
您应用程序中的各种组件都可以被部署到单一的 Web 应用程序中,只有利用中级 Java 消息服务(Java Messaging Service,JMS)队列才能实现。会话功能不会被轻易地提供,因为阐明基本模式不是必需的。
为了启动这个实例,您需要 J2EE Web Application Server 以及 JMS 提供者的支持。在编写实例代码的时候,我使用了 OpenJMS 开放资源从 Sourceforge.net(见
参考资料)处获得的 JMS 提供者。
您一旦安装了操作环境,就是时候开始编写代码了。正如预料的那样,我们例子(见
清单 1)中的代码不是非常复杂。以两个商业组件负责处理需求信息这个例子开始。每一个都是作为 HTTP servlet 来实现的,它是在服务器启动时初始化并且实现了标准的 JMS MessageListener 接口。每个 servlet 都监听特定的 JMS 队列以获取需求信息,一旦收到了该信息就执行指定的操作,将回复信息放置在单一的共享输出的响应队列中。例外情况,一个监听器将输入字符串转换成大写字母,而另一个将其转换成小写字母,这对于监听器所编写的代码都是完全相同的。
清单 1. UppercaseListenerServlet.java 和 LowercaseListenerServlet.java
package com.ibm.developerworks.wspattern.three;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
public class UppercaseListenerServlet
extends HttpServlet
implements MessageListener, Servlet {
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.getQueueConnection(context);
session = JNDIHelper.getQueueSession(connection);
queue = JNDIHelper.getQueue(context, "upperqueue");
receiver = JNDIHelper.getQueueReceiver(session, queue);
receiver.setMessageListener(this);
System.out.println("UppercaseListenerServlet is listening");
} catch (Exception e) {
System.out.println("UppercaseListenerServlet init Error");
e.printStackTrace(System.out);
}
}
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
TextMessage txtMessage = (TextMessage) message;
String txt = txtMessage.getText().toUpperCase();
TextMessage response = session.createTextMessage();
response.setJMSCorrelationID(txtMessage.getJMSCorrelationID());
response.setText(txt);
Queue queue = JNDIHelper.getQueue(context, "queue6");
JNDIHelper.getQueueSender(session, queue).send(response);
}
} catch (Exception e) {
System.out.println("UppercaseListenerServlet onMessage Error");
e.printStackTrace(System.out);
}
}
}
package com.ibm.developerworks.wspattern.three;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.Queue;
import javax.jms.QueueConnection;
import javax.jms.QueueReceiver;
import javax.jms.QueueSession;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
public class LowercaseListenerServlet
extends HttpServlet
implements MessageListener, Servlet {
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.getQueueConnection(context);
session = JNDIHelper.getQueueSession(connection);
queue = JNDIHelper.getQueue(context, "lowerqueue");
receiver = JNDIHelper.getQueueReceiver(session, queue);
receiver.setMessageListener(this);
System.out.println("LowercaseListenerServlet is listening");
} catch (Exception e) {
System.out.println("LowercaseListenerServlet init Error");
e.printStackTrace(System.out);
}
}
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
TextMessage txtMessage = (TextMessage) message;
String txt = txtMessage.getText().toLowerCase();
TextMessage response = session.createTextMessage();
response.setJMSCorrelationID(txtMessage.getJMSCorrelationID());
response.setText(txt);
Queue queue = JNDIHelper.getQueue(context, "responsequeue");
JNDIHelper.getQueueSender(session, queue).send(response);
}
} catch (Exception e) {
System.out.println("LowercaseListenerServlet onMessage Error");
e.printStackTrace(System.out);
}
}
}
|
您将看到路由器的简短代码(见
清单 2),介绍了一种可接受两个参数的称为
process 的操作。第一个参数用于确认客户端的请求是否被执行了,可选择
upper 或
lower。第二个参数是将被转化成大写或小写字符的输入字符串。一旦收到 process 请求,路由器就会将输入字符串放入
upperqueue 或
lowerqueue 中,这由选择操作来决定。适当的监听器从中获取信息并处理需求,将转换好的文本放入
responsequeue。路由器从
responsequeue 中取得信息并将转换好的文本返回到客户端,如
图 4 所示。
图 4. RouterService 应用实例
清单 2. RouterService.java
package com.ibm.developerworks.wspattern.three;
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.jms.TextMessage;
import javax.naming.Context;
public class RouterService {
public String process(String action, String target) {
String response = null;
try {
Context context = JNDIHelper.getInitialContext();
QueueConnection connection = JNDIHelper.getQueueConnection(context);
QueueSession session = JNDIHelper.getQueueSession(connection);
String queueName =
("upper".equalsIgnoreCase(action)) ? "upperqueue" : "lowerqueue";
Queue queue = JNDIHelper.getQueue(context, queueName);
QueueSender sender = JNDIHelper.getQueueSender(session,queue);
TextMessage message = session.createTextMessage();
String corrID = Long.toString(System.currentTimeMillis());
message.setJMSCorrelationID(corrID);
message.setText(target);
sender.send(message);
Queue rqueue = JNDIHelper.getQueue(context, "responsequeue");
String selector = "JMSCorrelationID = '" + corrID + "'";
QueueReceiver rec = JNDIHelper.getQueueReceiver(session, rqueue, selector);
TextMessage resp = (TextMessage)rec.receive(10 * 1000);
response = resp.getText();
} catch (Exception e) {
response = e.getMessage();
}
return response;
}
}
|
作为监听器的 servlet 以及
RouterService 类都使用了一个共享的有效类,该类用来封装 JMS API 工作流程中的各种琐碎的细节。对于这个有效类,为方便起见,在
清单 3 中给出。
清单 3. JNDIHelper.java
package com.ibm.developerworks.wspattern.three;
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 getQueueConnection(
Context context)
throws NamingException,
JMSException {
QueueConnectionFactory factory =
(QueueConnectionFactory) context.lookup(
"JmsQueueConnectionFactory");
QueueConnection connection = factory.createQueueConnection();
connection.start();
return connection;
}
public static QueueSession getQueueSession(QueueConnection connection)
throws JMSException {
QueueSession session =
connection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
return session;
}
public static Queue getQueue(Context context, String name)
throws NamingException {
Queue queue = (Queue) context.lookup(name);
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;
}
}
|
一旦生成了
RouterService.java,那么最后一步就是作为 Web 服务来部署它。进行这项工作依靠应用程序服务器以及您所使用的开发工具的支持。例如,我使用 IBM®WebSphere® Application Server Version 5.1 和 WebSphere Studio 应用程序开发工具集。不考虑工具集的作用,
RouterService 类成为 Web 服务的基础的实现类。一个有关路由器接收所有来源于服务接口的请求的实例(见下面的
清单 4)。
清单 4. RouterService WSDL 描述
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions
targetNamespace="http://three.wspattern.developerworks.ibm.com"
xmlns:impl="http://three.wspattern.developerworks.ibm.com"
xmlns:intf="http://three.wspattern.developerworks.ibm.com"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<wsdl:types>
<schema
targetNamespace="http://three.wspattern.developerworks.ibm.com"
xmlns="http://www.w3.org/2001/XMLSchema"
xmlns:impl="http://three.wspattern.developerworks.ibm.com"
xmlns:intf="http://three.wspattern.developerworks.ibm.com"
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<element name="action" nillable="true" type="xsd:string"/>
<element name="target" nillable="true" type="xsd:string"/>
<element name="processReturn" nillable="true" type="xsd:string"/>
</schema>
</wsdl:types>
<wsdl:message name="processRequest">
<wsdl:part name="action" type="xsd:string"/>
<wsdl:part name="target" type="xsd:string"/>
</wsdl:message>
<wsdl:message name="processResponse">
<wsdl:part name="processReturn" type="xsd:string"/>
</wsdl:message>
<wsdl:portType name="RouterService">
<wsdl:operation name="process" parameterOrder="action target">
<wsdl:input message="intf:processRequest" name="processRequest"/>
<wsdl:output message="intf:processResponse" name="processResponse"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name=
"RouterServiceSoapBinding" type="intf:RouterService">
<wsdlsoap:binding style=
"rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="process">
<wsdlsoap:operation soapAction=""/>
<wsdl:input name="processRequest">
<wsdlsoap:body
namespace="http://three.wspattern.developerworks.ibm.com"
use="literal"/>
</wsdl:input>
<wsdl:output name="processResponse">
<wsdlsoap:body
namespace="http://three.wspattern.developerworks.ibm.com"
use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="RouterServiceService">
<wsdl:port binding="intf:RouterServiceSoapBinding" name="RouterService">
<wsdlsoap:address
location="http://localhost:9080/WSPattern3/services/RouterService"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
|
一旦实现了此服务,部署完 Web 应用程序并启动了 Java Naming and Directory Interface(JNDI)提供者以及应用程序服务器(按这种顺序,否则实例将无法运行)之后,您就可以开始测试服务了(见下面
清单 5)。
清单 5. 与 RouterService 进行信息转换的实例
Request
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SOAP-ENV:Body>
<m:process xmlns:m="http://three.wspattern.developerworks.ibm.com">
<action>upper</action>
<target>this is a test</target>
</m:process>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Response
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Header/>
<soapenv:Body>
<p317:processResponse xmlns:p317=
"http://three.wspattern.developerworks.ibm.com">
<processReturn>THIS IS A TEST</processReturn>
</p317:processResponse>
</soapenv:Body>
</soapenv:Envelope>
Request
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SOAP-ENV:Body>
<m:process xmlns:m="http://three.wspattern.developerworks.ibm.com">
<action>lower</action>
<target>THIS IS A TEST</target>
</m:process>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Response
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Header/>
<soapenv:Body>
<p317:processResponse xmlns:p317=
"http://three.wspattern.developerworks.ibm.com">
<processReturn>this is a test</processReturn>
</p317:processResponse>
</soapenv:Body>
</soapenv:Envelope>
|

 |

|
结束语
作为文章的收尾,我们从这里给的实例中得出两点结论,您应当引起注意。第一,在例子中,您使用了一个输入参数来指定您想执行的操作。另一种方法,您可以使用 WS-Addressing 规范中的动作 SOAP 报头来指定操作。
更进一步地说,在本例中,活动是需求的外在因素。在许多路由器的执行过程中,您依据标准给出的发送指令可能没有比用专门的参数来得明显。例如,购买指令流程服务可能根据指令中包括的所有数据发出信息,或者依据购买者的身份验证等等。正如先前指出的,这些标准是动态的,可能在运行时改变,或者依据一系列既定的业务参数,这可能通过硬件编码输入到应用程序逻辑中。
许多当前的 Web 服务实现趋向于将硬代码商业逻辑直接归入服务类,它支持由 Web 服务接口定义的 WSDL。然而,这样做使得 Web 服务很不稳定,而且使得应用程序不能充分利用实现 Web 服务理念的松散连接的基本原理。而本系列中给出的技巧方法都是简单易掌握的,它们对提高面向服务的应用程序的设计水平大有帮助。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| EAR file | ws-pattern3.zip | 1397 KB | HTTP |
|---|
参考资料
关于作者  | 
|  | James Snell 是 IBM Emerging Technologies Toolkit 开发组的成员,过去的几年里他主要研究新兴的 Web 服务技术及标准。目前,他正在维护 developerWorks 中关于新技术的
网络日志。
|
对本文的评价
|