第3章〓Servlet会话跟踪 本章思维导图 本章目标 掌握会话跟踪的相关技术。 理解Cookie的原理。 掌握Cookie的读写方法。 理解Session的原理。 理解Session的生命周期。 熟练掌握Session的使用方法。 掌握ServletConfig的使用方法。 掌握ServletContext的使用方法。 3.1会话跟踪技术简介◆ Internet通信协议可以分为两大类: 有状态协议(stateful)和无状态协议(stateless),两者最大的差别在于客户端与服务器之间维持联机上的不同。 HTTP即是一种无状态协议。HTTP采用“连接—请求—应答—关闭连接”模式。当客户端发出请求时,服务器才会建立连接,一旦客户端的请求结束,服务器便会中断连接,不会一直与客户端保持联机的状态。当下一次请求发起时,服务器会把这个请求看成一个新的连接,与之前的请求无关。 对于交互式的Web应用,保持状态是非常重要的。一个有状态协议可以用来帮助在多个请求和响应之间实现复杂的业务逻辑。例如: 在购物网站中,服务器会为每个用户分配一个购物车,购物车会一直伴随该用户的整个购物过程并且互不混淆,此种情况下,就需要为客户端和服务器之间的交互存储状态。本章所要讲述的会话跟踪技术可以解决这些问题。 会话跟踪技术是一种在客户端与服务器间保持HTTP状态的解决方案。从开发角度考虑,就是使上一次请求所传递的数据能够维持状态到下一次请求,并可辨认出是否为同一客户端所发送出来的。会话跟踪技术的解决方案主要分为以下几种。 Cookie技术。 Session技术。 URL重写技术。 隐藏表单域技术。 视频讲解 3.2Cookie技术◆ Cookie技术是一种在客户端保持会话跟踪的解决方案。Cookie是指某些网站为了辨别用户身份而储存在用户终端上的文本信息(通常经过加密)。Cookie在用户第一次访问服务器时,由服务器通过响应头的方式发送给客户端浏览器; 当用户再次向服务器发送请求时会附带上这些文本信息。图31为服务器对第一次客户端请求所响应的含有“SetCookie”响应头的报文信息,图32为客户端再次请求时附带的含有“Cookie”请求头的报文信息。 图31第一次请求时服务器响应的Cookie报头 图32再次请求时附带的Cookie报头 服务器在接收到来自客户端浏览器的请求时,通过Cookie能够分析请求头的内容而得到客户端特有的信息,从而动态生成与该客户端相对应的内容。例如,在很多登录界面中可以看到“记住我”这样的选项,如果勾选则下次再访问该网站时就会自动记住用户名和密码。另外,一些网站可以根据用户的使用喜好不同浏览器设置稍有不同,进行个性化的风格设置、广告投放等,这些功能都可以通过存储在客户端的Cookie实现。 注意 在使用Cookie时,要保证浏览器接受Cookie。对IE浏览器来说,设置接受Cookie的方法: 选择浏览器的右上角“工具”菜单→“Internet选项”→“隐私”→“高级”→“接受”选项。 Cookie可以通过jakarta.servlet.http.Cookie类的构造方法来创建,其示例代码如下所示。 【示例】Cookie对象的创建 Cookie unameCookie = new Cookie("username","zhaokeling"); 其中,Cookie的构造方法通常需要两个参数。 第一个String类型的参数用于指定Cookie的属性名。 第二个String类型的参数用于指定属性值。 创建完成的Cookie对象可以使用HttpServletResponse对象的addCookie()方法,通过增加“SetCookie”响应头的方式(不是替换原有的)将其响应给客户端浏览器,存储在客户端机器上,示例代码如下所示。生成的Cookie仅在当前浏览器有效,不能跨浏览器。 【示例】服务器向客户端响应Cookie response.addCookie(unameCookie); 其中,addCookie()方法中的参数为一个Cookie对象。存储在客户端的Cookie,通过HttpServletRequest对象的getCookies()方法获取,该方法返回所访问网站的所有Cookie的对象数组,遍历该数组可以获得各个Cookie对象,示例代码如下所示。 【示例】获取并遍历客户端Cookie Cookie[] cookies = request.getCookie(); if(cookies != null) for(Cookie c : cookies){ out.println("属性名:" + c.getName()); out.println("属性值" + c.getValue()); } 在默认情况下,Cookie只能被创建它的应用获取。Cookie的setPath()方法可以重新指定其访问路径,例如将其设置为可被某个应用下的某个路径共享,或被同一服务器内的所有应用共享,如下述示例所示。 【示例】设置Cookie在某个应用下的访问路径 unameCookie.setPath("/ch03/jsp/"); 【示例】设置Cookie在服务器中所有应用下的访问路径 unameCookie.setPath("/"); Cookie有一定的存活时间,不会在客户端一直保存。默认情况下,Cookie保存在浏览器内存中,在浏览器关闭时失效,这种Cookie也称为临时Cookie(或会话Cookie)。若要让其长久地保存在磁盘上,可以通过Cookie对象的setMaxAge()方法设置其存活时间(以秒为单位),时间若为正整数,表示其存活的秒数; 若为负数,表示其为临时Cookie; 若为0,表示通知浏览器删除相应的Cookie。保存在磁盘上的Cookie也称为持久Cookie。下述示例描述存活时间为1周的持久Cookie。 【示例】设置Cookie的存活时间 unameCookie.setMaxAge(7*24*60*60); //参数以秒为基本单位 下述代码演示使用Cookie保存用户名和密码,当用户再次登录时在相应的文本栏显示上次登录时输入的信息。 首先,编写用于接收用户输入的HTML表单文件,在该例子中,没有使用HTML文件而是用一个Servlet来完成此功能,这是因为需要通过Servlet去读取客户端的Cookie,而HTML文件无法完成此功能。 【案例31】LoginServlet.java @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throwsServletException, IOException { String cookieName = "userName"; String cookiePwd = "pwd"; // 获得所有Cookie Cookie[] cookies = request.getCookies(); String userName = ""; String pwd = ""; String isChecked = ""; // 如果Cookie数组不为null,说明曾经设置过 // 也就是曾经登录过,那么取出上次登录的用户名和密码 if (cookies != null) { // 如果曾经设置过Cookie,checkbox状态应该是checked isChecked = "checked"; for (inti = 0; i < cookies.length; i++) { // 取出登录名 if (cookies[i].getName().equals(cookieName)) { userName = cookies[i].getValue(); } // 取出密码 if (cookies[i].getName().equals(cookiePwd)) { pwd = cookies[i].getValue(); } } } response.setContentType("text/html;charset=GBK"); PrintWriter out = response.getWriter(); out.println("\n"); out.println("
您购买的书籍有:
"); // 遍历显示购物车中的书籍名称和选择次数 for (StringbookName : car.keySet()) { out.println("" + bookName + " , " + car.get(bookName) + "本
"); } } else { out.println("您还未购买任何书籍!
"); } out.println(""); } } 用户提交书籍选择表单后的运行结果如图38所示。 图38表单提交后的运行结果 单击“继续购买”链接,返回到bookChoose.jsp图书页面可以继续购买,结果如图39所示。 图39继续购买后的运行结果 视频讲解 3.4URL重写技术◆ URL重写是指服务器程序对接收的URL请求重新写成网站可以处理的另一个URL的过程。URL重写技术是实现动态网站会话跟踪的重要保障。在实际应用中,当不能确定客户端浏览器是否支持Cookie的情况下,使用URL重写技术可以对请求的URL地址追加会话标识,从而实现用户的会话跟踪功能。 例如,对于如下格式的请求地址: http://localhost:8080/ch03/EncodeURLServlet 经过URL重写后,地址格式变为: http://localhost:8080/ch03/EncodeURLServlet;jsessionid=24666BB458B4E0A68068CC49A97FC4A9 其中“jsessionid”即为追加的会话标识,服务器可以通过它来识别跟踪某个用户的访问。 URL重写通过HttpServletResponse的encodeURL()方法和encodeRedirectURL()方法实现,其中encodeRedirectURL()方法主要对使用sendRedirect()方法的URL进行重写。URL重写方法根据请求信息中是否包含“SetCookie”请求头来决定是否进行URL重写,若包含该请求头,会将URL原样输出; 若不包含,则会将会话标识重写到URL中。 URL重写的示例代码如下所示。 【示例】encodeURL()方法的使用 out.print("链接请求"> 【示例】encodeRedirectURL()方法的使用 response.sendRedirect(response.encodeRedirectURL("EncodeURLServlet")); 下述代码演示在浏览器Cookie禁用后,普通请求和重定向请求的URL重写方法,以及重写后会话标识“jsessionid”的跟踪情况。 图310演示对IE浏览器Cookie的禁用设置。 图310IE浏览器Cookie的禁用设置 【案例36】UrlRewritingServlet.java @WebServlet("/UrlRewritingServlet") public class UrlRewritingServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); // 获取会话对象 HttpSession session = request.getSession(); // 对CommonServlet和UseRedirectServlet两个请求地址进行URL重写 String link1 = response.encodeURL("CommonServlet"); String link2 = response.encodeURL("UseRedirectServlet"); // 使用超链接形式对URL重写地址进行请求 out.println("对一个普通Servlet的请求"); out.println("对一个含有重定向代码的Servlet的请求"); } } 启动服务器,在IE中访问“http://localhost:8080/ch03/UrlRewritingServlet”,运行结果和页面源码如图311所示。 图311UrlRewritingServlet.java运行结果和页面源码 从运行结果可以看出,两个Servlet请求地址经URL重写后,都被附加了jsessionid标识。下述代码演示第一个超链接CommonSevlet对会话标识的获取。 【案例37】CommonServlet.java @WebServlet("/CommonServlet") public class CommonServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { PrintWriter out = response.getWriter(); // 获取经URL重写传递来的会话标识值 String sessionId = request.getSession().getId(); out.println(sessionId); } } 单击第一个超链接,运行结果如图312所示。 图312CommonServlet.java运行结果 下述代码演示第二个超链接UseRedirectServlet对重定向URL的重写方法。 【案例38】UseRedirectServlet.java @WebServlet("/UseRedirectServlet") public class UseRedirectServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 对重定向的URL进行重写 String encodeURL = response.encodeRedirectURL("CommonServlet"); // 进行重定向 response.sendRedirect(encodeURL); } } 单击第二个超链接,可以发现运行结果与图312完全相同。 由此实例可以看出,在客户端浏览器完全禁用了Cookie后,通过在请求地址后附加会话标识的URL重写技术仍可实现会话的跟踪。但使用此种方式,有以下几个方面需要注意。 如果应用需要使用URL重写,那么必须对应用的所有请求(包括所有的超链接、表单的action属性值和重定向地址)都进行重写,从而将jsessionid维持下来。 浏览器对URL地址长度有限制,所以在对含有查询参数的GET请求进行URL重写时,需要注意其总长度。 由于静态页面不能进行会话标识的传递,因此所有的URL地址都必须为动态请求地址。 3.5隐藏表单域技术◆ 利用Form表单的隐藏表单域技术,可以在完全脱离浏览器对Cookie的使用限制以及在用户无法从页面显示看到隐藏标识的情况下,将标识随请求一起传送给服务器处理,从而实现会话的跟踪。 设置隐藏表单域的示例代码如下所示。 【示例】在Form表单中定义隐藏域 在服务器端通过HttpServletRequest对象获取隐藏域的值,示例代码如下所示。 【示例】隐藏域的获取 String flag = request.getParameter("userID"); 由于使用隐藏表单域技术进行会话跟踪的基本前提是只能通过Form表单来传递标识信息,因此此技术在实际应用中并不常用。 3.6ServletConfig接口◆ jakarta.servlet.ServletConfig接口的定义为: public abstract interface jakarta.servlet.ServletConfig 容器在初始化一个Servlet时,将为该Servlet创建一个唯一的ServletConfig对象,并将这个ServletConfig对象通过init(ServletConfig config)方法传递并保存在此Servlet对象中。 ServletConfig接口的主要方法如表33所示。 表33ServletConfig接口的主要方法 方法方 法 描 述 getInitParameter(String param)根据给定的初始化参数名称,返回参数值,若参数不存在,返回null getInitParameterNames()返回一个Enumeration对象,里面包含了所有的初始化参数名称 getServletContext()返回当前ServletContext()对象 getServletName()返回当前Servlet的名字,即@WebServlet的name属性值。如果没有配置这个属性,则返回Servlet类的全限定名 使用ServletConfig接口中的方法主要可以访问两项内容: Servlet初始化参数和ServletContext对象。前者通常由容器从Servlet的配置属性中读取(如initParams或当前Web应用的所有初始化参数:"); while (paramNames.hasMoreElements()) { String name = paramNames.nextElement(); out.print(name + " "); } out.println("
webSite参数的值:" + webSite); out.println("
adminEmail参数的值:" + adminEmail + "
"); } } 启动服务器,在IE中访问“http://localhost:8080/cha03/ContextInitParamServlet”,运行结果如图313所示。 图313ContextInitParamServlet运行结果 视频讲解 3.7.2存取应用域属性 ServletContext对象可以理解为容器内的一个共享空间,可以存放具有应用级别作用域的数据,Web应用中的各个组件都可以共享这些数据。这些共享数据以key/value的形式存放在ServletContext对象中,并以key作为其属性名被访问。具体的应用域属性的存取方法如表35所示。 表35ServletContext对象应用域属性的存取方法 方法方 法 描 述 setAttribute(String name,Object object) 把一个对象和一个属性名绑定并存放到ServletContext中,参数name指定属性名,参数Object表示共享数据 getAttribute(String name)根据参数给定的属性名,返回一个Object类型的对象 getAttributeNames()返回一个Enumeration对象,该对象包含了所有存放在ServletContext中的属性名 removeAttribute(String name)根据参数指定的属性名,从ServletContext对象中删除匹配的属性 注意 应用域具有以下两层含义: 一是表示由Web应用的生命周期构成的时间段; 二是表示在Web应用范围内的可访问性。 下述代码通过一个网站访问计数的例子演示应用域属性的存取方法。 【案例311】ContextAttributeServlet.java @WebServlet("/ContextAttributeServlet") public class ContextAttributeServlet extends HttpServlet { private static final long serialVersionUID = 1L; public ContextAttributeServlet() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //设置响应到客户端的文本类型 response.setContentType("text/html;charset=UTF-8"); //获取ServletContext对象 ServletContext context = super.getServletContext(); //从ServletContext对象获取count属性存放的计数值 Integer count = (Integer) context.getAttribute("count"); if (count == null) { count = 1; } else { count = count + 1; } //将更新后的数值存放到ServletContext对象的count属性中 context.setAttribute("count", count); //获取输出流 PrintWriter out = response.getWriter(); //输出计数信息 out.println("本网站目前访问人数是: " + count + "
"); } } 再新建一个Servlet命名为ContextAttributeOtherServlet,代码内容与ContextAttributeServlet完全相同,启动服务器,在IE中先后访问“http://localhost:8080/ch03/ContextAttributeServlet”与“http://localhost:8080/ch03/ContextAttributeOtherServlet”,运行结果如图314所示。 图314ContextAttributeServlet与ContextAttributeOtherServlet运行结果 由上述代码可以看出,对于存放在ServletContext对象中的属性count,不同的Servlet都可以通过ServletContext对象对其进行访问和修改,并且一方的修改会影响另一方获取的数据值,因此在多线程访问情况下,需要注意数据的同步问题。 视频讲解 3.7.3获取当前应用信息 ServletContext对象还包含有关Web应用的信息,例如: 当前Web应用的根路径、应用的名称、应用组件间的转发,以及容器下其他Web应用的ServletContext对象等。具体信息的获取如表36所示。 表36ServletContext接口访问当前应用信息的方法 方法方 法 描 述 getContextPath()返回当前Web应用的根路径 getServletContextName()返回Web应用的名称,即当前Web应用的上下文根路径是:" + contextPath + "
"); out.println("当前Web应用的名称是:" + contextName + "
"); } } 启动服务器,在IE中访问“http://localhost:8080/ch03/ContextAppInfoServlet”,运行结果如图315所示。 图315ContextAppInfoServlet.java运行结果 注意 Tomcat服务器默认不能跨应用访问,因此若要使用当前应用的ServletContext对象的getContext(String uripath)方法访问同一容器下的其他应用,需要将"%TOMCAT_HOME%/conf/context.xml"文件中的“Web容器的名字和版本为:" + serverInfo +"
"); out.println("Web容器支持的Servlet API的主版本号为:" + majorVersion +"
"); out.println("Web容器支持的Servlet API的次版本号为:" + minoVersion +"
"); } } 启动服务器,在浏览器中访问http://localhost:8080/ch03/ContextLogInfoServlet,运行结果如图316所示。 图316ContextLogInfoServlet.java运行结果 ContextLogInfoServlet中记录的日志信息在Tomcat服务器控制台显示效果如图317所示。 图317日志信息在Tomcat服务器控制台显示效果 3.7.5获取服务器文件资源 使用ServletContext接口可以直接访问Web应用中的静态内容文件,例如HTML、GIF、Properties文件等,同时还可以获取文件资源的MIME类型以及其在服务器中的真实存放路径,具体方法如表38所示。 表38ServletContext接口访问服务器端文件系统资源的方法 方法方 法 描 述 getResourceAsStream(String path)返回一个读取参数指定的文件的输入流,参数路径必须以"/"开头 getResource(String path)返回由path指定的资源路径对应的一个URL对象,参数路径必须以"/"开头 getRealPath(String path)根据参数指定的虚拟路径,返回文件系统中的一个真实的路径 getMimeType(String file)返回参数指定的文件的MIME类型 下述代码演示使用ServletContext接口访问当前ch03应用中images目录下的mypic.jpg文件。 【案例314】ContextFileResourceServlet.java @WebServlet("/ContextFileResourceServlet") public class ContextFileResourceServlet extends HttpServlet { private static final long serialVersionUID = 1L; public ContextFileResourceServlet() { super(); } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置响应到客户端MIME类型和字符编码方式 response.setContentType("text/html;charset=UTF-8"); // 获取ServletContext对象 ServletContext context = super.getServletContext(); // 获取用于读取指定静态文件的输入流 InputStream is = context.getResourceAsStream("/images/mypic.jpg"); // 获取一个映射到指定静态文件路径的URL URL url = context.getResource("/images/mypic.jpg"); // 从URL对象中获取文件的输入流 InputStream in = url.openStream(); // 比较使用上述两种方法获取同一文件输入流的大小 boolean isEqual = is.available() == in.available(); // 根据指定的文件虚拟路径获取真实路径 String fileRealPath = context.getRealPath("/images/mypic.jpg"); // 获取指定文件的MIME类型 String mimeType = context.getMimeType("/images/mypic.jpg"); // 获取输出流 PrintWriter out = response.getWriter(); out.println("两种方式获取同一文件输入流的大小是否相等:" + isEqual + "
"); out.println("虚拟路径"/images/mypic.jpg"的真实路径为:" + fileRealPath + "
"); out.println("mypic.jpg的MIME类型为:" + mimeType + "
"); out.close(); } } 启动服务器,在浏览器中访问http://localhost:8080/ch03/ContextFileResourceServlet,运行结果如图318所示。 图318ContextFileResourceServlet运行结果 本章总结 Cookie是保存在客户端的小段文本。 通过请求可以获得Cookie,通过响应可以写入Cookie。 Session是浏览器与服务器之间的一次通话,它包含浏览器与服务器之间的多次请求、响应过程。 Session可以在用户访问一个Web站点的多个页面时共享信息。 在Servlet中通过request.getSession()获取当前Session对象。 关闭浏览器、调用Session的invalidate()方法或者等待Session超时都可以使Session失效。 HttpSession使用getAttribute()和setAttribute()方法读写数据。 ServletContext是运行Servlet的容器。 在Servlet中可以通过getServletContext()方法获取ServletContext实例。 ServletContext使用getAttribute()和setAttribute()方法读写数据。 本章习题 1. 下列关于Cookie的说法正确的是。(多选) A. Cookie保存在客户端B. Cookie可以被服务器端程序修改 C. Cookie中可以保存任意长度的文本D. 浏览器可以关闭Cookie功能 2. 写入和读取Cookie的代码分别是。 A. request.addCookies()和response.getCookies() B. response.addCookie()和request.getCookie() C. response.addCookies()和request.getCookies() D. response.addCookie()和request.getCookies() 3. HttpServletRequest的方法可以得到会话。(多选) A. getSession()B. getSession(boolean) C. getRequestSession()D. getHttpSession() 4. 下列选项可以关闭会话的是。(多选) A. 调用HttpSession的close()方法 B. 调用HttpSession的invalidate()方法 C. 等待HttpSession超时 D. 调用HttpServletRequest的getSession(false)方法 5. 在HttpSession中写入和读取数据的方法是。 A. setParameter()和getParameter()B. setAttribute()和getAttribute() C. addAttribute()和getAttribute()D. set()和get() 6. 关于HttpSession的getAttribute()方法和setAttribute()方法,说法正确的是。(多选) A. getAttribute()方法返回类型是String B. getAttribute()方法返回类型是Object C. setAttribute()方法保存数据时如果名字重复会抛出异常 D. setAttribute()方法保存数据时如果名字重复会覆盖以前的数据 7. 设置session的有效时间(也叫超时时间)的方法是。 A. setMaxinactivelnterval(int interval) B. getAttributeName() C. setAttrlbuteName(Strlng name,java.lang.Object value) D. getLastAccessedTime() 8. 使HttpSession失效的三种方式是、、。 9. 测试在其他浏览器下session的生命周期,如Firefox、chrome等。