那么,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釋放出動態內容。我的觀點是,這個辦法一般會形成最干凈徹底的表現與內容的分離,使得你的開發團隊里的開發者和頁面設計師的角色與責任能夠清晰。實際上,你的應用越復雜,使用模式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; } } |
清單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也是可以接受的。
新聞熱點
疑難解答