JSP進階技術如何開發動態網站(附完整網上商店)
近年來,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>
<% } %>
</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按照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>
</table>
<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