結合Direct Web Remoting使用AJAX

轉自:http://soft.yesky.com/472/2253472.shtml

  在上一篇文章中,我介紹瞭如何用 JavaScript 對象標註(JSON)以一種在客戶機上容易轉化成 JavaScript 對象的格式對數據進行序列化。有了這個設置,就可以用 JavaScript 代碼調用遠程服務,並在響應中接收 JavaScript 對象圖,但是又不像遠程過程調用。這一次,將學習如何更進一步,使用一個框架,把從 JavaScript 客戶代碼對服務器端 Java 對象進行遠程調用的能力正式化。

  DWR 是一個開放源碼的使用 Apache 許可協議的解決方案,它包含服務器端 Java 庫、一個 DWR servlet 以及 JavaScript 庫。雖然 DWR 不是 Java 平臺上唯一可用的 Ajax-RPC 工具包,但是它是最成熟的,而且提供了許多有用的功能。請參閱 參考資料,在繼續學習之前下載 DWR。

  DWR 是什麼?

  從最簡單的角度來說,DWR 是一個引擎,可以把服務器端 Java 對象的方法公開給 JavaScript 代碼。使用 DWR 可以有效地從應用程序代碼中把 Ajax 的全部請求-響應循環消除掉。這意味着客戶端代碼再也不需要直接處理 XMLHttpRequest 對象或者服務器的響應。不再需要編寫對象的序列化代碼或者使用第三方工具才能把對象變成 XML。甚至不再需要編寫 servlet 代碼把 Ajax 請求調整成對 Java 域對象的調用。

  DWR 是作爲 Web 應用程序中的 servlet 部署的。把它看作一個黑盒子,這個 servlet 有兩個主要作用:首先,對於公開的每個類,DWR 動態地生成包含在 Web 頁面中的 JavaScript。生成的 JavaScript 包含存根函數,代表 Java 類上的對應方法並在幕後執行 XMLHttpRequest。這些請求被髮送給 DWR,這時它的第二個作用就是把請求翻譯成服務器端 Java 對象上的方法調用並把方法的返回值放在 servlet 響應中發送回客戶端,編碼成 JavaScript。DWR 還提供了幫助執行常見的用戶界面任務的 JavaScript 工具函數。

  關於示例

  在更詳細地解釋 DWR 之前,我要介紹一個簡單的示例場景。像在前一篇文章中一樣,我將採用一個基於在線商店的最小模型,這次包含一個基本的產品表示、一個可以包含產品商品的用戶購物車以及一個從數據存儲查詢產品的數據訪問對象(DAO)。Item 類與前一篇文章中使用的一樣,但是不再實現任何手工序列化方法。圖 1 說明了這個簡單的設置:

圖 1. 說明 Cart、CatalogDAO 和 Item 類的類圖


  在這個場景中,我將演示兩個非常簡單的用例。第一,用戶可以在目錄中執行文本搜索並查看匹配的商品。第二,用戶可以添加商品到購物車中並查看購物車中商品的總價。

  實現目錄

  DWR應用程序的起點是編寫服務器端對象模型。在這個示例中,我從編寫 DAO 開始,用它提供對產品目錄數據存儲的搜索功能。CatalogDAO.java 是一個簡單的無狀態的類,有一個無參數的構造函數。清單 1 顯示了我想要公開給 Ajax 客戶的 Java 方法的簽名:

   清單 1. 通過 DWR 公開的 CatalogDAO 方法

 
/** 
 * Returns a list of items in the catalog that have  
 *  names or descriptions matching the search expression 
 * @param expression Text to search for in item names  
 *  and descriptions  
 * @return list of all matching items 
 */ 
public List<Item> findItems(String expression); 

/** 
 * Returns the Item corresponding to a given Item ID 
 * @param id The ID code of the item 
 * @return the matching Item 
 */ 
public Item getItem(String id); 

  接下來,我需要配置 DWR,告訴它 Ajax 客戶應當能夠構建 CatalogDAO 並調用這些方法。我在清單 2 所示的 dwr.xml 配置文件中做這些事:

   清單 2. 公開 CatalogDAO 方法的配置
 
<!DOCTYPE dwr PUBLIC 
  "-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN" 
  "http://www.getahead.ltd.uk/dwr/dwr10.dtd"> 
<dwr> 
  <allow> 
    <create creator="new" javascript="catalog"> 
      <param name="class"  
        value="developerworks.ajax.store.CatalogDAO"/> 
      <include method="getItem"/>  
      <include method="findItems"/>  
    </create>  
    <convert converter="bean"  
      match="developerworks.ajax.store.Item"> 
      <param name="include"  
        value="id,name,description,formattedPrice"/> 
    </convert> 
  </allow> 
</dwr> 

  dwr.xml 文檔的根元素是 dwr。在這個元素內是 allow 元素,它指定 DWR 進行遠程的類。allow 的兩個子元素是 create 和 convert。

  create 元素

  create 元素告訴 DWR 應當公開給 Ajax 請求的服務器端類,並定義 DWR 應當如何獲得要進行遠程的類的實例。這裏的 creator 屬性被設置爲值 new,這意味着 DWR 應當調用類的默認構造函數來獲得實例。其他的可能有:通過代碼段用 Bean 腳本框架(Bean Scripting Framework,BSF)創建實例,或者通過與 IOC 容器 Spring 進行集成來獲得實例。默認情況下,到 DWR 的 Ajax 請求會調用 creator,實例化的對象處於頁面範圍內,因此請求完成之後就不再可用。在無狀態的 CatalogDAO 情況下,這樣很好。

  create 的 javascript 屬性指定從 JavaScript 代碼訪問對象時使用的名稱。嵌套在 create 元素內的 param 元素指定 creator 要創建的 Java 類。最後,include 元素指定應當公開的方法的名稱。顯式地說明要公開的方法是避免偶然間允許訪問有害功能的良好實踐 —— 如果漏了這個元素,類的所有方法都會公開給遠程調用。反過來,可以用 exclude 元素指定那些想防止被訪問的方法。

  convert元素

  creator負責公開用於 Web 遠程的類和類的方法,convertor 則負責這些方法的參數和返回類型。convert 元素的作用是告訴 DWR 在服務器端 Java 對象表示和序列化的 JavaScript 之間如何轉換數據類型。

  DWR自動地在Java和JavaScript 表示之間調整簡單數據類型。這些類型包括Java原生類型和它們各自的類表示,還有 String、Date、數組和集合類型。DWR 也能把 JavaBean 轉換成 JavaScript 表示,但是出於安全性的原因,做這件事要求顯式的配置。

  清單2中的 convert 元素告訴 DWR 用自己基於反射的 bean 轉換器處理 CatalogDAO 的公開方法返回的 Item,並指定序列化中應當包含 Item 的哪個成員。成員的指定採用 JavaBean 命名規範,所以 DWR 會調用對應的 get 方法。在這個示例中,我去掉了數字的 price 字段,而是包含了 formattedPrice 字段,它採用貨幣格式進行顯示。

  現在,我準備把 dwr.xml 部署到 Web 應用程序的 WEB-INF 目錄,在那裏 DWR servlet 會讀取它。但是,在繼續之前,確保每件事都按照希望的那樣運行是個好主意。

  測試部署

  如果DWRServlet的web.xml定義把 init-param debug 設置爲 true,那麼就啓用了 DWR 非常有幫助的測試模式。導航到 /{your-web-app}/dwr/ 會把 DWR 配置的要進行遠程的類列表顯示出來。在其中點擊,會進入指定類的狀態屏幕。CatalogDAO 的 DWR 測試頁如圖 2 所示。除了提供粘貼到 Web 頁面的 script 標記(指向 DWR 爲類生成的 JavaScript)之外,這個屏幕還提供了類的方法列表。這個列表包括從類的超類繼承的方法,但是只有在 dwr.xml 中顯式地指定爲遠程的才標記爲可訪問。

圖 2. CatalogDAO 的 DWR 測試頁
DWR 爲 CatalogDAO 生成的診斷和測試頁

  可以在可訪問的方法旁邊的文本框中輸入參數值並點擊 Execute 按鈕調用方法。服務器的響應將在警告框中用 JSON 標註顯示出來,如果是簡單值,就會內聯在方法旁邊直接顯示。這個測試頁非常有用。它們不僅允許檢查公開了哪個類和方法用於遠程,還可以測試每個方法是否像預期的那樣工作。

  如果對遠程方法的工作感到滿意,就可以用 DWR 生成的 JavaScript 存根從客戶端代碼調用服務器端對象。

  調用遠程對象

  遠程 Java 對象方法和對應的 JavaScript 存根函數之間的映射很簡單。通用的形式是 JavaScriptName.methodName(methodParams ..., callBack),其中 JavaScriptName 是 creator 的 javascript 屬性指定的名稱,methodParams 代表 Java 方法的 n 個參數,callback 是要用 Java 方法的返回值調用的 JavaScript 函數。如果熟悉 Ajax,可以看出這個回調機制是 XMLHttpRequest 異步性的常用方式。

  在示例場景中,我用清單 3 中的 JavaScript 函數執行搜索,並用搜索結果更新用戶界面。這個清單還使用來自 DWR 的 util.js 的便捷函數。要特別說明的是名爲 $() 的 JavaScript 函數,可以把它當作 document.getElementById() 的加速版。錄入它當然更容易。如果您使用過 JavaScript 原型庫,應當熟悉這個函數。

   清單 3. 從客戶機調用遠程的 findItems()

 
/* 
 * Handles submission of the search form 
 */ 
function searchFormSubmitHandler() { 

  // Obtain the search expression from the search field 
  var searchexp = $("searchbox").value; 

  // Call remoted DAO method, and specify callback function 
  catalog.findItems(searchexp, displayItems); 

  // Return false to suppress form submission 
  return false; 
} 
        
/* 
 * Displays a list of catalog items 
 */ 
function displayItems(items) { 

  // Remove the currently displayed search results 
  DWRUtil.removeAllRows("items"); 

  if (items.length == 0) { 
    alert("No matching products found"); 
    $("catalog").style.visibility = "hidden"; 
  } else { 

    DWRUtil.addRows("items",items,cellFunctions); 
    $("catalog").style.visibility = "visible"; 
  } 
}

  在上面的 searchFormSubmitHandler() 函數中,我們感興趣的代碼當然是 catalog.findItems(searchexp, displayItems);。這一行代碼就是通過網絡向 DWR servlet 發送 XMLHttpRequest 並用遠程對象的響應調用 displayItems() 函數所需要的全部內容。

  displayItems() 回調本身是由一個 Item 數組表示調用的。這個數組傳遞給 DWRUtil.addRows() 便捷函數,同時還有要填充的表的 ID 和一個函數數組。表中每行有多少單元格,這個數組中就有多少個函數。按照順序使用來自數組的 Item 逐個調用每個函數,並用返回的內容填充對應的單元格。

  在這個示例中,我想讓商品表中的每一行都顯示商品的名稱、說明和價格,並在最後一列顯示商品的 Add to Cart 按鈕。清單 4 顯示了實現這一功能的單元格函數數組:

   清單 4. 填充商品表的單元格函數數組
 
/* 
 * Array of functions to populate a row of the items table 
 * using DWRUtil's addRows function 
 */ 
var cellFunctions = [ 
  function(item) { return item.name; }, 
  function(item) { return item.description; }, 
  function(item) { return item.formattedPrice; }, 
  function(item) { 
    var btn = document.createElement("button"); 
    btn.innerHTML = "Add to cart"; 
    btn.itemId = item.id; 
    btn.onclick = addToCartButtonHandler; 
    return btn; 
  } 
]; 

  前三個函數只是返回 dwr.xml 中 Item 的 convertor 包含的字段內容。最後一個函數創建一個按鈕,把 Item 的 ID 賦給它,並指定在點擊按鈕時應當調用名爲 addToCartButtonHandler 的函數。這個函數是第二個用例的入口點:向購物車中添加 Item。

實現購物車

DWR 的安全性

DWR 設計時就考慮了安全性。使用 dwr.xml 明確地列出那些想做遠程處理的類和方法,可以避免意外地把那些可能被惡意利用的功能公開出去。除此之外,使用調試測試模式,可以容易地審計所有公開到 Web 上的類和方法。

DWR 也支持基於角色的安全性。通過 bean 的 creator 配置,可以指定用戶訪問特定 bean 所必須屬於的 J2EE 角色。通過部署多個 URL 受保護的 DWRServlet 實例,每個實例都有自己的 dwr.xml 配置文件,也可以提供擁有不同遠程功能的用戶集。

  用戶購物車的 Java 表示基於 Map。當 Item 添加到購物車中時,Item 本身作爲鍵被插入 Map。 Map 中對應的值是一個 Integer,代表購物車中指定 Item 的數量。所以 Cart.java 有一個字段 contents,聲明爲 Map<Item,Integer>。

  使用複雜類型作爲哈希鍵給 DWR 帶來一個問題 —— 在 JavaScript 中,數組的鍵必須是標量的。所以,DWR 無法轉換 contents Map。但是,對於購物車用戶界面來說,用戶需要查看的只是每個商品的名稱和數量。所以我向 Cart 添加了一個名爲 getSimpleContents() 的方法,它接受 contents Map 並根據它構建一個簡化的 Map<String,Integer>,只代表每個 Item 的名稱和數量。這個用字符串作爲鍵的 map 表示可以由 DWR 的轉換器轉換成 JavaScript。

  客戶對 Cart 感興趣的其他字段是 totalPrice,它代表購物車中所有商品的金額彙總。使用 Item,我還提供了一個合成的成員叫作 formattedTotalPrice,它是金額彙總的格式化好的 String 表示。

  轉換購物車

  爲了不讓客戶代碼對 Cart 做兩個調用(一個獲得內容,一個獲得總價),我想把這些數據一次全都發給客戶。爲了做到這一點,我添加了一個看起來有點兒怪的方法,如清單 5 所示:

   清單 5. Cart.getCart() 方法
 
/** 
 * Returns the cart itself - for DWR 
 * @return the cart 
 */  
public Cart getCart() { 
  return this; 
} 

  雖然這個方法在普通的 Java 代碼中可能完全是多餘的(因爲在調用這個方法時,已經有對 Cart 的引用),但它允許 DWR 客戶讓 Cart 把自己序列化成 JavaScript。

  除了getCart(),需要遠程化的另一個方法是 addItemToCart()。這個方法接受目錄 Item 的 ID 的 String 表示,把這個商品添加到 Cart 中並更新總價。方法還返回 Cart,這樣客戶代碼在一個操作中就能更新 Cart 的內容並接收購物車的新狀態。

  清單 6 是擴展的 dwr.xml 配置文件,包含 Cart 類進行遠程所需要的額外配置:

   清單 6. 修改過的 dwr.xml 包含了 Cart 類
 
<!DOCTYPE dwr PUBLIC 
    "-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN" 
    "http://www.getahead.ltd.uk/dwr/dwr10.dtd"> 
<dwr> 
  </allow> 
    </create creator="new" javascript="catalog"> 
      </param name="class"  
        value="developerworks.ajax.store.CatalogDAO"/> 
      </include method="getItem"/> 
      </include method="findItems"/> 
    <//create> 
    </convert converter="bean"  
      match="developerworks.ajax.store.Item"> 
      </param name="include"  
        value="id,name,description,formattedPrice"/> 
    <//convert> 
    </create creator="new" scope="session" javascript="Cart"> 
      </param name="class"  
        value="developerworks.ajax.store.Cart"/> 
      </include method="addItemToCart"/> 
      </include method="getCart"/> 
    <//create> 
    </convert converter="bean"  
      match="developerworks.ajax.store.Cart"> 
      </param name="include"  
        value="simpleContents,formattedTotalPrice"/> 
    <//convert> 
  <//allow> 
</dwr> 

  在這個版本的 dwr.xml 中,我添加了 Cart 的 creator 和 convertor。create 元素指定應當把 addItemToCart() 和 getCart() 方法遠程化,而且重要的是,生成的 Cart 實例應當放在用戶的會話中。所以,購物車的內容在用戶的請求之間會保留。

  Cart 的 convert 元素是必需的,因爲遠程的 Cart 方法返回的是 Cart 本身。在這裏我指定在 Cart 的序列化 JavaScript 形式中應當存在的成員是 simpleContents 這個圖和 formattedTotalPrice 這個字符串。

  如果對這覺得有點兒不明白,那麼只要記住 create 元素指定的是 DWR 客戶可以調用的 Cart 服務器端方法,而 convert 元素指定在 Cart 的 JavaScript 序列化形式中包含的成員。

  現在可以實現調用 Cart 的遠程方法的客戶端代碼了。

  調用遠程的 Cart 方法

  首先,當商店的 Web 頁首次裝入時,我想檢查保存在會話中的 Cart 的狀態,看是否已經有一個購物車了。這是必需的,因爲用戶可能已經向 Cart 中添加了商品,然後刷新了頁面或者導航到其他地方之後又返回來。在這些情況下,重新載入的頁面需要用會話中的 Cart 數據對自己進行同步。我可以在頁面的 onload 函數中用一個調用做到這一點,就像這樣:Cart.getCart(displayCart)。請注意 displayCart() 是一個回調函數,由服務器返回的 Cart 響應數據調用。

  如果 Cart 已經在會話中,那麼creator 會檢索它並調用它的 getCart() 方法。如果會話中沒有 Cart,那麼 creator 會實例化一個新的,把它放在會話中,並調用 getCart() 方法。

  清單 7 顯示了 addToCartButtonHandler() 函數的實現,當點擊商品的 Add to Cart 按鈕時會調用這個函數:

   清單 7. addToCartButtonHandler() 實現
 
/* 
 * Handles a click on an Item's "Add to Cart" button 
 */ 
function addToCartButtonHandler() { 

  // 'this' is the button that was clicked. 
  // Obtain the item ID that was set on it, and 
  // add to the cart. 
  Cart.addItemToCart(this.itemId,displayCart); 
} 

  由 DWR 負責所有通信,所以客戶上的添加到購物車行爲就是一個函數。清單 8 顯示了這個示例的最後一部分 —— displayCart() 回調的實現,它用 Cart 的狀態更新用戶界面:

   清單 8. displayCart() 實現
 
/* 
 * Displays the contents of the user's shopping cart 
 */ 
function displayCart(cart) { 

  // Clear existing content of cart UI 
  var contentsUL = $("contents"); 
  contentsUL.innerHTML=""; 

  // Loop over cart items 
  for (var item in cart.simpleContents) { 

    // Add a list element with the name and quantity of item 
    var li = document.createElement("li"); 
    li.appendChild(document.createTextNode( 
                    cart.simpleContents[item] + " x " + item 
                  )); 
    contentsUL.appendChild(li); 
  } 

  // Update cart total 
  var totalSpan = $("totalprice"); 
  totalSpan.innerHTML = cart.formattedTotalPrice; 
} 

  在這裏重要的是要記住,simpleContents 是一個把 String 映射到數字的 JavaScript 數組。每個字符串都是一個商品的名稱,關聯數組中的對應數字就是購物車中該商品的數量。所以表達式 cart.simpleContents[item] + " x " + item 可能就會計算出 “2 x Oolong 128MB CF Card” 這樣的結果。

 DWR 商店應用程序

  圖3 顯示了這個基於 DWR 的 Ajax 應用程序的使用情況:顯示了通過搜索檢索到的商品,並在右側顯示用戶的購物車:

圖 3. 基於 DWR 的 Ajax 商店應用程序的使用情況
示例場景的截屏,帶有搜索結果和購物車

  DWR 的利弊

調用批處理

在 DWR 中,可以在一個 HTTP 請求中向服務器發送多個遠程調用。調用 DWREngine.beginBatch() 告訴 DWR 不要直接分派後續的遠程調用,而是把它們組合到一個批請求中。DWREngine.endBatch() 調用則把批請求發送到服務器。遠程調用在服務器端順序執行,然後調用每個 JavaScript 回調。

批處理在兩方面有助於降低延遲:第一,避免了爲每個調用創建 XMLHttpRequest 對象並建立相關的 HTTP 連接的開銷。第二,在生產環境中,Web 服務器不必處理過多的併發 HTTP 請求,改進了響應時間。

  現在可以看出用 DWR 實現由 Java 支持的 Ajax 應用程序有多麼容易了。雖然示例場景很簡單,我實現用例的手段也儘可能少,但是不應因此而低估 DWR 引擎相對於自己設計 Ajax 應用程序可以節約的工作量。在前一篇文章中,我介紹了手工設計 Ajax 請求和響應、把 Java 對象圖轉化成 JSON 表示的全部步驟,在這篇文章中,DWR 替我做了所有這些工作。我只編寫了不到 50 行 JavaScript 就實現了客戶機,而在服務器端,我需要做的所有工作就是給常規的 JavaBean 加上一些額外方法。

  當然,每種技術都有它的不足。同任何 RPC 機制一樣,在 DWR 中,可能很容易忘記對於遠程對象進行的每個調用都要比本地函數調用昂貴得多。DWR 在隱藏 Ajax 的機械性方面做得很好,但是重要的是要記住網絡並不是透明的 —— 進行 DWR 調用會有延遲,所以應用程序的架構應當讓遠程方法的粒度比較粗。正是爲了這個目的,addItemToCart() 才返回 Cart 本身。雖然讓 addItemToCart() 作爲一個 void 方法可能更自然,但是這樣的話對它的每個 DWR 調用後面都必須跟着一個 getCart() 調用以檢索修改後的 Cart 狀態。

  對於延遲,DWR 在調用的批處理中有自己的解決方案(請參閱側欄的 調用批處理)。如果不能爲應用程序提供適當粗粒度的 Ajax 接口,那麼只要有可能把多個遠程調用組合到一個 HTTP 請求中,就請使用調用批處理。

  分離的問題

  從實質上看,DWR 在客戶端和服務器端代碼間形成了緊密的耦合,這有許多含義:首先,遠程方法 API 的變化需要在 DWR 存根調用的 JavaScript 上反映出來。第二(也是最明顯的),這種耦合會造成對客戶端的考慮會滲入服務器端代碼。例如,因爲不是所有 Java 類型都能轉化成 JavaScript,所以有時有必要給 Java 對象添加額外方法,好讓它能夠更容易地遠程化。在示例場景中,我通過把 getSimpleContents() 方法添加到 Cart 來解決這個問題。我還添加了 getCart() 方法,它在 DWR 場景中是有用的,但在其他場景中則完全是多餘的。由於遠程對象粗粒度 API 的需要以及把某些 Java 類型轉化成 JavaScript 的問題,所以可以看到遠程 JavaBean 會被那些只對 Ajax 客戶有用的方法“污染”。

  爲了克服這個問題,可以使用包裝器類把額外的特定於 DWR 的方法添加到普通 JavaBean。這意味着 JavaBean 類的 Java 客戶可能看不到與遠程相關聯的額外的毛病,而且也允許給遠程方法提供更友好的名稱 —— 例如用 getPrice() 代替 getFormattedPrice()。圖 4 顯示的 RemoteCart 類對 Cart 進行了包裝,添加了額外的 DWR 功能:

圖 4. RemoteCart 爲遠程功能對 Cart 做了包裝
RemoteCart 包裝器類的類圖

  最後,需要記住:DWR Ajax 調用是異步的,所以不要期望它們會按照分派的順序返回。在示例代碼中我忽略了這個小問題,但是在這個系列的第一篇文章中,我演示瞭如何爲響應加時間戳,以此作爲保證數據到達順序的一種簡單手段。

  結束語

  正如所看到的,DWR 提供了許多東西 —— 它允許迅速而簡單地創建到服務器端域對象的 Ajax 接口,而不需要編寫任何 servlet 代碼、對象序列化代碼或客戶端 XMLHttpRequest 代碼。使用 DWR 部署到 Web 應用程序極爲簡單,而且 DWR 的安全性特性可以與 J2EE 基於角色的驗證系統集成。但是 DWR 並不是對於任何一種應用程序架構都適合,所以在設計域對象的 API 時需要做些考慮。

  如果想學習用 DWR 進行 Ajax 的利弊的更多內容,最好的方式就是下載並開始實踐。DWR 有許多我沒有介紹的特性, 文章源代碼 是把 DWR 投入使用的一個良好起點。請參閱 參考資料,學習關於 Ajax、DWR 和相關技術的更多內容。

  這個系列中要指出的最重要的一點是:對於 Ajax 應用程序,沒有包治百病的解決方案。Ajax 是一個快速發展的領域,不斷有新技術涌現。在這個系列的文章中,我的重點在於帶您開始在 Ajax 應用程序的 Web 層中利用 Java 技術 —— 不管是選擇基於 XMLHttpRequest 的帶有對象序列化框架的技術,還是選擇 DWR 這樣的更高級抽象。

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