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的服務。
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元素。
listeners.ContextListener
處理錯誤
當servlet執行時,可能產生許多異常。而當異常產生時,Web容器將產生一個包含A Servlet Exception Has Occurred訊息的預設頁。但是,用戶也可返回一個容器,該容器應包含為給定異常指定的錯誤頁。為了指定這樣一個頁,用戶要為Web套用添加部署描述符添加一個error-page元素。這些元素將Duke書店應用程式返回的異常映射到errorpage.html:
exception.BookNotFoundException /errorpage.html exception.BooksNotFoundException/errorpage.html exception.OrderException /errorpage.html
共享信息
像大多數對象一樣,Web組件通常與其他一些對象協同工作,以完成任務。要做到這一點,可以有多種方法。Web組件可以使用私有的helper(助手)對象(例如,JavaBeans組件),也可以共享那些有公共作用域屬性的對象,它們可以使用資料庫,還可以調用其他的Web資源。Java Servlet技術機制允許一個Web組件調用其他的Web資源,這在調用其他Web資源中有描述。
使用作用域對象
幾個協作的Web組件通過一些對象來共享信息,這些對象是作為四個作用域對象的屬性來維護的。這些屬性可以通過表示域的類的[get|set]Attribute方法訪問。表14-3列出了這個作用域對象。
表14-3 作用域對象
作用域對象 | 類 | 哪些組件可以對其進行訪問 |
Web 上下文 | 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上下文中的對象。
· 多個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的do Method方法(在該方法中, 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頁包含了Add To Cart。 CatalogServlet 將命名為Add的參數提出,如下:
String bookId = request.getParameter("Add");· 當一個表單與一個GET HTTP方法一起被提交時, 在URL上附加一個查詢字元串。在Duke書店應用程式中,首先CashierServlet產生了一個表單,然後在表單中輸入一個用戶名,該表單附加在映射到ReceiptServlet的URL上,最後ReceiptServlet使用getParameter方法提取用戶名。