js|架構|設計 摘要:通過開發一個熟悉的基於web的購物店,你將學到如何工具化mvc設計模式並且真正地在使用jsp的時候分離內容和表現。Govind Seshadri 會向你展示這是多麼的容易(2000字(原文字數))。
儘管相對拋開最近的相關介紹而言,jsp技術正在很好地以自己的方式成為卓越的建立提供動態web內容的應用程式的java技術。java開發人員因為許多不同的理由喜愛jsp。一些人喜歡它給互動式web頁面帶來了“一次編寫,到處運行”的變化這個事實;另一些人欣賞它易學易用並且協助人們把java作為伺服器端指令碼使用。但是都公認一件事——使用jsp最大的好處是能夠有效地分離內容與表現。在這篇文章裡,我來提供一個深入的看法,關於如何使用jsp模式2架構獲得最佳的內容與表現的分離。這個模式也可以被看作流行的mvc設計模式在服務端的實現。請注意在開始之前你應該熟悉jsp和servlet編程,因為我不會在這篇文章中討論文法問題。
那麼,servlet有什麼問題?
既然jsp用來提供動態web內容並且對於從表現層中分離內容很不錯,一些人也許想知道為什麼servlet要從jsp中脫離出來與它並列。servlet的功用沒有問題。它對於服務端處理幹得很好,而且,由於它重要的已安裝基礎,就適合這個。實際上,從結構上說,你可以把jsp看作實現為servlet 2.1 api的擴充的servlet進階抽象。仍然不應該不加區別地使用servlet;它可能不會適用於每一個人。舉個例子來說,儘管頁面設計者能夠很容易地使用常規html或者xml工具編寫jsp頁面,而servlet通常更適合後台開發人員,他們通常使用某種IDE——一個通常需要高層次的編程專門知識的過程。當發布servlet時,即使開發人員也必須留意和確認在內容和表現之間沒有緊耦合。通常,你可以通過加入第三方的html封裝包比如htmlkona來做這個。即使這樣做了,儘管帶來了一些簡單的對於螢幕變化的伸縮性,仍然不能為你防止免受表現格式自身的變化的影響。例如,如果你的表現形式從html轉變到dhtml,你將仍然需要確認你的封裝包是否相容這種新格式。在最壞的情況下,如果封裝包不能用了,你可能最終會在動態內容內部寫入程式碼表現形式。那麼,解決辦法是什嗎?就像你你將要看到的,一個辦法將會同時使用jsp和servlet來建立應用系統。
差異哲學
早期的jsp規範主張兩種使用jsp技術建立應用的哲學思路。這兩種思路,用術語來說就是jsp模式1和模式2,本質上的區別在於大部分請求的處理髮生的位置。在模式1架構中,如圖1所示,jsp頁面獨立地負責處理請求和發送反饋給用戶端。這裡仍然有內容和表現的分離,因為所有的資料訪問是使用bean完成的。儘管模式1架構應該很適合簡單應用,但是對於複雜的實現是不可取的。這種結構的任意使用通常會導致大量的指令碼和java代碼嵌入到jsp頁面中,特別是在有大量的請求需要處理的情況下。儘管這可能對java開發人員來說不是一個大問題,但是卻無疑是一個問題,如果你的jsp頁面是由設計師建立和維護的話——在大項目中通常如此。最終,這個問題甚至會導致角色定義和責任分配的混亂,引起本可以輕鬆避免的專案管理的麻煩。
圖1:jsp模式1結構
模式2架構如圖2所示,是一個為動態內容服務的混合方案,因為它同時使用了servlet和jsp。它利用了兩種技術的優勢,使用jsp產生表現層而servlet負責執行敏感任務。在這裡,servlet扮演控制器的角色,負責請求處理和產生jsp要使用的bean和對象,以及根據客戶的動作決定下一步轉寄到哪一個jsp頁面。特別要注意的是jsp頁面內部並沒有處理邏輯;它只是簡單地負責取得可能是servelet事先建立的對象和bean,並為在了靜態模版中插入從servlet釋放出動態內容。我的觀點是,這個辦法一般會形成最乾淨徹底的表現與內容的分離,使得你的Team Dev裡的開發人員和頁面設計師的角色與責任能夠清晰。實際上,你的應用越複雜,使用模式2帶來的好處就越多。
圖2:jsp模式2結構
為了弄清模式2背後的概念,我們來參觀一個細化的具體實現:一個叫做音樂無界的線上音樂市集樣品。
瞭解音樂無界
主視圖,或者說表現層,對於我們的音樂無界由jsp頁面EShop.jsp產生(見清單1)。你會注意到這個頁面幾乎僅僅處理這個應用的主要使用者介面,而且沒有做任何處理工作——一個最佳的jsp指令碼。也注意一下另一個jsp頁面,cart.jsp(見清單2),通過指令<jsp:include page="Cart.jsp" flush="true" />包含在EShop.jsp之內。
清單1 EShop.jsp |
<%@ page session="true" %> <html> <head> <title>Music Without Borders</title> </head> <body bgcolor="#33CCFF"> <font face="Times New Roman,Times" size="+3"> Music Without Borders </font> <hr><p> <center> <form name="shoppingForm" action="/examples/servlet/ShoppingServlet" method="POST"> <b>CD:</b> <select name=CD> <option>Yuan | The Guo Brothers | China | $14.95</option> <option>Drums of Passion | Babatunde Olatunji | Nigeria | $16.95</option> <option>Kaira | Tounami Diabate| Mali | $16.95</option> <option>The Lion is Loose | Eliades Ochoa | Cuba | $13.95</option> <option>Dance the Devil Away | Outback | Australia | $14.95</option> <option>Record of Changes | Samulnori | Korea | $12.95</option> <option>Djelika | Tounami Diabate | Mali | $14.95</option> <option>Rapture | Nusrat Fateh Ali Khan | Pakistan | $12.95</option> <option>Cesaria Evora | Cesaria Evora | Cape Verde | $16.95</option> <option>Ibuki | Kodo | Japan | $13.95</option> </select> <b>Quantity: </b><input type="text" name="qty" SIZE="3" value=1> <input type="hidden" name="action" value="ADD"> <input type="submit" name="Submit" value="Add to Cart"> </form> </center> <p> <jsp:include page="Cart.jsp" flush="true" /> </body> </html> |
清單2 Cart.jsp |
<%@ page session="true" import="java.util.*, shopping.CD" %> <% Vector buylist = (Vector) session.getValue("shopping.shoppingcart"); if (buylist != null && (buylist.size() > 0)) { %> <center> <table border="0" cellpadding="0" width="100%" bgcolor="#FFFFFF"> <tr> <td><b>ALBUM</b></td> <td><b>ARTIST</b></td> <td><b>COUNTRY</b></td> <td><b>PRICE</b></td> <td><b>QUANTITY</b></td> <td></td> </tr> <% for (int index=0; index < buylist.size();index++) { CD anOrder = (CD) buylist.elementAt(index); %> <tr> <td><b><%= anOrder.getAlbum() %></b></td> <td><b><%= anOrder.getArtist() %></b></td> <td><b><%= anOrder.getCountry() %></b></td> <td><b><%= anOrder.getPrice() %></b></td> <td><b><%= anOrder.getQuantity() %></b></td> <td> <form name="deleteForm" action="/examples/servlet/ShoppingServlet" method="POST"> <input type="submit" value="Delete"> <input type="hidden" name= "delindex" value='<%= index %>'> <input type="hidden" name="action" value="DELETE"> </form> </td> </tr> <% } %> </table> <p> <form name="checkoutForm" action="/examples/servlet/ShoppingServlet" method="POST"> <input type="hidden" name="action" value="CHECKOUT"> <input type="submit" name="Checkout" value="Checkout"> </form> </center> <% } %> |
這裡,Cart.jsp處理基於session的購物車的表現形式,它指定了我們的MVC結構中的模型。觀察Cart.jsp開頭這一段指令碼:
<%
Vector buylist = (Vector) session.getValue("shopping.shoppingcart");
if (buylist != null && (buylist.size() > 0)) {
%>
基本上,這段指令碼從session中提出了購物車。如果購物車為空白或者還未建立,它不會顯示任何東西;因此,當使用者第一次訪問的時候,他見到的頁面如圖3。
圖3:音樂無界,主視圖
如果購物車不是空的,那麼已選中的物品會一次一個地從購物車中被提出,像下面的指令碼示範的那樣:
<%
for (int index=0; index < buylist.size(); index++) {
CD anOrder = (CD) buylist.elementAt(index);
%>
一旦描述物品的變數已建立,它們就簡單地被JSP運算式插入到靜態HTML模版中去。圖4顯示了使用者已經放了一些東西到購物車裡去是的情況。
圖4:音樂無界,購物車視圖
這裡要注意的重要的一件事是對所有動作的處理既不發生在EShop.jsp也不在Cart.jsp裡,而是由控制器servlet,ShoppingServlet.java處理,見清單3:
清單3 ShoppingServlet.java |
import java.util.*; import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import shopping.CD; public class ShoppingServlet extends HttpServlet { public void init(ServletConfig conf) throws ServletException { super.init(conf); } public void doPost (HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { HttpSession session = req.getSession(false); if (session == null) { res.sendRedirect("http://localhost:8080/error.html"); } Vector buylist= (Vector)session.getValue("shopping.shoppingcart"); String action = req.getParameter("action"); if (!action.equals("CHECKOUT")) { if (action.equals("DELETE")) { String del = req.getParameter("delindex"); int d = (new Integer(del)).intValue(); buylist.removeElementAt(d); } else if (action.equals("ADD")) { //any previous buys of same cd? boolean match=false; CD aCD = getCD(req); if (buylist==null) { //add first cd to the cart buylist = new Vector(); //first order buylist.addElement(aCD); } else { // not first buy for (int i=0; i< buylist.size(); i++) { CD cd = (CD) buylist.elementAt(i); if (cd.getAlbum().equals(aCD.getAlbum())) { cd.setQuantity(cd.getQuantity()+aCD.getQuantity()); buylist.setElementAt(cd,i); match = true; } //end of if name matches } // end of for if (!match) buylist.addElement(aCD); } } session.putValue("shopping.shoppingcart", buylist); String url="/jsp/shopping/EShop.jsp"; ServletContext sc = getServletContext(); RequestDispatcher rd = sc.getRequestDispatcher(url); rd.forward(req, res); } else if (action.equals("CHECKOUT")) { float total =0; for (int i=0; i< buylist.size();i++) { CD anOrder = (CD) buylist.elementAt(i); float price= anOrder.getPrice(); int qty = anOrder.getQuantity(); total += (price * qty); } total += 0.005; String amount = new Float(total).toString(); int n = amount.indexOf('.'); amount = amount.substring(0,n+3); req.setAttribute("amount",amount); String url="/jsp/shopping/Checkout.jsp"; ServletContext sc = getServletContext(); RequestDispatcher rd = sc.getRequestDispatcher(url); rd.forward(req,res); } } private CD getCD(HttpServletRequest req) { //imagine if all this was in a scriptlet...ugly, eh? String myCd = req.getParameter("CD"); String qty = req.getParameter("qty"); StringTokenizer t = new StringTokenizer(myCd,"|"); String album= t.nextToken(); String artist = t.nextToken(); String country = t.nextToken(); String price = t.nextToken(); price = price.replace('$',' ').trim(); CD cd = new CD(); cd.setAlbum(album); cd.setArtist(artist); cd.setCountry(country); cd.setPrice((new Float(price)).floatValue()); cd.setQuantity((new Integer(qty)).intValue()); return cd; } } |
每次使用者在EShop.jsp中添加一件物品,請求都被發送到這個控制器servlet。它依次決定合適的動作,然後處理相應要添加的物品的請求參數。它就執行個體化一個新的CD bean(見清單4)代表這個選擇的物品,接著在把這個bean放進session之前處理購物車的更新。
清單4 CD.java |
package shopping; public class CD { String album; String artist; String country; float price; int quantity; public CD() { album=""; artist=""; country=""; price=0; quantity=0; } public void setAlbum(String title) { album=title; } public String getAlbum() { return album; } public void setArtist(String group) { artist=group; } public String getArtist() { return artist; } public void setCountry(String cty) { country=cty; } public String getCountry() { return country; } public void setPrice(float p) { price=p; } public float getPrice() { return price; } public void setQuantity(int q) { quantity=q; } public int getQuantity() { return quantity; } } |
注意我們在這個servlet中還包括了額外的智能,因此它能夠知道如果選擇了一張已在購物車中的CD,那麼應該簡單地增加session中CD bean的計數。它也處理從Cart.jsp中觸發的動作,比如使用者從購物車中刪除物品,或是繼續去收銀台結帳。注意控制器總是對哪個資源應該被調用來對特定的動作產生回饋有完全的控制權。例如,對購物車狀態的改變,像增加和刪除,會引起控制器將請求處理後轉寄給EShop.jsp頁面。這樣引起該頁面依照已更新的購物車依次重新顯示主視圖。如果使用者決定結帳,則請求被處理後轉寄給Checkout.jsp(見清單5),通過後面的請求分配器,象下面顯示的這樣:
String url="/jsp/shopping/Checkout.jsp";
ServletContext sc = getServletContext();
RequestDispatcher rd = sc.getRequestDispatcher(url);
rd.forward(req,res);
清單5 Checkout.jsp |
<%@ page session="true" import="java.util.*, shopping.CD" %> <html> <head> <title>Music Without Borders Checkout</title> </head> <body bgcolor="#33CCFF"> <font face="Times New Roman,Times" size=+3> Music Without Borders Checkout </font> <hr><p> <center> <table border="0" cellpadding="0" width="100%" bgcolor="#FFFFFF"> <tr> <td><b>ALBUM</b></td> <td><b>ARTIST</b></td> <td><b>COUNTRY</b></td> <td><b>PRICE</b></td> <td><b>QUANTITY</b></td> <td></td> </tr> <% Vector buylist = (Vector) session.getValue("shopping.shoppingcart"); String amount = (String) request.getAttribute("amount"); for (int i=0; i < buylist.size();i++) { CD anOrder = (CD) buylist.elementAt(i); %> <tr> <td><b><%= anOrder.getAlbum() %></b></td> <td><b><%= anOrder.getArtist() %></b></td> <td><b><%= anOrder.getCountry() %></b></td> <td><b><%= anOrder.getPrice() %></b></td> <td><b><%= anOrder.getQuantity() %></b></td> </tr> <% } session.invalidate(); %> <tr> <td> </td> <td> </td> <td><b>TOTAL</b></td> <td><b>$<%= amount %></b></td> <td> </td> </tr> </table> <p> <a href="/examples/jsp/shopping/EShop.jsp">Shop some more!</a> </center> </body> </html> |
Checkout.jsp僅僅從session中提出購物車並為此請求提取出總金額,然後顯示選中的物品和他們的總價格。圖5顯示了結算時的使用者視圖。一旦使用者去結帳,刪除session對象是同樣重要的。這個由頁面末端的session.invalidate()調用來完成。有兩個理由必須這樣做。第一,如果沒有使session無效,使用者的購物車不會重新初始化;如果使用者結帳後試圖開始新一輪的採購,她的購物車會繼續儲存著已經付過錢的物品。第二,如果使用者結帳後僅僅是離開了網站,這個session對象不會被垃圾收集機制回收而是繼續佔用寶貴的系統資源直到租約到期。因為預設的session租約時間是大約三十分鐘,在一個大容量系統上這將會很快導致系統記憶體耗盡。當然,我們都知道對一個耗盡了系統記憶體的應用程式會發生什麼。
圖5:結賬視圖
注意這個應用所有的資源都是session相關的,因為這裡的模式儲存在session裡。因此,你必須確保使用者不會因為某些原因甚至由於錯誤直接存取控制器。你可以在控制器檢測到缺少有效session的時候讓用戶端自動轉向到一個錯誤頁面(見列表6),來避免這種情況的發生。
列表6 error.html |
<html> <body> <h1> Sorry, there was an unrecoverable error! <br> Please try <a href="/examples/jsp/shopping/EShop.jsp">again</a>. </h1> </body> </html> |
部署音樂無界
我假定你正在使用來自sun的最新版本的JavaServer Web Development Kit (JSWDK)來運行這個例子。如果不是,參看資源小節去看看到哪裡取得它。假設伺服器安裝在\jswdk-1.0.1,這是Microsoft Windows系統下的預設路徑,可以象下面這樣部署音樂無界應用:
Create shopping directory under \jswdk-1.0.1\examples\jsp
Copy EShop.jsp to \jswdk-1.0.1\examples\jsp\shopping
Copy Cart.jsp to \jswdk-1.0.1\examples\jsp\shopping
Copy Checkout.jsp to \jswdk-1.0.1\examples\jsp\shopping
Compile the .java files by typing javac *.java
Copy ShoppingServlet.class to \jswdk-1.0.1\webpages\Web-Inf\servlets
Create shopping directory under \jswdk-1.0.1\examples\Web-Inf\jsp\beans
Copy CD.class to \jswdk-1.0.1\examples\Web-Inf\jsp\beans\shopping
Copy error.html to \jswdk-1.0.1\webpages
Once your server has been started, you should be able to access the application using http://localhost:8080/examples/jsp/shopping/EShop.jsp as the URL
只要你的伺服器啟動,你應該可以使用URL http://localhost:8080/examples/jsp/shopping/EShop.jsp來訪問這個應用程式。
利用jsp和servlet
著這個例子裡,我們從細節上檢查了控制的層次和模式2架構提供的靈活性。實際上,我們已經看到了servlet和jsp頁面最好的特性是如何開發出最大化的內容與表現的剝離。只要正確地應用,模式2架構會將所有的處理邏輯集中到控制器servlet手裡,而jsp頁面只負責視圖或者說表現層的工作。然而,使用模式2結構的阻力在於它的複雜性。因此,對於簡單應用使用模式1也是可以接受的。