近年來,Jsp技術現在已經成為一種卓越的動態網站開發技術。Java開發者出于各種理由喜愛使用jsp。有人喜愛其“一次開發,處處使用”的性能,另外的人覺得jsp使java成為一種易學的服務器端scripting語言。但是,jsp最大的長處在它將頁面的表現和頁面的商業邏輯分開了。本章中,我們將深入地討論如何使用jsp模式2體系結構來開發網站。這一模式可以被看作是通用模式瀏覽控制模式(popular Model-View-Controller,MVC)模式的服務器端實現。
Servlets有何缺陷?
當jsp成為開發動態網站的主要技術時,可能有人會問為何在jsp技術中我們不強調servlets。Servlets的應用是沒有問題的。它們非常適于服務器端的處理和編程,并且它們會長期駐留在他們現在的位置。但是,從結構上說,我們可以將jsp看作是servlet的一個高層的抽象實現,特別是在servlet 2.2 API下。但是,你仍然不能無拘束地使用servlet;它們并不適合每一個人。例如,頁面設計者可以方便地使用html或者xml工具開發jsp頁面,但servlet卻更適合于后端開發者使用,他們的工具是ide——一個需要更多編程訓練的開發領域。當發布servlet時,每個開發者必須小心地確定在頁面表現和頁面邏輯之間沒有緊密的關聯出現。你可以使用第三方html包裝工具,如htmlkona來混合html和servlet代碼。即使如此,這點靈活性還不足以讓你自由地改變風格本身。例如,你希望從html改變到dhtml,則包裝本身需要被小心地測試,以確保新的格式可以正確使用。在最壞的情況下,包裝不可用,你就需要應變馬來表現動態內容。所以,需要一種新的解決方案。你將會看到,一種方案就是混合jsp和servlet的使用。
不同的方式
早期的jsp標準給出了兩種使用jsp的方式。這些方式,都可以歸納為jsp模式1和jsp模式2,主要的差別在于處理大量請求的位置不同。在模式1中(圖1),jsp頁面獨自響應請求并將處理結果返回客戶。這里仍然有表現和內容的分離,因為所有的數據依靠bean來處理。盡管模式1 可以很好的滿足小型應用的需要,但卻不能滿足大型應用的要求。大量使用模式1,常常會導致頁面被嵌入大量的script或者java代碼。特別是當需要處理的商業邏輯很復雜時,情況會變得嚴重。也許對于java程序員來說,這不算大的問題。但如果開發者是前端界面設計人員——在大型項目中,這非常常見,——則代碼的開發和維護將出現困難。在任何項目中,這樣的模式多少總會導致定義不清的響應和項目管理的困難。
在圖2中顯示的模式2 結構,是一種面向動態內容的實現,結合了servlet 和jsp技術。它利用了兩種技術原有的優點,采用jsp來表現頁面,采用servlets來完成大量的處理。這里,servlet扮演一個控制者的角色,并負責響應客戶請求。接著,servlet創建jsp需要的bean和對象,再根據用戶的行為,決定將那個jsp頁面發送給用戶。特別要注意,jsp頁面中沒有任何商業處理邏輯;它只是簡單地檢索servlet先前創建的bean 或者對象,再將動態內容插入預定義的模版。從開發的觀點看,這一模式具有更清晰的頁面表現,清楚的開發者角色劃分,可以充分地利用開發小組中的界面設計人員。事實上,越是復雜的項目,采用模式2 的好處就越突出。
為了清楚地了解模式2 的開發過程,我們舉一個網上音樂商店的例子。
我們創建一個叫”音樂無國界”的銷售音樂制品的商店。“音樂無國界”在線商店的主界面,是一個叫“音樂無國界”的頁面(代碼1)。你會看到,這個頁面完全著眼于用戶界面,與處理邏輯無關。另外,注意另外一個jsp頁面,Cart.jsp(在代碼2中),用<jsp:include page="Cart.jsp" flush="true" />.嵌入Eshop.jsp中。
Listing 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>
Listing 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> <% } %>
<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按照MVC的模式1處理基于SESSION的購物車的表現。請看Cart.jsp開始處的代碼:
<% Vector buylist = (Vector) session.getValue("shopping.shoppingcart"); if (buylist != null && (buylist.size() > 0)) { %>
本質上,這段代碼從SESSION中取出“購物車”。如果“購物車”為空或者沒有被創建,它就什么也不顯示。所以,在用戶第一次訪問應用時,其界面如圖:
如果“購物車”不為空,用戶選擇的商品從車中取出,依次顯示在頁面上:
<% for (int index=0; index < buylist.size(); index++) { CD anOrder = (CD) buylist.elementAt(index); %>
一旦生成一個物品的說明,就使用JSP按照事先設定的模板將其插入靜態HTML頁面。下圖顯示了用戶選購一些物品后的界面:
需要注意的一個重要的地方是所有關于Eshop.jsp,Cart.jsp的處理有一個控制SERVLET,ShoppingServlet.java,代碼在源程序3中:
Listing 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。控制SERVLET決定進一步的行動,并處理增加的商品。接著,控制SERVLET實例化一個新的BEAN CD代表選定的商品,并在返回SESSION前更新購物車對象。
Listing 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中具有附加的智能,如果一個物品被重復選擇,不會增加新的記錄,而是在以前的記錄上更新計數。控制SERVLET也響應Cart.jsp中的行為,如修改數量,刪除商品,還有結帳。如果結帳,控制通過下述語句轉向Checkout.jsp頁面(源程序5):
String url="/jsp/shopping/Checkout.jsp"; ServletContext sc = getServletContext(); RequestDispatcher rd = sc.getRequestDispatcher(url); rd.forward(req,res);
Listing 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>
<p> <a href="/examples/jsp/shopping/EShop.jsp">Shop some more!</a> </center> </body> </html>
結帳頁面簡單地從SESSION中取出購物車,然后顯示每個物品和總金額。這里的關鍵是要結束SESSION,因此在頁面中有一個session.invalidate()調用。這一處理有兩個原因。首先,如果不結束SESSION,用戶的購物車不會被初始化,如果用戶要繼續購買,車中會保留他已經支付過的商品。另外,如果用戶不結帳就離開了,則SESSION會繼續占用有效的資源直到過期。過期時間一般是30分鐘,在一個大的站點上,這樣的情況會很快導致資源耗盡。當然,這是我們不愿看到的。
注意,所有的資源分配在這個例子中是基于SESSION的。所以,你必須確保控制SERVLET不被用戶訪問,即使是意外的訪問也不允許。這可以在控制檢查到一個非法訪問時用一個簡單的重定向錯誤頁面來處理。見源代碼6。
Listing 6: error.html
<html> <body> <h1> Sorry, there was an unrecoverable error!
Please try <a href="/examples/jsp/shopping/EShop.jsp">again</a>. </h1> </body> </html>
小結
本章的討論顯示,使用模式2,JSP和SERVLET可以在功能上最大限度的分開。正確地使用模式2,導致一個中心化的控制SERVLET,以及只完成顯示的JSP頁面。另一方面,模式2的實現很復雜。因此,在簡單的應用中,可以考慮使用模式1。
|