|
第10章 Java Servlet技术 网络一开始提供服务,服务供应者们就意识到动态内容的需要。Applets——是最早的用于动态内容的java类,主要通过使用客户平台传送动态用户经验。与此同时,开发者们也研究使用服务方平台以达到这个目的。最初,公共网关接口(CGI)脚本是产生动态内容的主要的技术。 尽管被广泛的应用,CGI脚本有着许多缺点,如平台相关性,缺乏可升级性。为克服这些缺点,Java servlet技术作为一种以简单的方式提供动态的,面向用户的技术诞生了。 什么是Servlet? Servlet是java语言类,用来拓展通过请求响应模式的服务端的能力。尽管servlets可以响应任何类型的响应,它们通常用于拓展基于Web的应用程序。在这中应用程序中,Java servlet技术定义了特定的HTTP servlet类。 包javax.servlet和javax.servlet.http提供了写servlets的接口和类。所有的servlets都必须实现servlet借口,它定义了生命周期方法。 当实现一个一般的服务时,你能使用或拓展GenericServlet类,该类提供了Java Servlet API,HttpServlet类提供了一些方法,如doGet及doPost,用来处理特定的http服务。 本章主要集中在编写产生响应HTTP请求的servlets。这里假设你以了解一些HTTP协议的知识;如果你不熟悉该协议,你可以参考附录A。 有关Servlets的例子 这一章使用了Duke’s Bookstore程序来演示编写servlets的任务。下面的表格列出了所有的处理书店功能的servlets。每一任务都通过servlet展示。例如,BookDetailsServlet展示了如何处理HTTP GET请求,BookDetailsServlet及CatalogServlet显示了如何构造响应,CatalogServlet显示了如何跟踪会话信息。
图书程序的数据保存在数据库中,数据库包也包含类BookDetails。购物车及购物车中的具体的项目由类Cart.ShopingCart及Cart.ShoppingCartItem来分别表示。 书店应用程序的源代码位于j2eetutorial/examples/src/web/bookstore1目录中,可以通过一下步骤来编译、部署、运行例子: 1、到j2eetutorial/examples目录编译例子并运行ant bookstore1 2、启动j2ee服务器 3、打开部署工具 4、启动Cloudscape数据库服务器 5、通过运行ant create-web-db来装载数据到数据库 6、创建名为Bookstore1App的J2EE应用程序 a.选择FileàNewàApplication. b.在文件选择中选择j2eetutorial/examples/src/web/bookstore1. c.在文件名域中,输入Bookstore1App. d.单击New Application e.单击OK. 7、创建WAR并将BannerServlet Web组件及所有的Duke’s书店内容加入到Bookstore1App程序中 a.选择FileàNewàWeb组件 b.在应用程序单选按钮中单击创建新的WAR文件并从复选框中选择Bookstore1App. c.单击Edit以增加文件内容 d.在编辑对话框中浏览到j2eetutorial/examples/build/web/bookstore1.选择BeanerServlet.class,CatalogServlet.class,ShowCartServlet.class,CashierServlet.class及ReceiptServlet.class.点击Add。增加errorpage.html,duke.books.gif,增加cart,database,exception,filters,listeners,messages和工具包,单击OK. e.单击Next f.选择servlet单选按钮 g.单击Next h.选择BannerServlet i.连续单击Next两次 j.在复选框别名栏中,单击Add,接着在别名域中输入/banner k.单击Finish. 8、增加下表列出的网络组件,对于每个Servlet,单击Add to Existing WAR File单选按钮并从复选框中选择Bookstore1WAR。
9、Cloudscape数据库增加资源引用。 a.选择Bookstore1WAR. b.选择资源引用标签 c.单击Add d.从类型栏里选择javax.sql.DataSource e.在代码名称域中输入jdbc/BookDB f.在JNDI域中输入jdbc/Cloudscape. 10、增加监听类listeners.ContextListener a.选择事件监听标签 b.单击Add c.在事件监听类栏的下拉框中选择listeners.ContextListener类 11、增加一个错误页面 a.选择文件引用标签 b.在错误映射栏中,单击Add c.在错误/异常域中输入exception.BookNotFoundException d.在资源调用域中输入/errorpage.html 12、增加filters filters.HitCounterFilter及filters.OrderFilter a.选择Filter Mapping b.单击Edit Filter List c.单击Add d.从Filter类栏中选择filters.HitCounterFilter e.单击Add f.从Filter类栏中选择filters.OrderFilter.部署工具会自动的在现实名称栏中输入OrderFilter. g.单击OK h.单击Add i.从Filter名称栏中选择HitCounterFilter j.从目标类型栏中选择servlet k.选择BookStoreServlet. l.对于OrderFilter也重复上面的步骤,目标类型是Servlet,目标是ReciptServlet 13、输入上下文的根 a.选择Bookstore1App b.选择网络上下文标签 c.输入bookstore1 14、部署应用程序 a.选择工具à部署 b.单击完成 15、打开bookstore的URL http://<host>:8000/booktore1/enter 处理错误 这部分介绍常见错误及解决方法(尤指网络客户运行错误),并列出了一些为什么网络客户连接失败的原因。此外,Duke’s Bookstore返回下面的异常: 1、BookNotFoundException:如果图书不在书店数据库中将产生这个异常, 2、BooksNotFoundException:如果书店数据不能返回将产生这个异常。 3、UnavailableException:如果一个servlet不能返回代表书店的网络上下文属性信息。 由于我们已经指定了一个错误页面,你将会看到这样的信息:The application is unavailable.Please try later。如果没有指定错误页面,网络容器将产生一个默认的页面产生这样的信息:A ServletException Has Occurred and a stack trace that can help diagnose the cause of the exception.如果使用errorpage.html,你不得不堪网络容器的日志来判断产生异常的原因。网络日志在下面的目录中:$J2EE_HOME/logs/<host>/web,并以catalina.<date>.log命名。 Servlet的生命周期 部署了的Servlet的生命周期是由容器控制的。当一个请求映射到相应的servlet时,容器产生下面的步骤: 1.如果servlet的实例不存在,容器会 a.载入servlet类 b.创建一个servlet实例 c.通过调用init方法初始化servlet实例 2.调用service方法, 如果容器需要删除servlet,可以通过调用destroy方法。处理servlet生命周期事件 在servlet生命周期中,你可以通过定义一个监听对象来监听它的生命周期,当生命周期事件发生时,该监听对象就会被调用。要使用这些监听对象,你必须定义监听类并指定监听类。 定义监听类 定义一个监听类并实现监听接口。下面的表列出了能够被监听并必须实现的相应的接口:
当一个监听方法被调用时,它恰当的传递一个包含信息的事件。例如,在HttpSessionListener接口中的方法传递一个包含HttpSession的HttpSessionEvent。 在Duke’s Bookstore应用程序中,类listeners.ContextListener创建及删除一个数据库帮助和计数对象。这个方法重新从ServletContextEvent返回网络上下文对象,接着吧该对象做为servlet的上下文属性保存起来。 Import database.BookDB; Import javax.servlet.*; Import util.Counter; Public final class ContextListener implements ServletContextListener { private ServletContext context=null; 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()); Context.setAttribute(“orderCounter”,counter); Context.log(“Created orderCounter”+counter.getCounter()); } public void contextDestroyed(ServletContextEvent event) { context=event.getServletContext(); BookDB bookDB=context.getAttribute(“bookDB”); Context.removeAttribute(“bookDB”); Context.removeAttribute(“hitCounter”); Context.removeAttribute(“orderCounter”); } } 指定事件监听类 处理错误 当Servlet执行时任何数量的异常都有可能发生。网络容器将会产生一个默认的页面包含这样的信息:A Servlet Exception Has Occurred when an exception occurs,你也可以指定容器返回一个特定的异常到指定的错误页面中。 共享信息 网络组件像其它的对象一样,通常同其它的对象协调工作。它们通过这样一些方法实现。可以使用私有帮助对象,可以共享作为公共域属性的对象,可以使用数据库,也可以调用其它资源。Java Servlet技术机制是的网络组件可以调用其它网络资源。 使用域对象 共享信息通过做为4类域对象属性的对象来协调网络组件,这些属性通过类的get/set属性方法来展示域。下表列出了域对象:
下图展示了由Duke’s Bookstore应用维护的域属性:
控制共享资源的并发入口 在一个多线程的服务器中,共享资源并发使用是可能的。除了域对象属性外,共享资源包括进驻内存的数据,例如实例或类变量,以及外部对象如文件,数据库连接,网络连接。并发可在下面的一些情况下发生: ☆ 多数网络组件访问对象存储在网络上下文中 ☆ 多数网络组件访问对象存储在会话中 ☆ 网络组件中的多线程访问实例变量。网络容器将产生一个线程来处理每个请求。如果你想让servlet在某个时刻只处理一个请求,servlet可以实现接口SingleThreadModel。如果一个servlet实现了该接口,可以保证没有两个线程回并发调用servlet的service方法。网络容器可以通过同步访问单个servlet实例以实现这个保证,或者通过维护网络组件实例的池并分发新的请求到一个闲着的servlet。这个接口不阻止由于网络组件访问共享资源(例如静态类变量或外部对象)产生的同步问题。 当资源能并发访问的时候,它们可以以一种不一致的方式来使用。为了防止这种情况,你必须控制通过并发技术实现的访问。 在前面的部分里,我们展示了被servlet共享的五种域属性:bookDB,Cart,currency,hitCounter及orderCounter. bookDB将在下一部分讨论。Cart,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); } } 访问数据库 数据在各网络组件间共享,J2EE应用程序的数据调用通常通过数据库来维护。网络组件通过JDBC2.0API来访问关系型数据库。书店应用程序的数据存放在数据库里并通过帮助类database.BookDB来访问。例如,当顾客下单时ReceiptServlet调用BookDB.buyBooks方法来更新书的清单。这个方法对购物车中的每本书都可调用buyBook。为确保订单完整的处理,这个调用被封在一个单一的JDBC事务中。共享数据库的连接通过get/release方法实现同步。 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 在网络容器载入并实例化Servlet之后,被客户请求之前,网络容器将初始化servlet。你可以通过改写init方法来自己定制这个过程来允许servlet读取持久配置数据,初始化资源,及处理其它一次性的事情。一个servlet不能完成其初始化过程将抛出UnavailableException. 所有的访问书店数据库的servlet都在init方法中初始化由网络上下文产生的指向数据库帮助类对象的变量: public class CatalogServlet extends HttpServlet { private BookDB bookDB; public void init() throws ServletException { boolDB=(BookDB)getServletContext().getAttribute(“bookDB”); if(bookDB==null) throw new UnavailavleException(“Couldn’t get database.”); } } 编写Service方法 servlet提供的service方法实现了GenericServlet类的service方法。HttpServlet的doMethod方法,或其它类定义的实现了Servlet接口的特定协议方法。在本章的下面几部分,service方法将在servlet中为客户提供服务。 一般的service方法模式通过request获取信息,访问外部资源,然后根据信息做出相应的响应。 对于httpServlet,正确的响应过程是首先填写响应头,接着从响应返回输出流,最后向输出流写上程序体内容。响应头必须在PrintWriter和ServletOutputStream之前设置,因为HTTP协议希望在程序体内容之前接受所有头信息。接着的两部分描述了如何从request获取信息并产生响应。 通过request取得信息 一个请求包含客户端与servlet之间的数据传递。所有的请求都实现了ServletRequest接口。该接口定义了访问一下信息的方法: ☆ 参数,典型的实用它来在客户端与Servlet之间传递信息。 ☆ 对象属性,主要用来在servlet容器中的servlet间的信息传递。 ☆ 使用通信协议的信息,以及请求调用中的客户与服务。 ☆ 本地相关信息。 例如,在CatalogServlet中,客户想购买的书籍的标志作为请求参数。在下面的代码中显示了如何使用方法getParameter来取得标志: String bookID=request.getParameter(“Add”); If(bookID !=null) { BookDetails book=bookDB.getBookDetails(bookID); } 你也可以从请求中获得输入流并解析数据。要读取字符数据流,可以使用由请求(request)方法getReader返回BufferedReader对象,要读取二进制数据,可以使用getInputStream方法返回的ServletInputStream对象。 HTTP servlet传递HTTP request对象HttpServletRequest,它包含requestURL,HTTP头,查询字符串,等等。 一个HTTP请求URL包含以下部分: http://<host>:<port><request path>?<query string> request path由下面的几部分组成: ☆ Context path: J2EE应用程序的servlet的上下文根构成的一连串斜线(/)。 ☆ Servlet path: 激发请求的组件路径部分,这部分以斜线开头。 ☆ Path info : 请求路径部分,这部分不是上下文路径,也不是servlet路径。 如果上下文路径是/catalog,别名列于下表:
下表给出了将URL分解的例子:
查询字符串由一系列参数和其值组成,参数分别从request的方法getParameter获得其值。有两种方法产生查询字符串: ☆ 查询字符串可以显示的出现在Web页面中。例如,由CatalogServlet产生的HTML页面可能包含下面的连接: <a href=”/bookstore1/catalog?Add=101”>Add to Cart</a> CatalogServlet通过下面的方法分解出这个参数: String bookId=request.getParameter(“Add”); ☆ 当一个表单通过GET HTTP提交时,查询字符串可以跟在URL后面。在Duke’s Bookstore 应用程序中,CasherServlet产生一个表单,接着一个输入到表单中用户名跟到URL后面映射到ReceiptServlet,最后ReceiptServlet通过getParameter方法获得用户名。 构造Responses 一个响应包含数据在服务器域客户之间的传递。所有的响应都实现了ServletResponse接口。该接口的方法允许你做下面的事: ☆ 获得输出流用来向客户端输出数据。发送字符型数据可以使用由response’s getWriter方法返回PrintWriter,要发送二进制数据,可以使用getOutputStream方法返回的ServletOutputStream要发送二者的混合数据,可以通过ServletOutputStream人工管理字符部分以创建一个多部分响应。 ☆ 指出内容类型(content type,例如text/html), ☆ 指出是否缓冲输出。默认情况下,输出流中的任何内容都是立即送到客户端。在任何数据实际发送到客户之前,缓冲允许数据写入,这样允许servlet有更多的时间设置正确的状态码、头信息或定向到期它网络资源。 ☆ 设置本地信息。 HTTP响应对象——HttpServletResponse,有属性代表HTTP头,例如: ☆ 状态码,用来线时请求失败的原因。 ☆ Cookies,用来在客户端存储特定的应用信息。有时cookies也用来作为标志来跟踪用户的会话。 在Duke’s书店应用中,BookDetailsServlet产生HTML页面以显示由servlet从数据库返回的有关书的信息。Servlet首先设置响应:响应的内容类型,缓冲大小;因为数据库访问可能产生异常,因此servlet缓存页面内容以重定向到错误页面。通过缓存响应,客户端不会看到连串的错误页面。DoGet方法由响应返回PrintWriter. Servlet首先分发请求到BannerServlet用来产生公用旗帜以代替响应,接着,Servlet从请求参数获得图书标志,并使用该标志从数据库返回相关信息,最后,servlet产生HTML以描述图书信息,并通过调用PrintWreter的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) { try { BookDetails bd=bookDB.getBookDetails(bookId); …… //print out the information obtained out.println(“<h2>”+bd.getTitle()+”</h2>” }catch(BookNotFoundException ex){}; } out.println(“</body></html>”); out.close(); } } 过滤请求及响应 过滤是一个可传送头或内容或者二者的请求及响应的对象。过滤对象不同于网络组件之处在于它不是自己产生响应,它提供了能依附其它网络资源的功能。因此,过滤对象不能独立于网络资源,它不只可以与一种网络资源组合。它的主要功能是: ☆ 查询请求并作相应的处理。 ☆ 阻止请求响应对进一步通过。 ☆ 修改请求头及数据。通过提供一个自定义的请求 ☆ 修改响应头及数据。通过提供一个自定义的响应。 ☆ 域外部资源交互。 过滤的应用包含授权、日志、图像转换,数据压缩,加密,表示流,及XML转换。 总之,使用过滤的任务包含以下方面: ☆ 编写filter ☆ 编写自定义请求及响应 ☆ 为每个网络资源指定过滤链。 编写Filter 过滤API由Filter,FilterChain及FilterConfig接口定义在javax.servlet包中。你通过实现Filter接口定义一个filter。这个接口中最重要的方法是doFilter,它传递request,response,filter.这个方法可以实现下面的动作: ☆ 检查请求头。 ☆ 如果希望修改请求头或数据可以自定义请求体 ☆ 如果希望修改响应头或数据可以自定义响应体 ☆ 如果当前的filter在链中是最后一个以网络资源或静态资源的filter,可以调用过滤链中的下一个实体,下一个实体是链尾的资源;另外,它是下一个在WAR中配置的filter。它通过调用链体中的doFilter来调用下一个实体。如果要做出选择,可以选择不通过调用下一实体来阻止请求。在下面的例子中,filter负责响应。 ☆ 在调用链中的下一个filter之后检查响应头。 ☆ 抛出异常以说明正在处理错误。 除了doFilter,你必须实现init, destroy方法。当filter实例化后,Init方法由容器自己调用。如果你希望传递初始参数到filter,你可以通过FilterConfig对象传递到init方法。 在Duke ‘s Bookstore程序中使用了HitCounterFilter及Orderfilter来增加,日志计数值。 在doFilter方法中,两个filter都从filter配置体中获得servlet context.所以它们可以访问作为context属性的计数值。在filter完成特定应用处理后,它们调用filter chain中的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(); filterConfig.getServletContext().log(sw.getBuffer().toString()); …… chain.doFilter(request,wrapper); …… } } 编写自定义的请求与响应 filter有很多种方法修改请求及响应。例如,filter可以可以增加一个属性到请求中,或插入数据到响应中。在Duke’s Bookstore这个例子中,HitCounterFilter插入counter的值到响应中。 Filter在修改响应必须在它返回到客户端时捕获响应,方法就是传递替代流到产生响应的servlet,替代流防止servlet关闭源响应。 为传递这替代流到servlet,filter创建一个响应包取代getWriter,getOutputStream方法以返回这替代流。这个替代流传递到filter链的doFilter方法。下面的部分描述了hit counter filter是如何描述早期及其它类型filter使用包的。 为重载请求方法,你可以包装请求到一个扩展ServletRequestWrapper或DttpServletWrapper对象。要重载响应方法,可以包装响应到扩展了ServletResponseWrapper或HttpServletResponseWrapper. HitCounterFilter在CharResponseWrapper中包装了响应。包装的响应被传递到filter链中的下一个对象。BookStoreServlet将响应写到由CharResponseWrapper产生的流中。当chain.doFilter返回时,HitCounterFilter从PrintWriter得到响应,并写入到缓存。Filter插入counter的值到缓存,重设响应头的内容长度,最后将缓存中的内容写到响应流。 PrintWriter out=response.getWriter(); CharResponseWrapper wrapper=new CharResponseWrapper((HttpServletResponse)response); chain.doFilter(request,wrapper); CharArrayWriter caw=new CharResponseWrapper(); 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().length()); Out.writer(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); } } 下图展示了进入Duke’s Bookstore页面:
指定filter映射 Web容器使用filter映射来决定如何将filter应用到网络资源。Filter映射通过命名将一个filter与网络组件匹配,或者通过URL映射到网络资源。在WAR中列出的filter映射,这些filters将被按顺序调用。你在部署工具中为WAR指定一个filter映射列表。 下图显示了过滤器F1映射到servlets S1,S2,S3,filter F2映射到Servlet S2,filter F3映射到servletsS1,S2.
调用其它网络资源 网络组件可以通过两种方法调用其它网络资源:间接的和直接的。当网络组件嵌入在内容返回到一个指向其它网络组件的客户URL时,网络组件间接的调用其它组件。在Duke’s bookstore这个应用程序中,多数网络组件包含嵌入的志向其它网络组件的URL,例如,ShowCartServlet通过嵌入的URL——/bookstore1/catalog间接的调用CatalogServlet。 当网络组件在值形式它也可以直接调用其它网络组件。有两种可能:它可以包含另一个网络资源,或者重定向到另一资源。 要调用在服务器上运行网络组件的可用资源,你必须首先通过getRequestDispatcher方法获得RequestDispatcher对象。 你可以通过请求或者Web context取得RequestDispatcher对象,这两个方法稍有不同。请求方法把请求资源的URL作为一个参数,请求可以使用相对路径。但Web context需要一个绝对路径。如果一个资源不可用,或服务器没有为那类资源实现RequestDispatcher对象,getRequestDispatcher方法将返回null.你的servlet应该处理这类情况。 响应中包含其它资源 包含另一个网络资源通常非常有用。例如,在响应中从网络组件返回的横幅内容或版权信息,要包含其它资源,可以调用RequestDispatcher的include方法。 如果资源是静态的,include方法使能服务端编程。如果资源事网络组件,结果是该方法发送请求到被包含的网络资源,并执行网络组件机返回结果到该包含的servlet中。一个被包含的网络组件可以访问请求对象,但它在响应对象方面却受到一定限制: ☆ 它可以写响应内容级提交一个响应。 ☆ 她不能设置头信息级调用任何影响头的方法。 Duke’s bookstore应用程序的横幅是由BannerServlet产生的。注意doGet,doPost方法都实现了,这是因为BannerServlet可以从调用的servlet中分派。 Public class BannerServlet extends HttpServlet { public void doGet (HttpServletRequest request,HttpServletResponse response) throws ServletException,IOException { PrintWriter out=response.getWriter(); Out.println(“<body bgcolor=\”#ffffff\:”>”+”<center>”+”<hr><br> ”+”<h1>”+”<font size=\”+3\” color=\”#3cc0066\”>Bookstore</font>”+Duke’s </font>”+<img src=\””+request.getContextPath()+”/duke.books.gif\”>”+”<font size=\”+3\” color=\black\”>Bookstore</font>”+”</h1>”+”</center>”+”<br> <hr><br>”); } public void doPost(httpServletRequest request,HttpServletResponse response) throws ServletException,IOException { PrintWriter out=response.getWriter(); Out.println(“<body bgcolor=\”#ffffff\:”>”+”<center>”+”<hr><br> ”+”<h1>”+”<font size=\”+3\” color=\”#3cc0066\”>Bookstore</font>”+Duke’s </font>”+<img src=\””+request.getContextPath()+”/duke.books.gif\”>”+”<font size=\”+3\” color=\black\”>Bookstore</font>”+”</h1>”+”</center>”+”<br> <hr><br>”); } Duke’s Bookstore中的每个servlet都包含有BannerServlet返回的结果,代码如下: RequestDispatcher dispatcher=getServletContext().getRequestDispatcher(“/banner”); If(dispatcher!=null){ Dispatcher.include(request,response); } 转移控制到其它网络组件 在有些程序中,你也需要一个网络组件事先处理一个请求让另外组件产生响应。例如,你也许想部分处理一个请求接着转送到其它组件。 转送控制到另外的网络组件,你可以调用RequestDispatcher的forward方法,当一个请求转送时,请求URL送到请求页面。如果源URL要求处理,你可以把它作为请求属性保存它。 访问Web Context 网络组件中执行的上下文是实现了ServletContext接口的对象。你可以通过getServletContext方法返回上下文对象,Web context提供了下列方法供访问: ☆ 初始参数 ☆ 与Web context相关的资源 ☆ 对象值属性 ☆ 日志能力 在Duke’s Bookstores程序中web context被filter:filters.HitCounterFilter和OrderFilter使用。 Filters存储了一个作为上下文属性的计数器。通过控制并发访问共享资源来回调counter的访问方法,防止并发运行的servlet产生不符的操作。Filter通过context的getAttribute方法返回counter对象。计数器增加的值由context的log方法记录。 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()); …… Context.log(sw.getBuffer().toString()); …… } } 维持客户端状态 很多应用程序需要一系列的来自客户端的相关的请求。例如,在Duke’s Bookstore应用程序中保存用户的购物车中的状态。基于网络的程序要负责维护这样的称之为session的状态,因为HTTP协议是无状态的。为了支持应用程序维持状态,Java servlet技术为管理会话提供了一个API,并允许几种机制实现会话。 访问一个会话 会话由HttpSession对象来代表。你可以通过调用request的getSession方法来获得一个会话。该方法返回与请求相关的当前会话对象,如果不存在则创建一个新的。由于getSession方法可能修改响应头信息(如果用cookies实现会话跟踪机制),它需要在返回Printwriter或ServletOutputStream之前调用。 将属性与会话绑定 你可以将一个对象值属性通过命名与会话绑定。这些属性可以被任何同一网络上下文的,而且是同一会话的请求部分的网络组件访问。 Duke’s Bookstore应用程序通过会话来存储一个客户的购物车信息。这样可以在个请求之间保存购物车信息,并且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(); } } 与会话相关联的通知对象 重新调用你的应用程序中以通知网络上下文及会话监听对象的servlet生命周期事件。你也可以通知某一与会话相关的对象,例如下面的例子: ☆ 当该对象重会话中增加或删除。为接收这个通知,你的对象必须实现java.http.HttpSessionBindListener接口。 ☆ 当对象绑定的会话将钝化或激活时。当一个会话在虚拟机之间转移时,或保存到持久存储器时,会话将相应的激活与钝化。为接收通知,你的对象必须实现该接口——java.http.HttpSessionActivationListener. 会话管理 由于没有办法向HTTP Client示意不再需要会话,每一个会话都有一个相关联的超时设置,以让它的资源回收。超时设置可通过session的[get|set]MaxInactiveInterval方法实现。也可以通过部署工具来设置: 1、选择相应的WAR 2、选择General标签 3、在Advanced box中输入超时时间 为确保一个处于活动状态的会话不超时,你需通过service方法周期性的访问会话,因为这样重置了时间计数器。 当特定的客户结束交互时,你使用会话的invalidate方法让会话失效,并删掉所有相关数据。 会话跟踪 网络容器可以使用一些方法将一个用户与会话相关联,所有的都在服务端与客户端传递一个标志,该标志可以在客户端通过cookie或URL中包含该标志的网络组件来返回到客户端。 如果你的应用程序使用了会话对象,任何时候你必须重写URL以确保会话可以跟踪,哪怕客户停止使用cookie。你可以通过对servlet返回的所有URL调用response的encodeURL(URL)方法。只有当cookie失效时该方法在URL中包含会话ID,否则它返回URL,并不改变它。 下面是ShowCartServlet的doGet方法对购物车下的显示页码的三个URL进行编码: out.println(”<p> <p><strong><a href=\””+response.encodeURL(request.getContextPath()+”/catalog”)+”\”>”+messages.getString(“ContinueShopping”)+”</a> ”+”<a href=\””+response.encodeURL(request.getContextPath()+”/cashier”)+”\”+message.getString(“Checkout”)+”</a> ”+”<a href=\””+response.encodeURL(request.getContextPath()+“/showCart?Clear=clear”)+”\”>”+messages.getString(“ClearCart”)+”</a></strong>”); 如果cookie关闭了,会话被编码如下: http://localhost:8080/bookstore1/cashier; jsessionid=c0o7fszeb1 如果cookie没有关闭,URL如下: http://localhost:8080/bookstore1/cashier 结束servlet 当servlet容器决定一个servlet需从service中删除时,容器调用servlet的destroy方法来实现。在这个方法中,可以释放servlet中使用的任何资源,及保存持久状态。下面的destroy方法释放了在init方法中创建的数据库对象: public void destroy() { bookDB=null; } 当一个servlet移出时,servlet的所有的service方法都应当结束。只有当所有service的请求都返回或过了服务器指定的宽限期,服务器调用destroy方法以确保完全结束。 如果servlet潜在的长时间运行服务请求,使用下面的技术来实现: ☆ 明了当前有多少线程在运行service方法。 ☆ 提供一个彻底的关闭方法destroy以通知长时间运行的关闭线程并等待它们结束。 ☆ 让长时间运行的方法周期性的验证关闭,如果必要,停止服务,接着返回。 跟踪服务请求 跟踪服务请求,包括servlet的一个变量来计运行着的服务方法的次数。改变两需要一致访问方法以增加,减少,然后返回其值。 Public class ShutdownExample extends HttpServlet { private int serviceCounter=0; …… //Access methods for serviceCounter protected synchronize void enteringServiceMethod() { serviceCounter++; } ptotected synchronized void leavingServiceMethod() { serviceCounter--; } protected synchronized void leavingServiceMethod() { return serviceCounter; } } 当服务方法每进入一次,service应增加服务次数,当方法返回时就减少一次。 通知方法Shut Down 为确保关闭,destroy方法需在所有的请求都结束时释放所有的资源。其中一部分就是检查服务次数,另一部分就是通知长时间运行的方法停止。为了实现通知,需要另外一个变量,该变量有通常的访问方法: public class ShutdownExample extends HttpServlet { private boolean shuttingDown; …… //Access methods for ahuttingDown 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)&&lisShuttingDown());I++) { try { partOfLongRunningOperation(i); }catch(InterruptedException e){} } } |