精彩的人生

好好工作,好好生活

BlogJava 首页 新随笔 联系 聚合 管理
  147 Posts :: 0 Stories :: 250 Comments :: 0 Trackbacks
  Web services 是一种很有前途的技术,在面向服务的架构( Service Oriented Architectures , SOA )中起着重要的作用。这种正在兴起的技术的一个关键方面就是提供了异步服务的能力。尽管现在的 web service 标准规范中包括了提供异步服务的内容,但客户端应用程序前景的细节还有一些混乱和模糊。 Web services 回调是实现这些异步服务的一个重要因素。这篇文章为创建带有回调操作的 Web services 的客户应用程序提供了实践指导。这篇文章中所有的代码段都来自于您可以下载的例子。这些例子包含了这些代码段的完整实现和使用指导。

术语

  在开始讨论支持回调的客户端之前,阐明相关术语是很重要的。下图就显示了客户端使用带有回调操作的 Web service 时所涉及到的主要实体。

Figure 1
图 1. 调用 Web service 的客户端

  上图描述了客户端对 Web service 的调用。 Web service 能够在一段时间后通过对客户端的回调做出响应 。因此,包含回调操作的 Web service 客户端的特别之处在于,客户端本身必须提供一个端点。我们调用这一回调端点 ,并将这个端点定义为由 URI 确定的唯一地址, SOAP 请求消息将发送到这个 URI 。

  将 SOAP 消息发送到Web service 端点 之后,客户端本身开始与 Web service 进行交互。由客户端发送给 Web service 的相关请求消息及其相关响应消息构成了客户端初始化操作 。如前所述,客户能够处理 Web service 发送到回调端点的请求消息。相关的请求和响应消息一起被称为一个回调 操作。

  理解这些术语之后,让我们走近一步考察 Web service 回调的概念,以及它与会话式 Web services 和异步 Web service 调用之间的关系。

异步、回调和会话

  异步 Web service 调用的概念有时容易与 Web services 回调和会话式 Web services 相混淆。虽然这三个概念很相关,但却不同。

  异步 Web services 调用 是指在不阻塞接收服务器发来的相应响应消息的情况下,客户端能够发送 SOAP 消息请求 。 BEA WebLogic Platform 8.1 web services 上进行异步 Web service 调用的过程已经详细地 记录在软件文档中了 ,因此在本文中不作进一步的讨论。

  Web services 回调 是指 Web services 提供者向客户端发回 SOAP 消息的情况。 Web Services Description Language (WSDL) specifications 将这种操作定义为一种“请求 / 响应”。支持回调操作的 Web services 客户端本身必须有一个 Web services 端点,这样 Web service 就可以利用这个 Web services 端点在任意时间发送回调请求,也就是说,可以是异步的。注意,这跟我们上面讨论的异步调用 没有关联。

  如果一系列可在两个端点之间来回传送的消息可以被唯一会话 ID 追踪,而这个 ID 又由特定的操作来标识消息流的始末,这种 Web services 就是 会话式 的。提供回调的 Web services 也定义为会话式的。这是因为正常情况下如果 Web services 能够对客户端进行回调访问,它就必须有它自己的回调端点信息。这种情况只有客户端做出了初始调用以后才会发生。因此,至少由客户启动的初始化操作和由 Web services 做出的回调操作是相互关联的,并且由唯一的会话 ID 跟踪。如果不是这样,客户端就无法分辨与不同初始调用相关联的回调操作。

  我们现在将考虑这些问题并创建支持回调的客户端,就像我们刚才所看到的,这些客户端构成了请求 - 响应和会话式 Web services 的基础。

创建支持回调的客户端

  正如前面讨论的,支持回调的 Web services 客户端需要提供一个能够异步接收和处理回调操作消息的回调端点。为避免必须提供回调端点这类复杂的事情,一种叫做 polling (轮询)的技术可以作为替代技术。然而这种技术要求客户端周期性地调用服务端以校验回调事件。如果这种事件很少发生,那么调用的开销就太大了。如果客户端提供一个回调端点并直接处理回调操作,这种开销就可以避免。

  我们还应该注意到,尽管可以通过用 JMS 上的 Web services (如果提供了这种连接)创建支持回调的客户端,但这种方法有一些限制,其中一个重要的限制就是客户端要被迫采用与 Web services 提供者相同的 JMS 实现。因此我们将集中于经过 HTTP 传输来完成我们的任务。有两个领域需要创建这样的客户端:创建适当的客户端启动 SOAP 消息,以及处理回调操作。

  当客户端启动有回调的 Web service 操作时,它需要以某种方式包含回调端点的 URI ,使其在请求消息中监听。 Web Services Addressing SOAP Conversation Protocol 规范都定义了 SOAP 头部元素,允许您实现这一目标。从理论上说,用于规定回调端点的规范并不重要。但是大多数 Web services 容器(包括 BEA WebLogic Server 8.1 )都还没有包含 Web services Addressing 规范的实现形式。当前, BEA WebLogic Workshop 8.1 的 Web services 支持 SOAP Conversation Protocol 规范,我们将在例子客户端中使用。

  根据 SOAP Conversation Protocol , SOAP 头部在会话的不同阶段是不同的。对于会话中的第一个客户端启动(开始)操作,我们需要通过 callbackLocation 头部元素 提供有回调端点的 Web service 。所有操作(包括第一个操作)也将需要唯一的 ID ,这个 ID 在整个会话过程中都用在我们的 SOAP 消息里。这可通过 conversationID 元素 来完成。下面是一个启动支持回调会话的 SOAP 请求消息的例子:

<soapenv:Envelope soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" 
    xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:enc="http://schemas.xmlsoap.org/soap/encoding/" 
    xmlns:env="http://schemas.xmlsoap.org/soap/envelop/" 
    xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">
  <soapenv:Header>
    <con:StartHeader soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next" 
     soapenv:mustUnderstand="0" 
     xmlns:con="http://www.openuri.org/2002/04/soap/conversation/">
      <con:conversationID>[123456]:192.168.1.100:8181</con:conversationID>
         <con:callbackLocation>
          http://192.168.1.100:8181/StockNotificationCallback
         </con:callbackLocation>
      </con:StartHeader>
 </soapenv:Header>
  <soapenv:Body>
    <n1:registerForThresholdNotif xmlns:n1="http://www.openuri.org/">
      <n1:stockTicker>CCC</n1:stockTicker>
      <n1:threshold>10</n1:threshold>
    </n1:registerForThresholdNotif>
  </soapenv:Body>
</soapenv:Envelope>

  现有的大多数 Java Web service 工具包(例如 BEA WebLogic 的 clientgen 、 Apache Axis 和 JWSDP )都允许您创建一个代理库,客户端程序可以容易地用它来调用 Web services 操作。但是,这些框架中没有一种支持回调操作,主要问题是它们的代理不生成所需的头部。在能提供这种支持以前,通过扩展它们对回调操作的支持来利用这些框架(例如复杂类 XML 映射),这种益处还是很需要的。一种用来达到这种效果的技术就是应用 SOAP 消息处理程序

  上面提到的所有 Web services 工具包都能支持 SOAP 消息处理程序。消息处理程序是一种 Java 类,它实现了 javax.xml.rpc.handler.GenericHandler 界面,并且还包含一种称为先送出(或后接收) SOAP 消息的方法。这里介绍我们客户端中的消息处理程序,它能够找出一个特定会话的当前阶段,并据此扩展带有所需头部的请求消息。

  注意到这一点是很重要的,客户端 SOAP 消息处理程序必须能确定消息属于会话的哪个阶段,以便创建合适的头部元素。生成会话 ID 也是客户端处理程序要完成的一个任务。

  一旦 Web services 端点收到扩展的请求消息,它就会将请求消息发送到由开始消息的 callbackLocation 元素规定的回调端点。在大多数情况下,客户端过程自身就需要提供这个端点,并且恰当地处理回调消息。如果客户端在 Web services 的容器中运行,这项工作就可以通过把有回调操作的 Web services 部署在同一个容器内来完成。然而,如果客户端不是正在容器中运行,这项工作就要靠在一个嵌入在客户端过程本身的轻量级容器中执行回调端点来完成。这使我们能够调用客户端生成的操作,并且处理同一过程上下文中的传入回调操作。注意在回调端点背后(和在客户端中)的过程实体要求不仅能够分配对适当域的代码操作,而且还能处理 XML 映射。

  当然,客户端也可以选择简单地设置恰当的 callbackLocation 头部元素来规定一个在容器中的回调端点,而这个容器与访问过程相分离。

  正如我们已经看到的,为带回调操作的 Web services 创建客户端并不是毫无意义的,它包含了复杂元素,比如处理 SOAP 消息、管理会话(包括阶段和 ID )、参数映射以及操作分配。当 Web service 通过 BEA WebLogic Workshop Service Control 访问时,这些复杂性就都不存在了,它会自动为用户执行所有操作。

使用服务控件创建支持回调的客户端

  通过 WebLogic Workshop Service Control 访问 Web services 在 软件文档 中已经做了详细描述。如果客户端实体能够使用控件(也就是 Java Process Definition 业务流程或其他控件; Page Flows 不能使用控件回调),这个解决方案就是最简单的使用支持回调的 Web services 的方案了。采用这种方法,所有涉及访问支持回调的 Web service 的复杂性就都迎刃而解了。

股票通知服务例子

  本文的例子包括一个股票通知( Stock Notification )的会话式 Web service 和一个能阐明概念的示例客户端。 Web service 提供的操作允许客户端注册股票接收机并设置一个相关的阈值价格。然后服务端就监视股票,只要股票价格超过了阈值价格就通过一个回调( onThresholdPassed() )通知客户端。

Figure 2
图 2. 本图说明了这个例子的配置

  很显然,轮询技术不是创建客户端的最佳解决方案:股票价格有可能经常超过阈值价格也可能极少超过阈值价格。轮询间隔短就会造成很多不必要的访问,而另一方面,轮询间隔长又可能导致股票价格超过阈值价格很长时间后客户端才被告知。

  客户端应用程序中回调端点的实现应该是一种更好的解决方案。让我们首先来看一下例子客户端是如何将操作传递给 StockNotification Web service 的。我们已经采用两种不同的技术实现了客户端。第一种技术使用 WebLogic Workshop 生成的代理库,并由 StockNotificationClient 类的 makeCallUsingBEAProxy() 方法来实现:

public void makeCallUsingBEAProxy() {
    try {
      // Get the proxy instance so that we can start calling the web service operations
      StockNotificationSoap sns = null;
      StockNotification_Impl snsi = new StockNotification_Impl();
      sns = snsi.getStockNotificationSoap();

      // Register our conversational handler so that it can properly set our
      // our conversation headers with a callback location
      QName portName = new QName("http://www.openuri.org/",
                                 "StockNotificationSoap");
      HandlerRegistry registry = snsi.getHandlerRegistry();
      List handlerList = new ArrayList();
      handlerList.add(new HandlerInfo(ClientConversationHandler.class, null, null));
      registry.setHandlerChain(portName, handlerList);

      // Register, call some methods, then sleep a bit so that our callback
      // location can receive some notifications and finally finish the conversation.
      sns.registerForThresholdNotif("AAAAA", "100");
      sns.addToRegistration("BBBBB", "5");
      StockList stocks = sns.getAllRegisteredStocks();

      ...

      sns.endAllNotifications();
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
}

  正如从上述代码段中看到的,我们可以使用 clientgen 生成的类。与没有回调的 Web service 调用相比,这段代码唯一强调的就是,在方法一开始就实例化和注册了客户端 SOAP 消息处理程序。这个处理程序截取了发出的 SOAP 消息,并加上必要的会话头部。每当客户端过程发出 SOAP 请求消息时,消息处理程序的 handleRequest() 方法就被调用,它利用了一个包含了输出 SOAP 请求信息的 MessageContext 对象。下面是例子客户端消息处理程序的代码段:

package weblogic.webservice.core.handler;

...

public class ClientConversationHandler
    extends GenericHandler {

public boolean handleRequest(MessageContext ctx) {  
  ...
  if (phase.equals("START")) {
    headerElement
              = (SOAPHeaderElement) header
              .addChildElement("StartHeader",
                               "con",
                               "http://www.openuri.org/2002/04/soap/conversation/");

    headerElement.addChildElement("con:callbackLocation")
              .addTextNode(CALLBACK_URI);
  }
  else if (phase.equals("CONTINUE") || phase.equals("FINISH")) {
    headerElement
              = (SOAPHeaderElement) header
              .addChildElement("ContinueHeader",
                               "con",
                               "http://www.openuri.org/2002/04/soap/conversation/");
  }

  headerElement.addChildElement("con:conversationID").addTextNode(convID);
  ...
}
}

  BEA clientgen 工具生成的代理库已经用了一个缺省的客户端 SOAP 消息处理程序,可创建会话式 StartHeader 头部元素。不幸的是,目前的 clientgen 输出不支持回调操作,因此缺省的处理程序不会创建所需的 callbackLocation 次级元素。所以我们必须保证自己的消息处理程序重写缺省的会话式处理程序,以便管理创建 callbackLocation 头部的额外任务。我们通过确保处理程序在一个与缺省处理程序相同的程序包中创建来做到这一点,这个程序包就是 weblogic.webservice.core.handler ,并且它还有具体的相同类名称 ClientConversationHandler 。注意,这种技术涉及到重写现有会话消息处理程序,但它并不能保证是向前兼容的。这个例子展示了消息处理程序如何用于创建回调头部,以及这种概念如何应用于任何其他支持 SOAP 消息处理程序的 Web services 框架中。

  有一种使用 SOAP 消息处理程序和代理类的可选技术,它使用 JAXM API 直接创建 SOAP 请求消息,并把它们送往 Web service 。这是我们例子客户端采用的第二种技术,它在 StockNotificationClient 类的 makeCallsUsingJAXM() 和 callMethodFromFile() 方法中实现:

public void makeCallsUsingJAXM() {
      callMethodFromFile("C:\\registerForNotifSOAP.xml");
      ...
      callMethodFromFile("C:\\endAllNotifSOAP.xml");
}

private void callMethodFromFile(String fileName) {

    try {
      // Create a SOAP connection object
      SOAPConnectionFactory scf = SOAPConnectionFactory.newInstance();
      SOAPConnection conn = scf.createConnection();

      // Create the SOAP request message from the specified file
      MessageFactory mf = MessageFactory.newInstance();
      SOAPMessage msg = mf.createMessage();
      SOAPPart sp = msg.getSOAPPart();

      // Read the file content into a string and replace the conversation ID with
      // the current one.
      String fileContent = readFileIntoString(fileName);
      fileContent = fileContent.replaceFirst(CONV_ID_HOLDER,
                                             ConvIDGenerator.getInstance().
                                             getConversationID());

      StreamSource prepMsg = new StreamSource(new StringBufferInputStream(
          fileContent));
      sp.setContent(prepMsg);
      msg.saveChanges();

      // Make the actual call
      conn.call(msg, TARGET_SERVICE_URI);
      conn.close();
    }
    catch (Exception ex) {
      throw new RuntimeException(ex);
    }
}   

  callMethodFromFile() 方法从指定的文件读取 SOAP 消息,用客户端的当前会话 ID 取代消息会话 ID ,最后生成实际调用。当使用 JAXM 方法时, Java 到 XML 的映射操作参数和返回值必须由客户端来完成。通过使用从文件中预先生成的 SOAP 消息(它已经包含了所需的参数值),我们已经避免了这一任务。尽管从概念上来说这种方法很好,但对于比较复杂的客户端端来说,这不是一种可行的解决方案。这些客户端必须从头开始通过编程方式使用 JAXM 来创建消息。

  既然我们已经回顾了客户端在生成 Web services 操作中的作用,下面让我们转向客户端的回调端点的实现方式和对来自 Web services 回调消息的处理。因为回调端点需要能够处理输入的 HTTP 请求, servlet 就是一种合适的选择。进一步说,我们非常需要一个 servlet ,它能够从输入的 HTTP 请求中提取 SOAP 消息,并以一种容易使用的方式提供给我们。 javax.xml.messaging 程序包里的 ReqRespListener 接口和 JAXMServlet 类提供给我们这个功能。只要安排合理,应用了这种接口和类的 servlet 将把所有的目标 HTTP post 请求自动转换成 SOAPMessage 实例,并通过 onMessage() 方法传递给我们。下面的代码显示了 onMessage() 方法如何帮助例子客户端实现这些功能。

public class CallBackHandlerServlet
    extends JAXMServlet
    implements ReqRespListener {


  public SOAPMessage onMessage(SOAPMessage message) {
    try {
      // We have just received a SOAP message at the callback
      // endpoint. In this example we will just assume that
      // the message received is in fact the onThresholdPassed
      // callback request. However, a servlet handling
      // multiple callbacks cannot make this assumption and
      // should process the SOAP message to see what callback
      // it relates to and do as appropriate for each.
      String stockTicker = extractOnThresholdPassedArgument(message);

	System.out.println("[DEV2DEV CALLBACK EXAMPLE]: Received treshold notification 
	                                for: " + stockTicker);

      // Now we have to create a proper response to the callback
      // request. Returning this response as part of the onMessage()
      // method will ensure that the SOAP response gets back to the server.
      SOAPMessage response = createThresholdPassedResponse();

      return response;
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  ...
}

  一旦回调请求消息被 onMessage() 方法所接收,客户端就从股票接收机中提取参数,将它输出到标准设备上,并生成一条相应的响应消息,它还会通过 servlet 返回到 Web service 。使用带有多个回调操作的 Web services 客户端也需要把传入的请求分发给客户端代码的合适部分,这个过程是基于相应的回调操作的。

  尽管 servlet 包含了合适代码来处理来自 Web services 的传入 onThresholdPassed() 回调消息,但它需要在监听回调 URI 的 servlet 容器中部署完成,这样才能完成回调端点的实现。 Jetty 开源项目有一个轻量级 servlet 容器,它可以嵌入在我们的客户端中。将 CallBackHandlerServlet servlet 部署在一个嵌入式 Jetty 容器中,允许我们将回调端点驻留在客户端 Java 进程中。 StockNotificationCallbackProcessor 类是一种客户端使用的公共类,它用于启动 Jetty 服务器和部署CallBackHandlerServlet servlet :

public class StockNotificationCallbackProcessor {

  public static final String CALLBACK_SERVER_PORT = ":8181";
  public static final String CALLBACK_SERVLET_CLASS = 
       "asynchwsconsumer.CallBackHandlerServlet";
  public static final String CALLBACK_SERVLET_NAME = "CallBackHandlerServlet";

  private HttpServer server;

  public StockNotificationCallbackProcessor() {}

  public void startListening() {

    try {
      // Configure the server so that we listen on the callback URI
      server = new Server();
      server.addListener(CALLBACK_SERVER_PORT);
      ServletHttpContext context = (ServletHttpContext) server.getContext("/");
      context.addServlet(CALLBACK_SERVLET_NAME, "/*",
                         CALLBACK_SERVLET_CLASS);

      // Start the http server
      server.start();
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void stopListening() {
    try {
      server.stop();
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}

  startListening() 方法把 CallBackHandlerServlet 部署在端口 8181 上并配置嵌入式 servlet 容器,这样所有 HTTP 请求都将指向这个端口。这种方法还启动了嵌入式 servlet 容器。

  现在我们已经考察了 Web service 操作的创建和回调操作的处理,下面我们来看看例子客户端的 main() 方法是如何使用这些元素的:

public static void main(String[] args) {

    StockNotificationClient snClient = new StockNotificationClient();

    snClient.snCallbackProcessor.startListening();

    snClient.makeCallUsingBEAProxy();

    ConvIDGenerator.getInstance().resetConversationID();

    snClient.makeCallsUsingJAXM();

    snClient.snCallbackProcessor.stopListening();
}

  正如您所看到的,这种方法开始于建立一个回调端点(正如前面所看到的,它包括启动嵌入式 servlet 容器和部署回调过程)。该方法接着使用 BEA clientgen 代理来调用股票通知的 Web service 。这样,这种与 Web services 的交互就通过调用 endAllNotifications() 端操作来完成会话,客户端需要改变会话 ID 以启动新会话。客户端使用 ConvIDGenerator 单一类来管理会话 ID ,而 ConvIDGenerator 类又使用 java.rmi.dgc.VMID 类来创建合适的格式化唯一会话 ID 。

  一旦会话 ID 发生了变化,客户端就会用 JAXM 再一次调用股票通知 Web services 。最后,客户端终止嵌入式 servlet 容器,这个过程就结束了。

  我们现在已经完成了对例子客户端的分析,该例子客户端可作为本文的一部分下载。当您运行客户端和股票通知 Web services 的时候,客户端和 Web service 都将把消息显示在标准输出设备上,用以描述会话的不同阶段,包括对回调操作的处理。

结束语

  本文展示了为包含回调操作的 Web services 创建独立客户端过程的步骤。这种客户端需要管理 Web services 调用的会话方面,并提供一个端点用于 Web service 进行回调操作。

下载

CallbackExample.zip (1.7Mb) ——这个文件包括了本文讲述的完整实例应用程序。

参考资料

原文出处

http://dev2dev.bea.com/pub/a/ 2005/03/c allback_clients.html

posted on 2006-05-07 16:19 hopeshared 阅读(1315) 评论(0)  编辑  收藏 所属分类: Web Service

只有注册用户登录后才能发表评论。


网站导航: