文章目錄
Java Servlet技術
Stephanie Bodoff
當Web剛開始被用來傳送服務時,服務提供者就已經意識到了動態內容的需要。Applet是為了實現這個目標的一種最早的嘗試,它主要關注使用用戶端平台來交付動態使用者體驗。與此同時,開發人員也在研究如何使用伺服器平台實現這個目標。開始的時候,公用網關介面(Common Gateway Interface ,CGI)指令碼是產生動態內容的主要技術。雖然使用得非常廣泛,但CGI指令碼技術有很多的缺陷,這包括平台相關性和缺乏可擴充性。為了避免這些局限性,Java Servlet技術因應而生,它能夠以一種可移植的方法來提供動態、面向使用者的內容。
什麼是
Servlet?
一個servlet就是Java程式設計語言中的一個類,它被用來擴充伺服器的效能,伺服器上駐留著可以通過“要求-回應”編程模型來訪問的應用程式。雖然servlet可以對任何類型的請求產生響應,但通常只用來擴充Web伺服器的應用程式。Java Servlet技術為這些應用程式定義了一個特定於HTTP的 servlet類。
javax.servlet和javax.servlet.http包為編寫servlet提供了介面和類。所有的servlet都必須實現Servlet介面,該介面定義了生命週期方法。
當實現一個通用的服務時,您可以使用或擴充由Java Servlet API提供的GenericServlet類。HttpServlet類提供了一些方法,諸如doGet和doPost,以用於處理特定於HTTP的服務。
本章主要講述如何編寫對HTTP請求產生響應的servlet。這裡假設您已經瞭解了一些HTTP協議的基礎知識。如果對這些協議不熟悉的話,您可以從HTTP概述中對HTTP協議有一個初步的瞭解。
Servlet
樣本
本章使用Duke's Bookstore應用程式來說明與servlet編程相關的任務。表14-1列出瞭解決每個書店功能的servlet。每個編程任務用一個或多個servlet來說明。例如,BookDetailsServlet說明如何處理HTTP GET請求,BookDetailsServlet和CatalogServlet顯示如何構建響應,而CatalogServlet 則說明如何跟蹤會話資訊。
表14-1 Duke's Bookstore Servlet例子
功能 |
Servlet |
進入書店 |
BookStoreServlet |
建立書店標識 |
BannerServlet |
瀏覽書店的目錄 |
CatalogServlet |
將書放入購物車 |
CatalogServlet, BookDetailsServlet |
擷取關於特定的某本書的一些詳細資料 |
BookDetailsServlet |
顯示購物車 |
ShowCartServlet |
從購物車中移除一本或多本書 |
ShowCartServlet |
購買購物車中的書 |
CashierServlet |
獲得對購買的確認 |
ReceiptServlet |
這些書店應用程式的資料儲存在資料庫中,並通過協助類database.BookDB進行儲存。database包也包括BookDetails類,一個BookDetails類用來代表一種書。購物車和購物車項用cart.ShoppingCart 和cart.ShoppingCartItem來分別表示。
書店應用程式的原始碼放在<JWSDP_HOME>/docs/tutorial/examples/web/bookstore1目錄中,這個目錄是在對指南包進行解壓縮時建立的。
要構建、安裝和運行這個執行個體,需完成以下步驟:
1. 在終端視窗中,轉到<JWSDP_HOME>/docs/tutorial/examples/web/bookstore1。
2. 運行build。 build目標將產生任何必要的編碼並且將檔案直接拷貝到<JWSDP_HOME>/docs/tutorial/examples/web/bookstore1/build 。
3. 確認Tomcat已經開始執行。
4. 運行ant install 。install 目標式通知Tomcat 已經有了新的上下文 。
5. 啟動PointBase公司的資料庫伺服器,並且在沒完全準備好的情況下仍然指向資料庫(見從Web應用中訪問資料庫)。.
6. 開啟書店的URL http://localhost:8080/bookstore1/enter以運行該應用程式。
要部署該應用程式,要完成以下步驟:
1. 運行ant package。這個包任務是建立一個WAR 檔案,該檔案包含WEB-INF/classes中的應用程式類 和META-INF中的context.xml 檔案。
2. 確認Tomcat已經開始執行。
3. 運行ant deploy。Deploy目標將WAR拷貝到Tomcat,並且通知已經有了新的上下文。
故障排除
一般的問題和其解決方案列舉了Web用戶端為什麼會失敗的一些原因。另外,Duke書店返回了以下異常:
· BookNotFoundException——如果一本書不能在書店的資料庫中找到,則返回該異常。如果使用者沒有運行ant create-book-db來載入書店資料庫中的資料、或沒有運行資料庫伺服器、或資料庫已經崩潰,這些都將產生該異常。
· BooksNotFoundException——如果書店的資料不能被擷取,則返回該異常。如果使用者沒有運行ant create-book-db來載入書店資料庫中的資料、或沒有運行資料庫伺服器、或資料庫已經崩潰,這些都將產生該異常。
· UnavailableException——如果servlet不能擷取到用來表示書店的Web內容屬性,則返回該異常。如果您沒有拷貝指向(PointBase)的用戶端庫<PB_HOME>/lib/pbclient45.jar to <JWSDP_HOME>/common/lib、或者如果指向的(PiontBase)伺服器沒有運行、或使用者沒有定義Tomcat中用來引用指向資料庫(PointBase)的資料來源,這都將產生該異常。
因為指定了一個錯誤頁,使用者將看到這樣的一個訊息The application is unavailable. Please try later. 如果指定了一個正確頁,Web容器將產生一個包含A Servlet Exception Has Occurred訊息的預設頁和一個用來協助診斷異常產生原因的棧。如果使用errorpage.html,使用者可以瞭解Web容器決定異常產生原因的日誌。Web日誌位於<JWSDP_HOME>/logs目錄中,由jwsdp_log.<date>.txt來命名。
Servlet
的生命週期
一個servlet的生命週期由部署servlet的容器來控制。當一個請求映射到一個servlet時,該容器執行下列步驟。
1. 如果一個servlet的執行個體並不存在,Web容器
a. 載入servlet類。
b. 建立一個servlet類的執行個體。
c. 調用init初始化servlet執行個體。該初始化過程將在初始化servlet中講述。
2. 調用service方法,傳遞一個請求和響應對象。服務方法將在編寫服務方法中講述。
如果該容器要移除這個servlet,可調用servlet的destroy方法來結束該servlet。結束過程將在結束Serlvet中討論。
處理Servlet生命週期事件
在servlet的生命週期中,使用者可以通過定義監聽器對象對事件進行檢測和產生反應。當生命週期事件發生時,調用該對象的方法。要使用這些監聽器對象,使用者必須定義監聽器類,並且指定相應的監聽器類。
定義監聽器類
您可以將監聽器類定義為一個listener介面的實現。Servlet生命週期事件列出了可以檢測的事件和相應的必須實現的介面。當調用一個監聽器方法時,需向該方法傳遞一個包含事件適當資訊的事件。例如,向HttpSessionListener介面中的方法傳遞的是一個HttpSessionEvent事件,這個事件包含了一個HttpSession。
表14-2Servle生命週期事件
對象 |
事件 |
監聽器介面和事件類別 |
Web上下文 (見訪問Web上下文) |
初始化和銷毀 |
javax.servlet. ServletContextListener 和 ServletContextEvent |
屬性的添加、刪除或替代 |
javax.servlet. ServletContextAttributeListener 和 ServletContextAttributeEvent |
會話 (見維護客戶給狀態) |
建立、失效和逾時 |
javax.servlet.http. HttpSessionListener 和 HttpSessionEvent |
屬性的添加、刪除或替代 |
javax.servlet.http. HttpSessionAttributeListener 和 HttpSessionBindingEvent |
listeners.ContextListener類負責建立和移除在Duke書店應用程式中使用的資料庫助手和計數器對象。方法從ServletContextEvent中擷取Web內容物件,進而儲存(和移除)作為servlet內容屬性的對象。
import database.BookDB;
import javax.servlet.*;
import util.Counter;
public final class ContextListener
implements ServletContextListener {
private ServletContext context = null;
public void contextInitialized(ServletContextEvent event) {
context = event.getServletContext();
try {
BookDB bookDB = new BookDB();
context.setAttribute("bookDB", bookDB);
} catch (Exception ex) {
System.out.println(
"Couldn't create database: "
+ ex.getMessage());
}
Counter counter = new Counter();
context.setAttribute("hitCounter", counter);
context.log("Created hitCounter"
+ counter.getCounter());
counter = new Counter();
context.setAttribute("orderCounter", counter);
context.log("Created orderCounter"
+ counter.getCounter());
}
public void contextDestroyed(ServletContextEvent event) {
context = event.getServletContext();
BookDB bookDB = context.getAttribute(
"bookDB");
bookDB.remove();
context.removeAttribute("bookDB");
context.removeAttribute("hitCounter");
context.removeAttribute("orderCounter");
}
}
指定事件監聽器類
為了指定一個事件監聽器類,使用者要為Web應用部署描述符添加一個listener元素。以下就是Duke書店應用程式的一個listener元素。
<listener>
<listener-class>listeners.ContextListener</listener-class>
</listener>
處理錯誤
當servlet執行時,可能產生許多異常。而當異常產生時,Web容器將產生一個包含A Servlet Exception Has Occurred訊息的預設頁。但是,使用者也可返回一個容器,該容器應包含為給定異常指定的錯誤頁。為了指定這樣一個頁,使用者要為Web應用添加部署描述符添加一個error-page元素。這些元素將Duke書店應用程式返回的異常映射到errorpage.html:
<error-page>
<exception-type>
exception.BookNotFoundException
</exception-type>
<location>/errorpage.html</location>
</error-page>
<error-page>
<exception-type>
exception.BooksNotFoundException
</exception-type>
<location>/errorpage.html</location>
</error-page>
<error-page>
<exception-type>exception.OrderException</exception-type>
<location>/errorpage.html</location> </error-page>
共用資訊
像大多數對象一樣,Web組件通常與其他一些對象協同工作,以完成任務。要做到這一點,可以有多種方法。Web組件可以使用私人的helper(助手)對象(例如,JavaBeans組件),也可以共用那些有公用範圍屬性的對象,它們可以使用資料庫,還可以調用其他的Web資源。Java Servlet技術機制允許一個Web組件調用其他的Web資源,這在調用其他Web資源中有描述。
使用範圍對象
幾個協作的Web組件通過一些對象來共用資訊,這些對象是作為四個範圍對象的屬性來維護的。這些屬性可以通過表示域的類的[get|set]Attribute方法訪問。表14-3列出了這個範圍對象。
表14-3 範圍對象
範圍對象 |
類 |
哪些組件可以對其進行訪問 |
網路內容 |
javax.servlet. ServletContext |
Web上下文中的Web組件。見訪問Web上下文 |
會話 |
javax.servlet. http.HttpSession |
處理屬於會話的請求的Web組件。見維護用戶端狀態。 |
請求 |
javax.servlet. ServletRequest 的子類型 |
處理請求的Web組件。 |
頁 |
javax.servlet. jsp.PageContext |
建立對象的JSP頁。見隱式對象。 |
圖14-1顯示了Duke書店應用程式維護的範圍屬性。
圖14-1 Duke書店範圍屬性
控制對共用資源的並發訪問
在多線程的伺服器中,可能出現對共用資源的並發訪問。除了範圍對象屬性外,共用資源還包括儲存空間中的資料(如執行個體和類變數)、外部對象(如檔案)、資料庫連接和網路連接。並發訪問可出現在多個情況下。
· 多個Web組件訪問儲存在Web上下文中的對象。t
· 多個Web組件訪問儲存在會話中的對象。
· 一個Web組件中的多個線程訪問執行個體變數。一個Web容器一般為每個請求建立一個線程來處理。如果使用者確認一個servlet執行個體每次只處理一個請求,servlet就能實現SingleThreadModel 介面。如果servlet實現了這個介面,使用者就能確保servlet的服務方法中不可能有兩個線程並發執行。Web容器可通過同步訪問一個servlet的單獨執行個體、或者通過維護一個Web組件池為每個執行個體調用一個新的請求來實現。這個介面並不能防止Web組件訪問共用資源(如靜態類變數、外部對象)導致的同步問題
當資源可以並發訪問時,使用資源也就可以用不一致的方式。為了防止這樣的情況發生,使用者必須使用在Java指導中的線程單元中描述的同步機制來控制訪問。
在以前的部分中,我們說明了被多個servlet共用的5個範圍屬性: bookDB, cart, currency, hitCounter和orderCounter。bookDB屬性將在下一節中討論。cart, currency和counter可以被多線程的servlet設定和讀。使用同步方法來控制訪問以防止這些對象的使用不一致。例如,下面是一個util.Counter類:
public class Counter {
private int counter;
public Counter() {
counter = 0;
}
public synchronized int getCounter() {
return counter; }
public synchronized int setCounter(int c) {
counter = c; return counter;
}
public synchronized int incCounter() {
return(++counter);
}
}
訪問資料庫
在Web組件之間共用,並且在對一個Web應用被調用的間隙內維持的資料通常是由一個資料庫來維護的。Web組件使用JDBC 2.0 API來訪問關聯式資料庫。書店應用程式的資料由資料庫來維護,並通過助手類database.BookDB訪問。例如,當使用者購買書後,ReceiptServlet調用BookDB.buyBooks方法來更新書的清單。buyBooks方法為每本包含在購物車中的書調用buyBook。為了確保命令被完全執行,buyBook的調用程式將被封裝在一個單獨的JDBC交易處理中。通過[get|release]Connection方法可以使共用資料庫串連同步使用。
public void buyBooks(ShoppingCart cart) throws OrderException {
Collection items = cart.getItems();
Iterator i = items.iterator();
try {
getConnection();
con.setAutoCommit(false);
while (i.hasNext()) {
ShoppingCartItem sci = (ShoppingCartItem)i.next();
BookDetails bd = (BookDetails)sci.getItem();
String id = bd.getBookId();
int quantity = sci.getQuantity();
buyBook(id, quantity);
}
con.commit();
con.setAutoCommit(true);
releaseConnection();
} catch (Exception ex) {
try {
con.rollback();
releaseConnection();
throw new OrderException("Transaction failed: " +
ex.getMessage());
} catch (SQLException sqx) {
releaseConnection();
throw new OrderException("Rollback failed: " +
sqx.getMessage());
}
}
}
初始化
Servlet
在Web容器載入和執行個體化servlet類之後、servlet執行個體傳遞來自用戶端的請求之前,Web容器對servlet進行初始化。使用者可以自訂這個初始化過程,以允許servlet讀持久的配置資料、初始化資源,並且忽略Servlet介面的init方法以執行任何其它的一次性的活動。servlet必須使用UnavailableException來完成初始化過程。
所有的訪問書店資料庫的servlet(BookStoreServlet, CatalogServlet, BookDetailsServlet, 和 ShowCartServlet)在它們的init方法中初始化一個變數,指向用Web上下文監聽器建立的資料庫小幫手物件。
public class CatalogServlet extends HttpServlet {
private BookDB bookDB;
public void init() throws ServletException {
bookDB = (BookDB)getServletContext().
getAttribute("bookDB");
if (bookDB == null) throw new
UnavailableException("Couldn't get database.");
}
}
編寫服務方法
servlet提供的服務實現在GenericServlet的service方法、HttpServlet的doMethod方法(在該方法中,Method可以帶Get、Delete、Options、Post、Put、Trace的值),或者是任何其他的由實現了Servlet介面的類定義的協議指定(protocol-specific)的方法中。在這一章剩下的部分中,服務方法這個術語將用於在一個向用戶端提供服務的servlet類中定義的任何方法。
服務方法的一般模式是從請求中提取資訊、訪問外部資源並且基於這些資訊填充響應。
對於HTTPservlet來說,填充響應的正確過程是:首先填充回應標頭,然後從響應中擷取一個輸出資料流,最後編寫輸出資料流的所有主體內容。回應標頭必須在PrintWriter或ServletOutputStream被擷取到之前設定好,因為HTTP協議希望獲得主體內容前的所有頭的資訊。下兩節將描述如何從請求中獲得資訊和產生響應。
從請求中獲得資訊
一個請求包含用戶端和servlet之間傳遞的資料。所有請求都實現了ServletRequest介面,該介面為訪問一下的資訊定義了方法:
· 參數,通常用來在用戶端和servlet之間傳送資訊
· 對象屬性(Object-valued attribute),通常用來在servlet容器與servlet之間或在協作的servlet之間傳遞資訊
· 有關協議的資訊,用來在請求、用戶端和涉及到該請求中的伺服器之間的通訊。
· 有關地區化的資訊。
例如,在CatalogServlet中,顧客希望購買的書的標識符作為參數包含在請求中。下面的這段代碼說明了如何使用getParameter方法提取標識符。
String bookId = request.getParameter("Add");
if (bookId != null) {
BookDetails book = bookDB.getBookDetails(bookId);
使用者也可以從請求中擷取一個輸入資料流,並對資料進行手工解析。要讀字元資料,可以使用由請求的getReader方法返回的 BufferedReader對象來完成。而要讀位元據,可以使用getInputStream 返回的ServletInputStream。
HTTP serlvet通過HTTP請求對象傳遞,HttpServletRequest包含了請URL、HTTP頭、查詢字串等等。
一個HTTP請求URL包含以下幾部分:
http://[host]:[port][request path]?[query string]
請求路徑由以下元素組成:
· 上下文路徑:向前的斜線/和servlet的Web應用的上下文根的拼接。
· servlet路徑:與啟用該請求的組件別名相應的路徑部分,由向前的斜線/開始。
· 路徑資訊:請求路徑的部分,不是上下文路徑或者servlet路徑的部分。
如果上下文路徑是/catalog和表14-4列舉出的別名,表14-5給出了一些執行個體,說明如何分解URL。
表14-4別名
模式 |
Servlet |
/lawn/* |
LawnServlet |
/*.jsp |
JSPServlet |
表 14-5 請求路徑元素
請求路徑 |
Servlet 路徑 |
路徑資訊 |
/catalog/lawn/index.html |
/lawn |
/index.html |
/catalog/help/feedback.jsp |
/help/feedback.jsp |
null |
查詢字串由參數和值的集合組成。每個參數都是從請求中用getParameter方法擷取得到的。這裡有兩種方法產生查詢字串:
· 一個查詢字串能在Web頁中明確地顯示出來。例如,一個HTML頁由CatalogServlet產生,該HTML頁包含了<a href="/bookstore1/catalog?Add=101">Add To Cart</a>。 CatalogServlet 將命名為Add的參數提出,如下:
String bookId = request.getParameter("Add");
· 當一個表單與一個GET HTTP方法一起被提交時, 在URL上附加一個查詢字串。在Duke書店應用程式中,首先CashierServlet產生了一個表單,然後在表單中輸入一個使用者名稱,該表單附加在映射到ReceiptServlet的URL上,最後ReceiptServlet使用getParameter方法提取使用者名稱。