Java EE 5 开发指南 - 第三章 Java Servlet技术

本文介绍了Java Servlet技术的基本概念、生命周期管理、错误处理、数据共享机制等内容,并通过Duke's Bookstore示例详细展示了如何使用Servlet处理HTTP请求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第三 Java Servlet技术

Web技术得到广泛应用的同时,人们认识到动态地提供Web内容的需求。Applet就是早期发展起来的一项技术,用于在客户端平台向用户提供动态的内容。同时,开发人员也在努力尝试在服务端实现动态内容的提供技术,CGICommon Gateway Interface,通用网关技术)就是当时主要使用的技术之一,但CGI技术有其很大的局限性,如平台依赖性、缺乏可靠的开发调试手段等。为了解决这一情况,Java Servlet应运而生,用于为用户在服务端提供面向用户的动态内容。

 

什么是Servlet

Servlet是使用Java语言编写的类,通过请求/响应的模式为用户提供应用服务。虽然Servlet可以处理任何形式的访问,但通常来说Servlet主要用于处理Web服务请求,在Java中指的是通过HTTP协议访问的Servlet类。

javax.servletjavax.servlet.http包中提供了编写servlet需要的相应接口和类。所有的servlet都要实现servlet接口,这个接口中定义了servlet的生命周期相关的方法。可能继承GenericServlet类来实现一个常规的Servlet。而使用HttpServlet类则可以实现处理HTTP协议相关的专用方法,如doGetdoPost等。

本章着重介绍编写处理HTTP请求的Servlet技术

 

Servlet示例

本章使用Duke’s BookStore示例来展露Serlvet编程技术。下表列出了在示例中通过Servlet技术实现的功能点。每个功能通过一个或多个Servlet来实现。例如,BookDetailsServlet展示了如何处理HTTP GET请求,BookDetailsServletCatalogServlet展示了如何处理响应,CatalogServlet展示了如何处理session信息。

功能

Servlet

进入书店

BookStoreServlet

创建页面台头

BannerServlet

浏览图书目录

CatalogServlet

添加图书到购物车

CatalogServlet,BookDetailsServlet

查看某项图书的详细信息

BookDetailsServlet

显示购物车

ShowCartServlet

从购物车中移除书籍

ShowCartServlet

购买购物车中的书籍

CashierServlet

发送购买感谢信息

ReceiptServlet

书店示例中的数据存放于数据库中,通过database包中的database.BookDBAO类来访问。Database包中还包含了表示书籍的Book类,以及表示购物车及购物项的cart.ShoppingCart类与cart.ShoppingCartItem类。

书店示例的源代码存放于<INSTALL>/javaeetutorial5javaeetutorial5/examples/web/bookstore1/目录下。在示例的部署描述符(web.xml)文件中包含以下配置:

l  应用程序名称

l  过滤器列表

l  过滤器映射关系列表

l  Servlet列表

l  Servlet访问映射列表

l  错误页面列表。

已知问题

Duke书店示例中的数据访问对象可能会返回以下异常:

l  BookNotFoundException: 当未找到匹配书籍数据时返回。可能的原因是未在数据库中加载数据、数据库未启动或已停止。可以通过ant create-tables命令创建数据库及其数据。

l  BooksNotFoundException: 当无法访问书店数据库时返回。可能的原因是数据库未载入数据、数据库服务器未启动或已中止。

l  UnavailableException: Servlet无法正常工作时返回。可能的原因是数据库服务未启动。

因为我们使用了一个错误页面,当发生以上错误时会看到一条消息:

The application is unavailable. Please try later.

如果未指定错误页面,Web容器将动态创建一个默认的信息页面,显示:

A Servlet Exception Has Occurred

并且包含相关的调用堆栈用于帮助调试人员发生问题。如果使用错误页面的话,只能通过服务器日志去发现错误原因。

 

Servlet生命周期

Web容器负责维护Servlet的生命周期。当某个访问请求被映射到servlet上时,容器将会执行以下操作:

l  如果servlet实例不存在,容器将加载servlet类、创建servlet实例、调用init方法对servlet进行初始化(初始化的内容在后面章节中讲述)。

l  调用相关的servlet方法,并传递requestresponse对象。服务方法在后面讲述。

l  如果容器需要移除某个servlet实例,在移除前将会调用servletdestory方法。

 

处理Servlet生命周期事件

可以通过定义监听器来监视和处理servlet生命周期相关的事件。

定义监听器

定义监听器需要实现相应的接口。下表中列出了可以被监听的事件及其相应的接口。当一个监听器方法被调用时,将会传递一个事件对象参数,其中包含了事件相关的信息。例如,在HttpSessionListener接口中将会传递HttpSessionEvent事件对象,其中包含了一个HttpSession对象。

对象

事件

接口与事件类

Web上下文

初始化与停止

Javax.servlet.ServletContextListenerServletContextEvent

 

添加、移除、替换属性(值)

Javax.servlet.ServletContextAttributeListenerServletContextAttributeEvent

会话

创建、失效、激活、钝化、超时

Javax.servlet.http.HttpSessionListener, javax.servlet.http.HttpSessionActivationListenerHttpSessionEvent

 

添加、移除、替换属性(值)

Javax.servlet.http.HttpSessionAttributeListenerHttpSessionBindingEvent

请求

servlet请求被处理

Javax.servlet.ServletRequestListenerServletRequestEvent

 

添加、移除、替换属性(值)

Javax.servlet.ServletRequestAttributeListenerServletRequestAttributeEvent

 

在书店示例中使用listeners.ContextListener类实现数据访问对象的创建与移除。在Web上下文监听器相应的方法中得到ServletContextEvent对象,并会数据访问对象存储在其属性中。

import database.BookDBAO;

import javax.servlet.*;

import util.Counter;

 

import javax.ejb.*;

import javax.persistence.*;

 

public final class ContextListener

  implements ServletContextListener {

  private ServletContext context = null;

 

  @PersistenceUnit

  EntityManagerFactory emf;

 

  public void contextInitialized(ServletContextEvent event) {

    context = event.getServletContext();

    try {

      BookDBAO bookDB = new BookDBAO(emf);

      context.setAttribute("bookDB", bookDB);

    } catch (Exception ex) {

      System.out.println(

        "Couldn't create database: " + ex.getMessage());

    }

    Counter counter = new Counter();

    context.setAttribute("hitCounter", counter);

    counter = new Counter();

    context.setAttribute("orderCounter", counter);

  }

 

  public void contextDestroyed(ServletContextEvent event) {

    context = event.getServletContext();

    BookDBAO bookDB = context.getAttribute("bookDB");

    bookDB.remove();

    context.removeAttribute("bookDB");

    context.removeAttribute("hitCounter");

    context.removeAttribute("orderCounter");

  }

}

 

指定事件监听器

定义好事件监听器后,需要在部署描述符中配置该事件监听器。使用NetBeans配置的方法如下:

l  打开web.xml文件

l  选择顶部的常规标签

l  点击添加

l  找到要添加的监听器类

l  确定保存

处理错误

servlet运行时可能会出现各种各样的错误,当错误发生时,Web容器会生成默认的信息页面显示:A Servlet Exception Has Occurred。但我们为应用程序指定特定的错误处理页面,用其来处理相应的错误并提供示用户。例如在书店示例中使用errorpage.html页面来处理exception.BookNotFoundexception.BooksNotFoundexception.OrderException异常。

 

共享数据

Web开发中,一个Web构件经常会协同其它的组件一同工作来完成其任务。这里有几个方法可以实现这一功能。可以使用某个辅助类对象(如JavaBeans组件)、可以共享对象共享的属性、可以使用相同的数据库、可以调用其它的Web资源。Servlet技术中允许某个Web构件调用其它Web构件,这将会在后面的章节中讲述。

使用范围域对象

Web应用中可以使用四种范围域对象来共享信息,你可以通过这四种范围域的get/set方法访问其属性。下表中列出了这四种范围域:

范围域

可访问的条件

Web上下文

Javax.servlet.ServletContext

在同一Web上下文中的所有Web构件

会话

Javax.servlet.http.HttpSession

属性某同一个会话中的所有请求

请求

Javax.servlet.ServletRequest的所有子类

处理该请求的Web构件

页面

Javax.servlet.jsp.JspContext

创建该页面对象的JSP页面。

下图中显示了Duke书店中使用的范围域

 

共享资源并发访问的控制

在多线种的服务器中,很可能会存在共享资的并发访问情况。除了在范围域中的对象以外,共享资源还包含内存中的数据(如某个实例对象)、外部对象(如文件)、数据库连接、网络连接等。遇到以下情形时可能会引起并发访问:

l  多个Web构件访问存放在Web上下文中共享对象

l  多个Web构件访问存放在会话中的共享对象

l  在多个线程中的某个Web构件访问同一个实例变量。Web容器会显式创建一个线程来处理对某个servlet的请求。如果希望防止这种情况出来,可以在Servlet中实现SingleThreadModel接口,这样在任何时间Web容器都会使用同一样Servlet实例来处理所有对它的请求,或者在处理下一个请求之前先保证先释放前一个servlet实例。但SingleThreadMode接口并不能保证对于外部资源或静态类变量的同步访问。此外,Servlet 2.4中不建议使用SingleThreadMode接口。

如果资源存在并发访问的情况,那么就可能会出现并发冲突。解决的办法是使用同步技术来防止这一情况出现。

如前所述,在书店的示例中我们使用了五个共享属性(属性对象),分别是:bookDBcartcurrencyhitCounter、与orderCounterbookDB将在下一节中讨论。其中cartcurrencycounters允许被多线程并发访问,我们在其访问方法上使用了同步技术,如:

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程序使用Java持久化API访问数据库,通过database包中的database.BookDBAO类来访问。例如,当用户购买书籍时,ReceiptServlet调用BookDBAO.buyBooks方法更新库存量,该方法会在购物车中存放的购物项之间进行迭代循环调用buyBook方法,如:

public void buyBooks(ShoppingCart cart) throws OrderException{

 

  Collection items = cart.getItems();

  Iterator i = items.iterator();

  

  try {

    while (i.hasNext()) {

      ShoppingCartItem sci = (ShoppingCartItem)i.next();

      Book bd = (Book)sci.getItem();

      String id = bd.getBookId();

      int quantity = sci.getQuantity();

      buyBook(id, quantity);

    }

  } catch (Exception ex) {

    throw new OrderException("Commit failed: " +

      ex.getMessage());

  }

}

 

public void buyBook(String bookId, int quantity)

  throws OrderException {

 

  try {

    Book requestedBook = em.find(Book.class, bookId);

    

    if (requestedBook != null) {

      int inventory = requestedBook.getInventory();

      if ((inventory - quantity) >= 0) {

        int newInventory = inventory - quantity;

        requestedBook.setInventory(newInventory);

      } else{

        throw new OrderException("Not enough of "

          + bookId + " in stock to complete order.");

      }

    }

  } catch (Exception ex) {

    throw new OrderException("Couldn't purchase book: "

      + bookId + ex.getMessage());

  }

}

为了保证定单处理时的数据完整性,buyBooks方法的调用被包含在了一个简单的事务之中,代码如下,其中会调用UserTransaction类的begincommit方法进行事务处理,而当错误发生时,会调用rollback方法撤消所有的操作。

 

初始化Servlet

Web容器在实例化某个Servlet对象之后,而在为其传递请求之前,Web容器会调用Servletinit方法,允许Servlet进行定制的初始化、读取持久数据、初始资源、以及执行一些其它方面的一次性操作。你可以重写Servlet接口中的init方法来实现自己的初始化操作。如果初始化操作未能正确地完成,Servlet将会返回UnavailableException异常。

在书店示例中所有访问数据库的servletBookStoreServletCatalogServletBookDetailsServletShowCartServlet)都在初始方法时创建了一个变量指向在上下文监听器中创建的数据访问对象。

public class CatalogServlet extends HttpServlet {

  private BookDBAO bookDB;

  public void init() throws ServletException {

    bookDB = (BookDBAO)getServletContext().

      getAttribute("bookDB");

    if (bookDB == null) throw new

      UnavailableException("Couldn't get database.");

  }

}

 

编写servlet的服务方法

当一个servlet实现自GenericServlet时,将会提供一个service方法,如果是实现自HttpServlet,则会提供一些doXXXX方法(XXXX可能是GetDeleteOptionsPostPutTrace),又或者是其它某种协议特定的接口,也会实现一些特定的方法。本章余下的内容将讲述如何使用这组方法为客户端访问提供服务。

通常,服务方法的作用是获取用户的请求信息、访问外部资源,并生成相应的响应输出信息。

作为HTTP访问的servlet对象,其处理响应输入的顺序是:首先获取响应输出流对象,然后设置响应头信息,接着通过输出流对象输出所有的内容信息。响应头信息必须在输出内容提交前被设置,任何在内容提供交设置的头信息将被Web容器所忽略。下面两节将展示如何通过request对象获取请求信息并产生输出内容。

Request对象获取信息

一个reqeust(请求对象)包含从客户端传来的信息。所有的request对象都实现了ServletRequest接口。其中定义了一些方法用于获取以下信息:

l  参数信息,显式地从客户端传来的数据信息

l  请求属性信息,包含从Servlet容器传来的数据,也可以用于在servlet之间传递数据

l  与请求所使用的协议相关的信息,以及在请求中的客户端与服务端一些相关信息

l  一些本地化相关的信息

例如,在CatalogServlet中使用请求参数传递了客户名称购买的书籍的标识。下列代码片断展示出如何通过getParameter方法获取传递过来的标识:

String bookId = request.getParameter("Add");

if (bookId != null) {

  Book book = bookDB.getBook(bookId);

你也可以从请求对象上获得到数据流对象,并手动转换需要的数据。可以使用getReader方法返回的BufferedReader对象获取字符型数据,也可以使用getInputStream方法得到ServletInputStream对象读取二进制数据。

当某个servlet被请求时,容器将会传递一个HttpServletRequest对象给它,其中包含有请求URLHTTP头信息、查询字符串等。

一个HTTP请求URL包含以下路径:

http://[host]:[port][request path]?[query string]

请求路径(request path)其中包含了如下信息:

l  上下文路径:带有斜线(/)的程序上下文路径

l  Servlet路径:所请求的Servlet别名,使用斜线开头

l  路径信息:其它不属于上下文也不属于Servlet别名的路径信息

如果上下文路径是“/catalog”,下表列出一别名访问的示例:

模式

Servlet别名

/lawn/*

LawnServlet

/*.jsp

JSPServlet

 

请求路径

Servlet路径

路径信息

/catalog/lawn/index.html

/lawn

/index.html

/catalog/help/feedback.jsp

/help/feedback.jsp

Null

 

查询字符串由多个参数/值对组成。可以通过request对象的getParameter方法获取每个参数的值。以下是生成查询字符串的方法:

l  查询字符串可以显示地出现在Web页面中。例如,在HTML页面中可以包含访问CatalogServlet的链接:“<a href="/bookstore1/catalog?Add=101">Add To Cart</a>”。在CatalogServlet中可以通过以下语句查询值:“String bookId = request.getParameter("Add");”。

l  当某个表单使用GET方式提交时,查询字符串可以被追加在URL后面。在书店示例中,CashierServlet生成一个表单,当用户在表单中输入用户名并点提交时,信息会被作为查询字符串提交给ReceiptServlet处理,在ReceiptServlet可以使用getParameter方法获取信息。

 

构造响应内容

一个响应包含了从服务端向客户传递的内容。所有的响应对象都实现ServletResponse接口。通过这个接口中定义的方法可以实现:

l  获取输出流,向用户发送数据。如果发送的是字符数据,可以使用通过getWriter方法获取的PrintWriter对象。如果发送的是二进制数据,可以使用通过getOutputStream方法获取的ServletOutputStream对象。如果输出的即有字符数据也有二进制数据,那么可以手动转换字符数据后使用ServletOutputStream输出。

l  指定返回的内容类型(content type),通过response对象的setContentType(String)指定。这个方法必须在输出内容提交之前调用。已注册的内容类型可以查询IANA组织的网站(http://www.iana.org/assignments/media-types/);

l  设置是否使用输出缓存,使用setBufferSize(int)方法来指定。默认情况下,通过response输出的的内容会立即发送给客户端。使用缓存机制则可以在内容实际输出到客户端之前进行缓存,这样可以允许servlet有更多的时间去处理设置响应状态、设置HTTP头信息、以及转向到别一个Web资源等操作。这个方法必须在任何内容的输出及提交之前调用。

l  设置本地化信息,如使用的字符集等。具体内容参阅第14章。

HTTP响应对象——HttpServletResponse——包含以下一些与HTTP头信息相关的内容:

l  状态码,用于指定某个请求不合法或已被转向

l  Cookies,用于在客户端电脑中存储应用程序相关的一些数据。有时Cookies也用来存储用户的会话信息

在书店例子中,BookDetailsServlet生成了一个HTML页面,用于显示从数据库中查询的某本书的详细信息。在Servlet中首先设置了响应头信息:内容类型与缓存大小。之所以使用缓存是因为在访问数据的过程中可能会出现异常,当异常出现时页面会被重定向到错误页面。使用缓存保证了当错误发生的时候用户不会看到servlet响应内容与错误页面内容混在一起的情况。在设置头信息之后,servlet通过response对象获取了PrintWriter对象。

地填充响应内容的过程中,servlet首先分配了一个向BannerServlet的请求,用于产生一个通用的页面台头。这项功能在后面章节中会有叙述。然后servlet获取请求参数中的书籍唯一标识,并访问数据库获取到该书籍的详细。最后servlet使用HTML标记格式生成书籍的详细内容发送给客户,并调用PrintWriter对象的close方法提交输出。

public class BookDetailsServlet extends HttpServlet {

  ...

   public void doGet (HttpServletRequest request,

      HttpServletResponse response)

      throws ServletException, IOException {

    ...

    // set headers before accessing the Writer

    response.setContentType("text/html");

    response.setBufferSize(8192);

    PrintWriter out = response.getWriter();

 

    // then write the response

    out.println("<html>" +

      "<head><title>+

      messages.getString("TitleBookDescription")

      +</title></head>");

 

    // Get the dispatcher; it gets the banner to the user

    RequestDispatcher dispatcher =

      getServletContext().

      getRequestDispatcher("/banner");

    if (dispatcher != null)

      dispatcher.include(request, response);

 

    // Get the identifier of the book to display

    String bookId = request.getParameter("bookId");

    if (bookId != null) {

      // and the information about the book

      try {

        Book bd =

          bookDB.getBook(bookId);

        ...

        // Print the information obtained

        out.println("<h2>" + bd.getTitle() + "</h2>" +

        ...

      } catch (BookNotFoundException ex) {

        response.resetBuffer();

        throw new ServletException(ex);

      }

    }

    out.println("</body></html>");

    out.close();

  }

}

 

 

过滤请求和响应

过滤器是一种可以改变请求与响应的头信息和内容的Web构件,它不同于普通的Web构件,过滤器不能创建请求,通常过滤器附加在其它的Web资源之上。一个过滤器不应该依赖于某种特定的Web资源,这样的过滤可以选择在一种或多种Web资源上生效。过滤器的任务包含如下:

l  查询请求并作出相应的处理

l  阻塞请求与响应操作

l  修改请求头信息,以此来提供定制化的请求

l  修改响应头信息,以此来提供定制化的响应

l  与外部资源整合

过滤器的应用包括:验证、登录、图像转换、数据压缩、编码、XML转换等。

可以为某个Web资源按顺序设置一个或多个过滤器,也可以不设置。设置是通过部署描述符在部署时设置,在系统运行时将会实例化过滤器对象。

实现过滤器的步骤是:

l  编写过滤器

l  编写定制化的输入/输出操作

l  为需要处理的每个Web资源指定过滤器链

编写过滤器

实现过滤器是通过javax.servlet包中的FilterFilterChainFilterConfig接口。一个过滤器是一个实现了Filter的类,其中最重要的方法是doFilter,它负责传递请求对象、响应对象、以及过滤器链对象。通过doFilter方法可以实现:

l  检查请求头信息

l  如果需要的话可以修改请求对象中的头信息

l  需要的话可以修改响应对象的头信息及响应内容

l  调用过滤器链中的下一环节。如果当前过滤器是过滤器链中的最后一环,那么下一环将是用户所访问的Web资源。过滤器通过调用过滤器链对象的doFilter方法执行下一环节,调用时需要为其传递请求对象和响应对象。当然,过滤器也可以选择不调用下一环节来阻塞当前请求,此时,过滤器需要自己负责相应的响应操作。

l  检查输出头信息

l  抛出异常以指出发生了某项错误

除了doFilter方法外,过滤器还需要实现initdestroy方法。当过滤器被实例化时容器将会调用过滤器的Init方法,并传递初始化参数。如果希望通过处理初始化参数,那么需要在init方法中接收FilterConfig对象。

Duke书店的示例中,使用了HitCounterFilterOrderFilter两个过滤器,用于处理计数器。

doFilter方法中,借助于初始化参数对象,过滤器可以获取到servlet上下文信息,从而得到存储在上下文属性中的计数值。在经过业务处理后,doFilter方法又调用了过滤器链的doFilter方法。以下示例中省略的代码将在后面的章节中讲述:

public final class HitCounterFilter implements Filter {

  private FilterConfig filterConfig = null;

 

  public void init(FilterConfig filterConfig)

    throws ServletException {

    this.filterConfig = filterConfig;

  }

  public void destroy() {

    this.filterConfig = null;

  }

  public void doFilter(ServletRequest request,

    ServletResponse response, FilterChain chain)

    throws IOException, ServletException {

    if (filterConfig == null)

      return;

    StringWriter sw = new StringWriter();

    PrintWriter writer = new PrintWriter(sw);

    Counter counter = (Counter)filterConfig.

      getServletContext().

      getAttribute("hitCounter");

    writer.println();

    writer.println("===============");

    writer.println("The number of hits is: " +

      counter.incCounter());

    writer.println("===============");

    // Log the resulting string

    writer.flush();

    System.out.println(sw.getBuffer().toString());

    ...

    chain.doFilter(request, wrapper);

    ...

  }

}

 

编写定制化的请求和响应

有很多方法可以在过滤器中修改请求与响应信息。例如,过滤器可以在请求对象添加属性,或在响应中加入数据等。在书店的示例中,HitCounterFilter在响应内容中插入了计数值。

修改响应内容的过滤器通常要在内容输出到浏览器前获取响应对象。为此,可以传递一个响应对象的替代品给servlet。使用这个替代品是为了防止servlet在处理完成时关闭响应输出。

替代品是response对象的再次包装,重写了其中的getWritergetOutputStream方法。替代品将会传递给过滤器链的doFilter方法。替代品使用了设计模式中的包装模式,从而取代了默认的请求/响应的输出操作。下面的章节将讲述如何包装一个替代品。

如果要包装一个request方法,可以使用ServletRequestWrapperHttpServletRequestWrapper基类。如果包装的是response方法,则使用ServletResponseWrapperHttpServletResponseWrapper基类。

HitCounterFilter使用CharResponseWrapper类包装了response对象。包装后的对象将会被传递给过滤器链的下一环节,即BookStoreServlet。在BookStoreServlet使用CharResponseWraper对象输出其内容。当chain.doFilter方法返回时,HitCounterFilterPrintWrite中获取到servlet的输出并将其写入到缓存中。过滤器在输出缓存中插入了计数器的内容,然后重设了响应头信息中的内容长度,最后将缓存中的内容输入到客户端。

PrintWriter out = response.getWriter();

CharResponseWrapper wrapper = new CharResponseWrapper(

  (HttpServletResponse)response);

chain.doFilter(request, wrapper);

CharArrayWriter caw = new CharArrayWriter();

caw.write(wrapper.toString().substring(0,

  wrapper.toString().indexOf("</body>")-1));

caw.write("<p>/n<center>" +

  messages.getString("Visitor") + "<font color='red'>" +

  counter.getCounter() + "</font></center>");

caw.write("/n</body></html>");

response.setContentLength(caw.toString().getBytes().length);

out.write(caw.toString());

out.close();

 

public class CharResponseWrapper extends

  HttpServletResponseWrapper {

  private CharArrayWriter output;

  public String toString() {

    return output.toString();

  }

  public CharResponseWrapper(HttpServletResponse response){

    super(response);

    output = new CharArrayWriter();

  }

  public PrintWriter getWriter(){

    return new PrintWriter(output);

  }

}

 

指定过滤器映射

Web容器使用过滤器映射来决定如何调用过滤器。一个过滤器映射可以通过名称来匹配Web构件,也可以通过URL模式来匹配任何的Web资源。过滤器的指定顺序将将决定其执行的顺序。过滤器的映射信息包含在部署描述符中,接下我们看一看如何在NetBeans中使用过滤器。

添加过滤器:

l  打开部署描述符——web.xml文件

l  选择顶部的过滤器标签

l  展开编辑器中的Servlet过滤器节点

l  点击添加过滤器按钮,并输入过滤器名称

l  点击浏览按钮,找到过滤器类

l  输入描述信息后,点击确定保存

 

指定过滤器映射:

l  展开编辑器中的过滤器映射节点

l  在列表中选择过滤器

l  点击添加按钮

l  在添加过滤器映射窗口中,选择一项分发程序类型:

l  REQUEST:仅当请求来自于客户端时

l  FORWARD:仅当请求被转向到构件时

l  INCLUDE:仅当请求是被包含到共它构件中时

l  ERROR:仅当请求是被错误处理机制请求时

l  以上四个选择可以组合使用,如果未指定则默认值为REQUEST

如果希望对程序中的每个请求都遇到到某个日志过滤器,那么可以使用“/*URL模式来匹配。下表中列出了在DUCK书店示例中使用的映射关系,在DUCK书店示例中使用Servlet名称来匹配过滤器,并且每个过滤器链中只包含一个过滤器。

过滤器

对应的Servlet

HitCounterFilter

Filters.HitCounterFilter

BookStoreServlet

OrderFilter

Filters.OrderFilter

ReceiptServlet

 

你可以将一个过滤器映射到一个或多个Web资源上,也可以在一个Web资源上映射多个过滤器。如下图,过滤器F1被映射到S1S2S3三个Servlet上,F2则只被映射到S2上,F3被映射到S1S2上。

 

回想一下,一个过滤器链是由一组传递doFilter方法的过滤器所组成,其中传递的顺序就是在描述符中过滤器映射的定义顺序。

当一个过滤器被映射在S1上面时,Web容器会调用F1doFilter方法。F1上有效的每个过滤器的doFilter方法被都前一个过滤器通过chain.doFilter方法调用。因为S1上有效的过滤器包含F1F3,所以F1的在调用china.doFilter方法时会调用F3过滤器,而F3则调用S1

 

Web资源之间的协作

Web组件调用其它的Web资源的方法有两种:间接地和直接地。间接地方式是指当前返回给客户端的页面中包含了访问其它资源的链接。如在书店示例中,几乎所有的页面都使用了链接指向其它的Web资源,在ShowCartServlet中就包含了查看目录的链接:“/bookstore1/catalog”。

Web组件也可以直接使用其它资源的内容,这也有两种方法,一种是将其它组件的内容包含在当前页面中,另一种方法是将当前的请求转向到另一个组件去处理。

如果希望调用另一个Web资源,首先必须使用getRequestDispacther(“URL”)方法获取RequestDispatcher对象。

可以使用request对象或Web上下文对象获取ReqeustDispatcher对象,这两种方法之间有着微小的差别。getReqeustDispatcher需要一个URL作为参数,如果是通过reqeust对象来获取,那么这个URL可以是一个相对路径,而如果是通过上下文来获取,那么URL必须是一个绝对路径。此外,如果提供的URL有效并且指向的资源允许分发的话,那么getReqeustDispactcher将返回一个RequestDispacther对象,否则返回一个空值,我们在编写程序时需要检查这个空值。

在响应输出中包含其它资源

在某个页面中包含另一个资源的内容是很常见的,比如横幅、版权信息等。包含的方法是调用RequestDispactherinclude方法。

如果被包含的资源是静态内容,那么操作将会是服务端包含。如果包含的是一个Web构件,那么包含的处理方法是:传递reqeust对象给被包含的构件、执行被包含的构件、将其执行结果(即响应内容)包含在当前位置。一个被包含的Web构件有权限使用request对象,但对于response对象的使用只能进行如下的一些操作:

l  可以生成响应输出的内容并可以提交输出

l  不可以设置响应头信息、以及调用任何会响应头信息的方法(如setCookie

在书店示例中的横幅是使用BannerSerlvet生成的统一格式。注意其中的doGetdoPost方法都被实现了,因为BannerSerlvet可能在不同的请求方式下被包含。

public class BannerServlet extends HttpServlet {

  public void doGet (HttpServletRequest request,

    HttpServletResponse response)

    throws ServletException, IOException {

      output(request, response);

  }

  public void doPost (HttpServletRequest request,

    HttpServletResponse response)

    throws ServletException, IOException {

      output(request, response);

}

 

private void output(HttpServletRequest request,

    HttpServletResponse response)

    throws ServletException, IOException {

    PrintWriter out = response.getWriter();

    out.println("<body bgcolor=/"#ffffff/">" +

    "<center>" + "<hr> <br> &nbsp;" + "<h1>" +

    "<font size=/"+3/" color=/"#CC0066/">Duke's </font>" +

    <img src=/"" + request.getContextPath() +

    "/duke.books.gif/">" +

    "<font size=/"+3/" color=/"black/">Bookstore</font>" +

    "</h1>" + "</center>" + "<br> &nbsp; <hr> <br> ");

  }

}

 

每个书店示例中的servlet都使用如下的代码来包含横幅内容:

RequestDispatcher dispatcher =

  getServletContext().getRequestDispatcher("/banner");

if (dispatcher != null)

  dispatcher.include(request, response);

} 

 

转向到另一个Web构件

在某些应用中,可能会希望用某一个Web构件对某个请求做一些预处理,然后将请求发送给另一个构件来生成响应内容。

要想转向到另一个Web构件,可以使用RequestDispatcher对象的forward方法。当某个请求被转向后,请求的URL变被置为转向的页面地址,而原地址及相关信息将被保存在reqeust的属性中(属性名称为:javax.servlet.forward.[request_uri | context-path | servlet_path | path_info | query_string])。

public class Dispatcher extends HttpServlet {

  public void doGet(HttpServletRequest request,

    HttpServletResponse response) {

    RequestDispatcher dispatcher = request.

      getRequestDispatcher("/template.jsp");

    if (dispatcher != null)

      dispatcher.forward(request, response);

  }

  public void doPost(HttpServletRequest request,

  ...

}

 

转向方法的目的是赋予另一个构件以响应用户请求的能力,如果你在转向前已经访问了ServletOutputStreamPrintWriter对象,那么转向将会引发IllegalStateException异常。

 

访问Web上下文

Web上下文是实现了ServletContext接口的对象。可以通过getServletContext方法获取。使用上下文中提供的方法可以获取到:

l  初始化参数

l  与上下文相关的资源

l  属性值(对象)

l  日志功能

书店示例中的com.sun.bookstore1.filters.HitCounterFilterOrderFilter两个过滤器中使用了Web上下文对象,每个过滤器都保存了计数器属性。回忆一下在并发访问一节的内容,我们讲述了计数器使用了同步技术来防止并发访问冲突。过滤使用上下文对象的getAttribute方法获得计数器,并将增量后的值存回属性中。

public final class HitCounterFilter implements Filter {

  private FilterConfig filterConfig = null;

  public void doFilter(ServletRequest request,

    ServletResponse response, FilterChain chain)

    throws IOException, ServletException {

    ...

    StringWriter sw = new StringWriter();

    PrintWriter writer = new PrintWriter(sw);

    ServletContext context = filterConfig.

      getServletContext();

    Counter counter = (Counter)context.

      getAttribute("hitCounter");

    ...

    writer.println("The number of hits is: " +

      counter.incCounter());

    ...

    System.out.println(sw.getBuffer().toString());

    ...

  }

}

 

维护客户端状态

有些程序需要将某个用户的一连串请求联合起来以区别与其它的请求。例如,在书店示例中需要保存每个用户的购物车状态。因为HTTP是无状态的,所以基于Web的程序使用session来管理用户状态。在Javaservlet技术中提供了访问sessionAPI,并且允许有多种方式来实现session

访问session

SessionJava中被表示为一个HttpSesion对象,可以通过reqeust对象的getSession方法得到,该方法将返回一个与当前reqeust相关的session对象,如果当前reqeust对象上不存在session,那么将创建一个。

session中关联对象

可以将某个对象使用相应的名称保存在session的属性中。这样的属性可以被同一个Web上下文中的属于同一个会话的所有请求获取到。

Duke书店中保存一个购物车对象在session中,这样一来,购物车就可以在多个请求之间共享,并允许相关的几个servlet对购物车进行维护,如:CatalogServlet负责向购物车中添加购物项,ShowCartServlet负责显示、删除项、以及清空购物车,而CashierServlet则处理购物车的总金额。

public class CashierServlet extends HttpServlet {

  public void doGet (HttpServletRequest request,

    HttpServletResponse response)

    throws ServletException, IOException {

 

    // Get the user's session and shopping cart

    HttpSession session = request.getSession();

    ShoppingCart cart =

      (ShoppingCart)session.

        getAttribute("cart");

    ...

    // Determine the total price of the user's books

    double total = cart.getTotal();

 

session属性监听器

回忆一下我们在管理servlet生命周其一节中所说的内容,我们可以为servlet的各个生命周期添加事件监听器,同样,我们也可以为session的属性添加事件监听器:

l  实现javax.servlet.http.HttpSessionBindingListener接口,可以实现监听对象在session中的添加与移除。

l  session被钝化时,session的数据将被暂存于外部存储器中。当该session需要再次使用时将被重新激活。Session的钝化与激活可以使用实现了javax.servlet.http.HttpSessionActivationListener接口的监听器来监控。

 

会话管理

因为没有任何方法可以知道客户端不再需要session了,所以每个session都包含了一个过期时间,以此可以使得session中的资源能够得到回收。过期时间可以使用session[get|set]MaxInactiveInterval方法进行设置,也可以在部署描述符中设置。使用NetBeans设置session过期时间的方法是:

l  打开web.xml文件

l  选择顶部的常规标签

l  在会话超时栏输入一个整数,这个整数表示当某个session不再活动后,经过多少分钟系统将回收其资源,并删除该session对象

为了确保session不会过期,你需要不断地周期性地在服务端访问session对象,这样才能使session始终处于活动状态。

在某些特殊的情况下,当客户端的操作已经结束时,我们可以使用session对象的invalidate方法明确地删除某个session及其保存的数据。书店示例中的ReceiptServlet就是客户端访问的最后一个页面,所以其中使用了该方法使得session失效。

public class ReceiptServlet extends HttpServlet {

  public void doPost(HttpServletRequest request,

          HttpServletResponse response)

          throws ServletException, IOException {

    // Get the user's session and shopping cart

    HttpSession session = request.getSession();

    // Payment received -- invalidate the session

    session.invalidate();

    ...

 

会话跟踪

Web容器有几种方法将不同用户的会话区分开来,这些方法都是在客户端服务端交互时传递一个唯一的标识。这个标识可以被保存在浏览器的cookie上,或者被附加在用户请求的URL中。

如果你的程序中使用到了session,而浏览器却关闭了cookie的话,那么你需要使用response对象的encodeURL方法对要输出的链接进行编码,以确保用户所看到的链接得到重写。encodeURL方法会检查浏览器是否打开了cookie,如果打开了cookie则不改动链接地址,如果浏览器关闭了cookie,那么用户的会话ID会被追加到URL后面。

在书店示例中的ShowCartServlet使用也encodeURL方法:

out.println("<p> &nbsp; <p><strong><a href=/"" +

  response.encodeURL(request.getContextPath() +

    "/bookcatalog") +

    "/">" + messages.getString("ContinueShopping") +

    "</a> &nbsp; &nbsp; &nbsp;" +

    "<a href=/"" +

  response.encodeURL(request.getContextPath() +

    "/bookcashier") +

    "/">" + messages.getString("Checkout") +

    "</a> &nbsp; &nbsp; &nbsp;" +

    "<a href=/"" +

  response.encodeURL(request.getContextPath() +

    "/bookshowcart?Clear=clear") +

    "/">" + messages.getString("ClearCart") +

    "</a></strong>");

 

如果浏览器关闭了cookie,那么被编码的URL类似于:

http://localhost:8080/bookstore1/cashier?jsessionid=c0o7fszeb1

如果cookie可用,那么URL是:

http://localhost:8080/bookstore1/cashier

 

Servlet的结束

Web容器决定要从服务中移除某个servlet时(可能的原因是当容器要回收内存,或准备关闭服务器),容器将会调用servletdestroy方法。在destroy方法中,你可以释放servlet占用的资源、保存持久性数据。下面的destroy方法就释放了在init方法创建的数据库连接资源:

public void destroy() {

  bookDB = null;

}

 

servlet被移除时,容器在调用destroy方法之前,将确保服务的所有方法都被执行完成,或者超过了等待期限。如果在servlet上有某个方法需要很长时间来执行,这个时间超过了容器等待servlet的期限的话,那么有可能在执行destroy方法时servlet仍然处于运行状态,你必须确认是不是有某个线程仍在处理客户请求,本章节余下的内容将讲述如何处理如下操作:

l  跟踪当前有多少个线程在执行服务端的方法

l  执行彻底的清除操作,通知执行线程即将关闭并等待其完成

l  在长时间操作中周期性检查是否需要关闭,如果需要则停止、整理并返回

 

跟踪服务请求

servlet中包含一个用于当前执行计数的域,并使用同步技术对该域进行增量/减量/返回值。

public class ShutdownExample extends HttpServlet {

  private int serviceCounter = 0;

  ...

  // Access methods for serviceCounter

  protected synchronized void enteringServiceMethod() {

    serviceCounter++;

  }

  protected synchronized void leavingServiceMethod() {

    serviceCounter--;

  }

  protected synchronized int numServices() {

    return serviceCounter;

  }

}

当服务方法开始执行时对访问计数进行增量,当方法返回之前对访问计数进行减量。以下是在HttpServlet子类中重写的service方法,其中要调用super.service来确认方法的功能可以被执行。

protected void service(HttpServletRequest req,

          HttpServletResponse resp)

          throws ServletException,IOException {

  enteringServiceMethod();

  try {

    super.service(req, resp);

  } finally {

    leavingServiceMethod();

  }

}

 

通知方法即将关闭

为了实现了一个彻底的关闭,在destroy方法执行资源释放之前,需要确保所有的服务方法已执行完毕。实现这项功能的第一步是实现访问计数器,然后还需要能够通知长时间执行的方法是时间关闭了。为了实现这项通知,需要再添加一个域,并实现相应的访问方法,如下:

public class ShutdownExample extends HttpServlet {

  private boolean shuttingDown;

  ...

  //Access methods for shuttingDown

  protected synchronized void setShuttingDown(boolean flag) {

    shuttingDown = flag;

  }

  protected synchronized boolean isShuttingDown() {

    return shuttingDown;

  }

}

下面是在destroy方法是使用添加的域通知即将关闭的方式:

public void destroy() {

  /* Check to see whether there are still service methods /*

  /* running, and if there are, tell them to stop. */

  if (numServices() > 0) {

    setShuttingDown(true);

  }

 

  /* Wait for the service methods to stop. */

  while(numServices() > 0) {

    try {

      Thread.sleep(interval);

    } catch (InterruptedException e) {

    }

  }

} 

 

创建优雅的长时间操作方法

实现彻底关闭的最后一个步骤是编写优雅的长时间操作方法,方法中在运行长时间操作的时间可以检测关闭信号量,如果已被通知关闭的话,可以中止当前的工作。

public void doPost(...) {

  ...

  for(i = 0; ((i < lotsOfStuffToDo) &&

    !isShuttingDown()); i++) {

    try {

      partOfLongRunningOperation(i);

    } catch (InterruptedException e) {

      ...

    }

  }

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值