程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 純 servlet:重新考慮視圖

純 servlet:重新考慮視圖

編輯:關於JAVA

設計 JSP 的目的是將 Web 開發人員的任務與設計動態頁面 UI 的非開發人 員的任務分離開來。遺憾的是,JSP 對於許多設計人員來說太復雜了,為解決各 種動態內容問題添加的軟件層讓他們覺得非常棘手。(例如,國際化要求將文本 存儲在其他地方並通過鍵來引用。)所以對於大多數項目,Java 開發人員只好 自己處理 JSP 代碼,這常常會包含本屬於設計人員的工作,使他們的精力消耗 在標記庫和其他東西上,無法集中於 Java 代碼。

與正統的方式不同,可以使用簡單的 helper 對象,根據常規 servlet 構建 簡潔優美的 Web 界面。本文講解如何以標准的 Java 形式編寫動態 Web 頁面的 視圖輸出。我將解釋這種方法的好處,並用一個計分應用程序演示這種方法,這 個程序管理一個 NCAA 三月狂熱 獎金池。

HTML 是動態的

這種純 servlet 方法非常簡單。它涉及一個 servlet 基類和一個定制的寫 出器對象,servlet 子類使用這個對象產生輸出。代碼很簡潔,因為大多數 HTML 封裝在 helper 對象的方法中,都可以按需重寫。代碼重用總是令人愉快 ,而且大多數 Web 站點的頁面共享許多 HTML,所以重用應該是個重要的考慮因 素。HTML 輸出方法產生直觀緊湊的 servlet 代碼,因此可維護性很高,這使代 碼的維護成本差不多直接與代碼規模成正比。通過將 JSP 界面重寫成純 servlet,可以將代碼縮減三分之二。

例如,要根據用戶權限輸出一個鏈接,就需要下面這樣冗長的構造代碼:

<c:if test="${user.permission[ sessionScope.ConstantMap[ EDIT_WIDGET ] ] != 0}">
  <c:url var="editUrl" value="/EditWidget.jsp"/>
  <div class="navigation"><a href="<c:out value="${editUrl}"/>">Edit
     this widget</a></div>
</c:if>

通過使用 Java 語法,代碼就簡潔多了:

if (user.getPermission(Constants.EDIT_WIDGET) != 0)
  out.printNavlinkDIV("/EditWidget.jsp", "Edit this widget");

另外,在同一個地方獲取和輸出業務對象,而不是通過請求對象傳遞它們, 這也會節省大量代碼。簡潔是美。

使用 JSP 和其他視圖技術可能是 Web 開發中最讓人頭疼的部分。JSP 頁面 不是 HTML 或 XML、Java 代碼、JavaServer Pages Standard Tag Library (JSTL)代碼或表達式語言(EL),而是這些東西的大雜燴。JSP 代碼不但是奇 怪的組合體,而且每個抽象層都給開發帶來新的障礙。例如,對 JSP 頁面進行 調試簡直就像探礦那樣困難。您知道某個地方出了毛病,但是無法找到出問題的 位置;神秘難懂的錯誤消息雖然指出了行號,但這個行號往往不是問題的真正所 在。

JSP 技術不能擴展基類,所以代碼重用只能通過 bean、include 文件和定制 的標記庫來進行。標記庫太麻煩,不適合進行有效的重用。為您所做的每處 API 修改維護一個 XML 是非常麻煩的,而且 “標記設計就是語言設計”(參見 參 考資料 中 Noel Bergman 的文章)。結果是在本已分了很多層的接口上又加了 一層。

我們正面對著全新的 World Wide Web。無論 Ajax 能否引領 Web 開發的方 向,Web 站點都會繼續向著更加智能化的方向發展。另外,盡管 HTML 本身總是 聲明性的,但是產生它的代碼卻不一定如此。JSP 技術和其他模板化系統必然過 分復雜,因為它們試圖以聲明式的方式表達本質上動態的輸出。這正是開發人員 無法容忍在 JSP 源代碼中添加 scriptlet 的原因:我們試圖表達的邏輯 具有 各種各樣的形式。

通過將 HTML 封裝成 Java 代碼,可以簡潔地表達輸出邏輯。if 語句和 for 循環可以采用大家熟悉的形式。頁面元素可以重構成方法,這樣就很容易理解和 維護它們。(對較大的 JSP 頁面進行維護是非常麻煩的,非常容易出現錯誤, 尤其是在缺少良好的注釋的情況下。)通過使用純 servlet,可以盡可能增加代 碼重用,因為不需要為每個頁面的構造編寫新的類。

狂熱的設計

為了演示純 servlet 的概念,我為一個 NCAA March Madness 錦標賽獎金池 構建了一個計分界面。(參見 三月狂熱 和 下載)。用戶可以從參加錦標賽的 64 支球隊中選擇他們認為最出色的 20 支球隊,並給每個球隊分配一個加權的 分數。比賽開始之後,他們的選擇就變成只讀的;當比賽結束時,管理員輸入獲 勝球隊的名稱。根據用戶選擇的球隊,自動地計算用戶的累積分數並顯示分數的 排名。

這個項目大約花費了我三周的業余時間,大部分時間花在樣式和圖像上(畢 竟我不是畫家)。除了一個 HTML 文件和其他靜態資源之外,UI 層由 21 個 Java 類組成,根據 JavaNCSS 的度量標准,一共有 1,334 個 Java 語句(參見 參考資料)。

逃離 MVC

這裡演示的純 servlet 設計在客戶機和業務邏輯之間建立一個視圖層。 Model-View-Controller(MVC,或者說 Model 2)實際上不是萬能的,而且支持 它的 Web 框架往往比較難以處理。Spring MVC 和 JavaServer Faces(JSF)太 過復雜,我可以斷言,Struts 的麻煩程度不亞於此,每次調整控制邏輯時都必 須調整臃腫復雜的配置文件。N. Alex Rupp(參見 參考資料)甚至將 MVC 稱為 反模式,一種 “看似聰明其實非常愚蠢的” Web 技術。

例如,開發人員常常誤解 Struts 中 Action 模塊的用途。業務邏輯常常被 放在這裡(如果不是都放在 JSP 中的話)。將視圖和控制器實現為 servlet 可 以促使業務邏輯放入恰當位置,因為 servlet 明確關注與浏覽器的接口。

對於這個項目,我使用了幾個來自我自己的 elseforif-servlet 庫的類(參 見 參考資料)。這是 設計的關鍵,因為它為生成 HTML 提供了一個方便的接口 。但是,本文的重點不是這個庫,而是證明我的方法的優點。

圖 1 是部分類圖,其中的 elseforif-servlet 元素以綠色表示:

圖 1. 部分類圖

樹結構的頂部是一個包含 HTML 字符串常量的接口,它為 HTML 寫出器對象 和使用它們的 servlet 提供了方便。(在後面將看到它們的作用。)接下來是 HTMLWriter 和 HTMLFlexiWriter,它們實現一些基本的低級 HTML 方法,它們 對於任何 Web 站點都是有用的。這兩者之間的區別是,HTMLWriter 直接寫到輸 出中,而 HTMLFlexiWriter 還可以以字符串形式返回輸出。將一個輸出方法的 結果作為參數傳遞給另一個方法常常是很方便的,例如:

out.printA(URL_ELSEFORIF, out.IMG("/img/elseforif.gif", 88, 31));

然後是 MadnessWriter 類,它增加了這個 Web 站點需要的高級輸出特性: 頁眉、頁腳和菜單等常見元素,即這個站點特有的所有重復內容。這是一個輕量 級、非線程安全的對象,抽象 servlet 基類 MadnessServlet 使用一個工廠方 法為各請求實例化此對象。

這個基類負責處理核心 servlet 控制邏輯,使具體子類可以將注意力放在它 們特有的任務上。在設置一些標准的 HTTP 頭並執行一些頁面級安全檢查之後, 它將 MadnessWriter 實例傳遞給受保護的 doBoth() 方法:

protected void doBoth(HttpServletRequest request, HttpServletResponse response,
    HttpSession session, MadnessWriter out) throws ServletException, IOException

MadnessServlet 還實現了 MadnessConstants,它使子類能夠輕松地訪問 HTMLConstants 中定義的靜態值。所以,通過結合使用 MadnessWriter 對象和 這些常量,servlet 實現了非常緊湊的 Java 風格的代碼。

按照 MVC 的說法,servlet(這裡的 UI 基本單元)構成了視圖層和控制層 。對於 HTTP 這樣的無狀態接口,這是有意義的。對視圖的請求和對數據更新的 請求采用同樣的基本形式,這兩者之間沒有明確的區別。為了保持模塊化,我在 一個 servlet 類中實現表單頁面,在另一個 servlet 類中實現它的處理器。但 是,無論怎樣對功能進行分隔,HTML 輸出邏輯、servlet 參數的處理和頁面流 邏輯都自我封閉的同級別的對象。雖然 MVC 對它們進行抽象是出於好意,但是 會導致功能混亂。

業務層的實現應該與視圖層沒有關聯。關鍵是要有一個簡單明了的業務接口 ,這樣的話,UI 代碼就可以只處理 UI 問題。(對於示例應用程序的業務層, 我在 Apache Derby 上構建了一個相當粗糙的 CRUD 接口。)

運行應用程序

這個 Web 應用程序是幾乎完全自含的,但是可能需要修改 web.xml 描述符 中的一些環境屬性,然後才能將它部署到 webapps 目錄中。至少需要指定創建 嵌入式 Derby 實例和存儲它的數據文件的位置。默認設置是 UNIX 路徑 —— /var/derby/ —— 所以如果您運行 Linux,那麼只需要創建這個目錄(並允許 servlet 容器寫這個目錄)。用用戶名 admin 和密碼 password 登錄這個站點 。在下載包的 README 文件中可以找到更多信息。

表單和它的處理器

現在該看看代碼了。在錦標賽的第一輪開始之前,用戶進入 Picks 頁面(見 圖 2),選擇他們喜歡的球隊。在此之後,他們可以通過只讀輸出的形式查看自 己和其他玩家的選擇情況。

圖 2. Picks 頁面

在生成這個頁面時,Picks servlet 做的第一件事情是從業務層獲取它的用 戶對象(在這個系統中,是 Player),並執行一項安全檢查:

PlayerManager playerMan = PlayerManager.GetInstance();
Player player = playerMan.select(session.getAttribute(P_PLAYER_ID), true);
boolean readOnly = GetCutoffDateIsPassed() && ! player.getAdmin();
String playerID = request.getParameter(P_PLAYER_ID);
if (playerID != null)
  if (readOnly || player.getAdmin())
   player = playerMan.select(playerID, true);
  else
   throw new ServletException("You may not view other players' picks"
      " until the cutoff date has passed: " + CutoffDate + ".");

這確保正常用戶根據業務規則查看或編輯選擇的球隊。它還建立一些局部變 量,這些變量將決定頁面的表現,尤其是 readOnly。接下來,建立一個 Team 對象數組,每個對象代表一支參賽球隊。然後,調用一個方法,從數組生成按字 母表排序的 map,下拉控件需要用到這個 map:

TeamManager teamMan = TeamManager.GetInstance();
Team[] teams = teamMan.selectAll();
Map selectTeams = getDropDownMap(teams);

現在,開始輸出:

out.printPreContent(null, out.SCRIPTFile ("/js/picks.js"));

這個方法輸出頁面的第一部分,包括完整的 HEAD 標記、BODY 開始標記和頁 面頂部的徽標。注意指向一個 JavaScript 文件的 URL,它添加在 HEAD 中。您 可能會認為,在部署 WAR 文件時這種方法會失效,因為它將在 URL 的開頭添加 上下文前綴 /madness。實際上,上下文前綴是動態地傳遞給 MadnessWriter 構 造函數的,然後構造函數自動地將它加在任何 URL 的開頭,並加上斜線;如果 您的上下文是不確定的,那麼這個特性就非常有用。

下一個調用輸出主菜單:

out.printMenu(URL_PICKS);

通過傳遞要顯示的頁面的 URL,讓 MadnessWriter 實例跳過這個頁面的鏈接 (也可以禁用它)。然後調用一個方法,開始輸出 TABLE 元素,我將這個元素 稱為框:

out.printBeginBox();

這會開始幾個標記,直到框包含的具體內容為止。(後面將通過一個相似的 調用結束這些標記。注意,上面的 printMenu() 調用了同樣的方法。)這種封 裝方式可以大大簡化調試。例如,我曾經遇到一個 bug,框中的某些邊界 TD 的 寬度是 1%,對於浏覽器窗口來說,這個寬度太大了。我將它改為 0%,從而在一 個地方進行修改就糾正了整個站點上的效果。這可以用定制的標記庫來完成,但 是沒這麼容易。

下面幾行輸出一個或兩個 DIV 元素,第一個在提交表單之後向用戶表示成功 :

if ("true".equals(request.getAttribute (P_SUCCESS)))
out.printDIV("smallHeading", "Team picks were saved successfully.");
out.printDIV("reminder", "(Reminder: \"Pick 20 \" represents the team you"
+ " think likeliest to win. \"Pick 1\" is the least likely.)");

"smallHeading" 和 "reminder" 自變量指定要應用於 DIV 開始標記的層疊 樣式表(CSS)類名,第二個自變量是在 DIV 標記之前輸出的文本。如果 reminder DIV 的內容比較復雜,我會調用 out.printBeginDIV("reminder"), 這個方法只輸出 DIV 開始標記。HTMLWriter 和 HTMLFlexiWriter 中也使用同 樣的命名模式。但是,HTMLConstants 中的字符串常量不太一樣,例如默認的 DIV 開始和結束標記分別使用 DIV 和 END_DIV。

在 reminder 後面,輸出一個表單,其中提供下拉控件讓用戶選擇 20 支球 隊。如果用戶只能查看已經做出的選擇,那麼只輸出球隊的名稱。按照 Java 語 法,這個邏輯的表達非常自然:

if (!readOnly)
  out.printSELECT(P_PICK + i, selectTeams, teamID);
else
  {
  String teamName = (String)(selectTeams.get(teamID));
  out.print((teamName != null) ? teamName : "(no pick)");
  }

printSELECT() 方法為 map 中的每個鍵/值對創建一個 OPTION,它預先選擇 鍵與 teamID 匹配的對象。

為了完成表單,需要輸出顯示在頁面右邊的球隊列表。球隊的數組按照 NCAA 地區和排名進行排序。每個地區有一個小標題,整個列表顯示為兩列。這需要一 些數學計算,所以將它放在一個單獨的方法中,見清單 1:

清單 1. 將輸出代碼放在一個方法中

private void doRegionList(Team[] teams, MadnessWriter out) throws IOException
  {
  out.print(TABLE + TR);
  out.printBeginTD(null, "regionList");
  for (int i = 0; i < teams.length; i++)
   {
   if ((i & 15) == 0)
    {
    if (i == 32)
     {
     out.print(END_TD + NL);
     out.printBeginTD(null, "regionList");
     }
    out.print(NL + DIV);
    out.print(REGION_NAMES[i >> 4]);
    out.print(":" + END_DIV + OL);
    }
   out.print(NL + LI);
   out.printHTMLEscape(teams[i].getFullName());
   out.print(" (");
   out.print((teams[i].getRank() & 15) + 1);
   out.print(")");
   out.print(END_LI);
   if ((i % 16) == 15)
    out.print(END_OL);
   }
  out.print(END_TABLE_3);
  }

END_TABLE_3 常量僅僅是 TD、TR 和 TABLE 結束標記的組合。這種方式似乎 有點兒古怪,但是掌握了它之後,就可以用簡潔的代碼建立良好的 HTML 設計, 這意味著只將它用於頁面結構,而將盡可能多的樣式放在樣式表中。

現在完成這個頁面:

out.printEndBox();
out.printPostContent();

第一行結束前面開始的框,printPostContent() 輸出頁面的其余部分,包括 頁腳。Picks 表單頁面完成了。

處理器 servlet(PicksAction)對提交的 Picks 頁面進行響應,它從請求 對象收集選擇的球隊 ID,並將它們傳遞給業務層來更新適當的 Player 實體, 在此之後返回到 Picks 表單頁面。這裡也執行一項安全檢查,確保用戶在比賽 開始之後無法更新他們的選擇。表單和它的處理器都是 servlet,不需要將它們 寫到單獨的界面。它們都使用同樣的業務對象來響應參數化的浏覽器請求,它們 一起構成一個 UI 組件。如果使用 MVC 框架,那麼就會將原本簡單的事情復雜 化了。

其他方面

盡管 Web 框架往往讓事情變得復雜,但是它們能夠解決許多比較小的問題。 基於 servlet 的設計提供了很高的靈活性,可以適當地解決這些問題,而不需 要依賴於任何解決方案。

安全性

在企業應用程序中,頁面級的安全性往往在 XML 描述符文件中以聲明性方式 來處理。同樣,根據我的經驗,往往需要一個更動態的代碼級接口來管理頁面中 特殊的行為 —— 例如,Picks servlet 中與日期相關的邏輯。這可以用 Servlet API 內置的安全方法來處理,比如請求對象上的 isUserInRole(),也 可以將它寫成單獨的接口。使用 Servlet API 對這兩種方式都有幫助。

國際化

盡管許多框架都可以以屬性文件的形式對文本值進行國際化,但是可以在 HTML 寫出器中用少量代碼實現同樣的結果。可以添加一個方法,比如 printText(),它以一個鍵作為自變量並輸出翻譯後的文本值(text() 會直接返 回文本)。servlet 輸出代碼仍然很簡潔,而且執行與等效的 JSP 相同的功能 (如果不是更多的話)。這還可以更好地控制如何處理缺失的翻譯詞 —— 是拋 出異常,還是使用默認語言。

智能皮膚

March Madness 設計實現了一些很有意思的東西。進入主頁並登錄,就會看 到一個歡迎消息。如果點擊 “Welcome” 後面的逗號,就會發現外觀和感覺發 生了變化。替換的皮膚僅僅是另一個 CSS 文件。我擴展了 MadnessWriter;當 選擇替換皮膚時,servlet 基類對這個子類進行實例化,並將實例傳遞給受保護 的服務方法。因此,MadnessWriter 子類不但可以覆蓋默認的樣式表,還可以覆 蓋結構性 HTML 輸出代碼,例如顯示不同的徽標以及在框周圍顯示更復雜的邊框 。servlet 中不需要特殊代碼。

關於縮進的說明

關於這種方式,有一點需要注意:生成的 HTML 沒有縮進,其格式的可讀性 不好。(但是,通過混合 HTML 和 scriptlet 在模板代碼中創建縮進常常導致 混亂。即使不使用 scriptlet,隨著時間的推移,剪切和粘貼也會使代碼支離破 碎。)

只需在輸出中添加一些新行字符,就能夠讓 March Madness 站點生成的 HTML 具有更好的可讀性。但是對於這種方法來說,HTML 的格式是否漂亮並不重 要,因為可以通過檢查 Java 代碼輕松地找到大多數布局 bug,不需要查看生成 的 HTML 源代碼。將元素和結構放在方法中大大提高了簡潔性和可維護性。

結束語

本文鼓勵讀者脫離常用 Web 框架的思維模式,考慮直接用 Java Servlet API 構建 Web 界面。Java Web 開發人員可用的框架和模板系統非常多,這讓人 誤以為這些是必不可少的,但是它們往往非常復雜,很難使用。盡管有的框架非 常適合某種類型的 Web 應用程序,但是也可以考慮用內置的語言特性(比如擴 展和封裝)來實現。正如 Bruce Tate 所說的(參見 參考資料),“以簡單靈 巧的方法來解決問題往往更好”。

Web 框架有適合它們的場景,當項目有專門的 HTML 設計人員來生成和維護 JSP/模板時,JSP/模板是非常合適的。但是對於某些項目來說,純 servlet 的 簡單性是非常有意義的。這種方法提供了控制能力和靈活性,而且不要求將所有 動態內容都放在請求對象中。可以簡便地對純 servlet 進行單元測試。重用 HTML 輸出也很簡單,只需添加或覆蓋一個方法。

所以試試這種方法吧。您可能會對它所帶來的結果感到吃驚。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved