爲異步 Web Services 創建支持回調的客戶端

Web services 是一種很有前途的技術,在面向服務的架構( Service Oriented Architectures , SOA )中起着重要的作用。這種正在興起的技術的一個關鍵方面就是提供了異步服務的能力。儘管現在的 web service 標準規範中包括了提供異步服務的內容,但客戶端應用程序前景的細節還有一些混亂和模糊。 Web services 回調是實現這些異步服務的一個重要因素。這篇文章爲創建帶有回調操作的 Web services 的客戶應用程序提供了實踐指導。這篇文章中所有的代碼段都來自於您可以下載的例子。這些例子包含了這些代碼段的完整實現和使用指導。

  術語

  在開始討論支持回調的客戶端之前,闡明相關術語是很重要的。下圖就顯示了客戶端使用帶有回調操作的 Web service 時所涉及到的主要實體。

  圖 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() )通知客戶端。

  圖 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 進行回調操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章