标题：java servlet
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 和 
HttpSessionBindingEventlisteners.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.ServletContextWeb上下文中的Web组件。见访问Web上下文会话javax.servlet.http.HttpSession处理属于会话的请求的Web组件。见维护客户端状态。 请求 javax.servlet.ServletRequest
的子类型处理请求的Web组件。 页javax.servlet.jsp.PageContext创建对象的JSP页。见隐式对象。 图14－1显示了Duke书店应用程序维护的作用域属性。
图14-1Duke书店作用域属性
  
控制对共享资源的并发访问在多线程的服务器中，可能出现对共享资源的并发访问。除了作用域对象属性外，共享资源还包括存储器中的数据（如实例和类变量）、外部对象（如文件）、数据库连接和网络连接。并发访问可出现在多个情况下。
· 多个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页包含了Add To Cart。 CatalogServlet 将命名为Add的参数提出，如下：
String bookId = request.getParameter("Add");· 当一个表单与一个GET HTTP方法一起被提交时， 在URL上附加一个查询字符串。在Duke书店应用程序中，首先CashierServlet产生了一个表单，然后在表单中输入一个用户名，该表单附加在映射到ReceiptServlet的URL上，最后ReceiptServlet使用getParameter方法提取用户名。