第 3 章Servlet 视频讲解 3.1Servlet 概述 Servlet是用Java语言编写的服务器端程序,在服务器端调用和执行。Servlet可以处理客户端发来的HTTP请求,并返回一个响应。狭义的Servlet指用Java语言实现的一个Servlet接口,广义的Servlet指任何实现了这个接口的类。虽然是用Java语言编写的程序,Servlet没有public static void main(String[] args)方法,不能独立运行。它的运行需要服务器提供运行环境。能够为Servlet提供运行环境的软件称为Web容器或Servlet容器(如Tomcat)。Web容器在接收客户端请求后生成响应。一台物理服务器上可以布置多个Web容器。而Servlet需要由Web容器实例化并调用。Servlet应用程序的体系结构如图31所示。 图31Servlet应用程序的体系结构 如图31所示,客户端向Servlet发出请求,该请求首先被HTTP服务器(如Nginx、Apache等)接收,HTTP服务器只负责静态页面(HTML页面)的解析,对于Servlet请求则转交给Web容器。Web容器会根据请求路径的映射关系调用相应的Servlet 进行处理。Servlet处理请求后,将响应返回给客户端。 与其他技术相比,Servlet技术具有以下特点: (1) Servlet使用了与CGI(Common Gateway Interface,通用网关接口)不同的处理模型,因此运行速度更快。 (2) Servlet使用了很多Web容器都支持的标准API(Application Programming Interface,应用程序编程接口)。 (3) Servlet具有Java语言的全部优点,如开发简单、平台独立等。 (4) Servlet可以使用Java API。 3.2Servlet 基础 针对Servlet开发,有一系列可用的接口和类。其中最重要的接口是jakarta.servlet.Servlet。Servlet接口定义了5个抽象方法,如表31所示。 表31Servlet接口的抽象方法 方 法 声 明说明 void init(ServletConfig config)Web容器在创建Servlet对象后,会调用此方法。该方法接收一个ServletConfig类型的参数,容器通过这个参数向Servlet传递初始化配置信息 ServletConfig getServletConfig()用于获取Servlet对象的配置信息,返回ServletConfig对象 String getServletInfo()返回一个字符串,其中包含Servlet信息,如作者、版本等 void service(ServletRequest request,ServletResponse response)负责响应用户请求,当容器收到客户端访问Servlet的请求时就会调用此方法。容器会构造一个表示客户端请求信息的ServletRequest对象和一个用于响应请求的ServletResponse对象作为参数传递给service()方法。该方法可以通过ServletRequest对象得到客户端的相关信息和请求信息,在对请求进行处理后,调用ServletResponse对象的方法设置响应信息 void destroy()负责释放Servlet对象占用的资源。当服务器关闭或Servlet对象被移除时,容器会调用此方法销毁Servlet对象 Servlet接口是Jakarta Servlet API的核心。所有的Servlet都可以实现这个接口。或者更直接地,用户自定义的Servlet可以继承Servlet接口的实现类。在Jakarta Servlet API中,有两个实现了Servlet接口的类: GenericServlet和HttpServlet。Servlet接口、GenericServlet类和HttpServlet类的关系如图32所示。对大多数开发者而言,可以通过直接继承HttpServlet类,创建一个Servlet。 图32Servlet接口、GenericServlet类和HttpServlet类的关系 图32给出了jakarta.servlet包中与Servlet接口有关的4个接口。ServletRequest和ServletResponse接口分别代表请求和响应对象; ServletConfig代表Servlet初始化时用来传递Servlet配置信息的对象。此外,在jakarta.servlet.http包中还提供了ServletRequest接口的子接口HttpServletRequest和ServletResponse接口的子接口HttpServletResponse,它们分别代表HTTP请求对象和HTTP响应对象。关于HttpServletRequest、HttpServletResponse和ServletConfig接口的更多内容见3.4节。 表31列举的5个抽象方法中,init()、service()和destroy()方法是与Servlet生命周期相关的3个方法。Servlet的生命周期可以被定义为Servlet从创建到销毁的整个过程。Servlet的生命周期可以分为初始化阶段、运行阶段和销毁阶段,如图33所示。 图33Servlet的生命周期 1. 初始化阶段 当客户端向Servlet容器发出HTTP请求访问Servlet时,Servlet容器会解析请求,检查内存中是否已经存在该Servlet对象。如果存在则直接使用该Servlet对象; 如果没有则创建Servlet对象。然后调用init()方法实现Servlet的初始化。初始化一般是完成一些一次性的工作,如读取持久化配置数据,执行一些耗时的操作 [如基于JDBC(Java Database Connetivity,Java数据库连接)API的数据库连接]。在Servlet的生命周期内,init()方法只被调用一次。 2. 运行阶段 这是Servlet生命周期中最重要的阶段。在这个阶段,Servlet容器会创建代表客户端请求的ServletRequest对象和代表服务器响应的ServletResponse对象,然后将它们作为参数传递给Servlet的service()方法。service()方法从ServletRequest对象中获得客户端请求信息并处理该请求,通过ServletResponse对象生成响应。在Servlet的整个生命周期内,对于每一个访问Servlet的请求,Servlet容器都会 创建新的ServletRequest和ServletResponse对象,并调用 service()方法处理该请求。即在Servlet的生命周期中,service()方法会被多次调用。 3. 销毁阶段 当Servlet容器关闭或Servlet对象被移除时,Servlet容器会调用destroy()方法。destroy()方法一般用于执行一些清理活动,如关闭数据库连接,停止后台线程,把Cookie数据写入磁盘等。在Servlet的生命周期中,destroy()方法只被调用一次。 【例31】创建一个Servlet并运行。 创建一个名为myservlet的动态Web项目,在src/main/java文件夹下创建一个名为com.example.servlet.demo的包。 右击该包,在弹出的快捷菜单中选择Servlet的创建向导,如图34所示。 图34选择Servlet的创建向导 指定新建的Servlet的名字和所在包,如图35所示,本例中的Servlet继承了jakarta.servlet.http.HttpServlet类。单击Next按钮,指定要自动生成的Servlet方法,如图36所示,本案例中选择生成init()、service()和destroy()方法。单击Finish按钮即完成Servlet的创建。 图35指定Servlet的包名和类名 图36指定要自动生成的Servlet方法 修改生成的Servlet代码。在init()方法和service()方法中分别加入控制台输出。修改后的代码如文件31所示。 【文件31】MyFirstServlet.java 1package com.example.servlet.demo; 2 3import jakarta.servlet.ServletConfig; 4import jakarta.servlet.ServletException; 5import jakarta.servlet.annotation.WebServlet; 6import jakarta.servlet.http.HttpServlet; 7import jakarta.servlet.http.HttpServletRequest; 8import jakarta.servlet.http.HttpServletResponse; 9import java.io.IOException; 10 11@WebServlet("/MyFirstServlet") 12public class MyFirstServlet extends HttpServlet { 13private static final long serialVersionUID = 1L; 14 15public MyFirstServlet() { 16super(); 17} 18public void init(ServletConfig config) throws ServletException { 19System.out.println("initialize"); 20} 21public void destroy() { 22} 23 24protected void service(HttpServletRequest request, 25HttpServletResponse response) 26throws ServletException, IOException { 27System.out.println("handle requests"); 28} 29} 运行Servlet的时候,需要先将项目部署到Tomcat服务器上(右击Servlet类文件,依次选择Run as→Run on server命令),启动Tomcat,然后在浏览器的地址栏输入“http://localhost:8080/myservlet/MyFirstServlet”。此时,控制台的输出如图37所示。 图37控制台的输出 可见,当MyFirstServlet接收到客户端请求时,首先进行初始化,由Servlet容器(Tomcat)调用init()方法,控制台输出字符串“initialize”。随后,Tomcat调用Servlet的service()方法处理用户请求,控制台输出字符串“handle requests”。此时,如果刷新浏览器窗口,会看到控制台又一次输出字符串“handle requests”。由此可见,在Servlet的生命周期中,init()方法只被调用一次,而用来处理请求的service()方法会被多次调用。 提示: 给Servlet发送请求的时候,请求的地址必须与@WebServlet注解中的参数完全一致。如本例中,@WebServlet标签中指定的参数是“/MyFirstServlet”,因此,该Servlet请求地址即为 “http://localhost:8080/myservlet/MyFirstServlet” 事实上,HttpServlet类不仅定义了service()方法,也定义了doGet()和doPost()方法。这样,在Servlet的生命周期中,处理客户端请求就有两种方案。一种是用service()方法处理请求,另一种是用doGet()和doPost()方法代替service()方法处理请求。这两个方法与客户端发送请求的方式密切相关。doGet()方法处理以GET方式发送的请求,doPost()方法处理以POST方式发送的请求。由于大多数客户端发送请求的方式都是GET和POST,因此,学习如何使用doGet()方法和doPost()方法处理请求就变得相当重要。下面通过一个案例来介绍doGet()和doPost()方法的使用。 【例32】分别以GET和POST方式向Servlet发送请求,并查看控制台输出,步骤如下。 1. 创建Servlet类 在com.example.servlet.demo包中创建一个名为RequestMethodServlet的类。 2. 重写doGet()和doPost()方法 重写doGet()和doPost()方法如文件32所示。 【文件32】RequestMethodServlet.java 1package com.example.servlet.demo; 2 3import jakarta.servlet.ServletException; 4import jakarta.servlet.annotation.WebServlet; 5import jakarta.servlet.http.HttpServlet; 6import jakarta.servlet.http.HttpServletRequest; 7import jakarta.servlet.http.HttpServletResponse; 8import java.io.IOException; 9import java.io.PrintWriter; 10 11@WebServlet("/RequestMethodServlet") 12public class RequestMethodServlet extends HttpServlet { 13 14protected void doGet(HttpServletRequest request, 15HttpServletResponse response) throws ServletException, IOException { 16PrintWriter out = response.getWriter(); 17out.print("this is doGet() method."); 18} 19 20protected void doPost(HttpServletRequest request, 21HttpServletResponse response) throws ServletException, IOException { 22PrintWriter out = response.getWriter(); 23out.print("this is doPost() method."); 24} 25} 3. 提交GET请求 启动Tomcat服务器,在浏览器的地址栏输入“http://localhost:8080/myservlet/RequestMethodServlet”, 以GET方式向Servlet发送请求,浏览器显示结果如图38所示。由此可见,当以GET方式向Servlet发送请求时,Servlet会调用doGet()方法处理请求。 图38提交GET请求后浏览器的显示结果 4. 提交POST请求 采用POST方式提交请求时,需要在src/main/webapp 目录下创建一个名为form.html的文件,其中表单的acion属性要与@WebServlet注解指定的参数一致。这里省略了代表项目根目录的正斜线(/),并指定请求提交方式为POST,代码如文件33所示。 【文件33】form.html 1<html> 2<body> 3<form action="RequestMethodServlet" method="post"> 4<label>用户名</label> 5<input type="text" name="name" /><br> 6<input type="submit" value="提交" /> 7</form> 8</body> 9</html> 启动Tomcat服务器后,在浏览器的地址栏输入“http://localhost:8080/myservlet/form.html”。填写相关内容后,单击“提交”按钮,以POST方式向Servlet发送请求,浏览器显示的结果如图39所示。由此可见,采用POST方式提交请求时,Servlet会调用doPost()方法处理请求。 图39提交POST请求后浏览器的显示结果 3.3Servlet 配置 不同于传统的Java应用程序,Servlet在创建后需要在服务器端做好配置。当然,有些集成化开发环境在创建Servlet的同时即完成了配置,例如文件31的创建过程。配置Servlet有两种方式: 部署描述符和注解。 1. 部署描述符 部署描述符可以在应用程序开发阶段、集成阶段和部署阶段传递Web应用程序的元素和配置信息。 在Servlet 5.0规范中,部署描述符是根据XML Schema文档定义的。对于例31中的Servlet,可采用如下的部署描述符进行配置。 在项目的WEBINF目录中创建(或修改)web.xml文件,代码如文件34所示。 【文件34】web.xml 1<servlet> 2<servlet-name>MyFirstServlet</servlet-name> 3<servlet-class> 4com.example.servlet.demo.MyFirstServlet 5</servlet-class> 6<load-on-start-up>1</load-on-start-up> 7<init-param> 8<param-name>catalog</param-name> 9<param-value>Spring</param-value> 10</init-param> 11</servlet> 12<servlet-mapping> 13<servlet-name>MyFirstServlet</servlet-name> 14<url-pattern>/MyFirstServlet</url-pattern> 15</servlet-mapping> 如文件34所示,元素<servlet>用于注册一个Servlet(第1~11行)。它的两个子元素<servletname>和<servletclass>分别用来指定Servlet的名字(第2行)和全限定名(第3~5行)。元素<servletmapping>用于映射外界对Servlet的访问路径(第12~15行),它的子元素<servletname>的值(第13行)必须与<servlet>元素中<servletname>的值完全一致。子元素<urlpattern>则是用于指定访问该Servlet的虚拟路径(第14行),该路径以正斜线(/)开始,代表当前Web应用程序的根目录。 元素<servlet>的子元素<loadonstartup>是一个可选项(第6行),它用于指定Servlet被加载的时机和顺序。在<loadonstartup>元素中,必须设置一个整数。如果这个值是一个负数或者没有设定这个元素,Servlet容器将在客户端首次请求这个Servlet的时候加载它; 如果这个值是正整数或0,Servlet容器将在Web应用启动时加载并初始化Servlet,<loadonstartup>设置的值越小,对应的Servlet被加载的优先级越高。 元素<servlet>的子元素<initparam>是一个可选项(第7~10行),用来配置Servlet的初始化参数。参数的名字和值分别由<paramname>和<paramvalue>子元素指定。 2. 注解 从Servlet 3.0规范开始,可以使用注解(Annotation)来告知Servlet 容器哪些Servlet会提供服务。在创建Servlet后,可以用@WebServlet注解来配置Servlet。在文件31中,使用@WebServlet("/MyFirstServlet")来配置一个Servlet(第11行)。因为设置了 @WebServlet注解,容器会自动读取注解中的内容,进而完成配置。该注解告知容器,如果请求的URL中包含 /MyFirstServlet ,则由当前的MyFirstServlet处理此请求。因此,访问这个Servlet的时候,只要在地址栏输入 “http://localhost:8080/myservlet/MyFirstServlet”即可。@WebServlet注解的属性如表32所示。 提示: 在配置一个Servlet的时候,部署描述符和注解只能选择一种,两种方式不可混用。本书的后续案例均以注解方式进行配置。 表32@WebServlet的属性 属性名类型说明 asyncSupportedboolean声明Servlet是否支持异步操作模式 descriptionString对Servlet的描述 displayNameStringServlet 的显示名,通常配合工具使用 initParamsWebInitParam[]指定一组 Servlet 的初始化参数,等价于<initparam> largeIconString指定Servlet的大图标 loadOnStartupint指定 Servlet的加载顺序,等价于<loadonstartup> nameString指定Servlet的名字,等价于<servletname>。如果没有显式指定,则该属性的取值即为类的全限定名 smallIconString指定Servlet的小图标 urlPatternsString[]指定一组 Servlet 的 URL 匹配模式,等价于<urlpattern> valueString[]等价于 urlPatterns 属性。两个属性不能同时使用 视频讲解 3.4Servlet常用接口 〖*2〗3.4.1HttpServletRequest接口 在Jakarta Servlet API中定义了一个HttpServletRequest接口。它继承自ServletRequest接口,专门用来封装HTTP请求。由于HTTP请求消息分为请求行、请求头和请求消息体(实体主体)3部分,因此,在HttpServletRequest接口中定义了获取请求行、请求头和请求消息体的相关方法。 1. 获取请求行的相关方法 请求行主要包括请求方法字段、URL字段、协议名称和版本号字段等。相关方法如表33所示。 表33获取请求行的相关方法 〖XB,HT5”SS;Y2<续表>〗 方 法 声 明说明 String getMethod()获取HTTP请求消息中的请求方法(如GET、POST等) String getRequestURI()获取请求行中资源名称的部分,即从协议名称到HTTP请求第一行中的查询字符串之前的部分 (不含服务器名称、端口号) StringBuffer getRequestURL()重建客户端用于发出请求的URL。返回的URL包含协议名、服务器名称、端口号和服务器路径,但不包括查询字符串参数 String getQueryString()返回请求行中的参数部分,即请求URL中问号(?)以后的内容 String getProtocol()返回请求行中的协议名和版本,如HTTP/1.1 String getContextPath()返回请求URI中指示请求上下文的部分。在请求URI中,上下文路径总是排在第一位。路径以“/”字符开头,但不以“/”字符结尾。对于默认(根)上下文中的Servlet,此方法返回空字符串“” String getServletPath()获取Servlet名称或Servlet的映射路径 String getRemoteAddr()获取客户端的IP地址 String getRemoteHost()获取客户端的完整主机名,如host.example.com。如果无法解析客户端的完整主机名,将返回客户端的IP地址 int getRemotePort()获取客户端网络连接的端口号 String getLocalAddr()获取Web服务器上用于接收请求的端口的IP地址 int getLocalPort()获取Web服务器上用于接收请求的端口的端口号 String getLocalName()获取Web服务器上接收请求的端口的主机名 String getServerName()返回请求发送到的服务器的主机名 int getServerPort()返回请求发送到的服务器的端口号 String getScheme()获取请求的协议名,如http,https或ftp 下面,通过一个案例来演示这些方法的使用。在src/main/java文件夹下新建一个名为com.example.servlet.request的包。在包中创建一个名为RequestLineServlet的类,在该类中编写用于获取请求行中相关信息的方法。 【例33】获取请求行信息。代码如文件35所示。 【文件35】RequestLineServlet.java 1package com.example.servlet.request; 2 3import java.io.IOException; 4import java.io.PrintWriter; 5 6import jakarta.servlet.ServletException; 7import jakarta.servlet.annotation.WebServlet; 8import jakarta.servlet.http.HttpServlet; 9import jakarta.servlet.http.HttpServletRequest; 10import jakarta.servlet.http.HttpServletResponse; 11 12@WebServlet("/RequestLineServlet") 13public class RequestLineServlet extends HttpServlet { 14 15protected void doGet(HttpServletRequest request, 16HttpServletResponse response) throws ServletException, IOException { 17response.setContentType("text/html;charset=UTF-8"); 18PrintWriter out = response.getWriter(); 19// 获取请求行的相关信息 20out.print("getMethod:" + request.getMethod() + "<br>"); 21out.print("getRequestURI:" + request.getRequestURI() + "<br>"); 22out.print("getQueryString:"+request.getQueryString() + "<br>"); 23out.print("getProtocol:" + request.getProtocol() + "<br>"); 24out.print("getContextPath:"+request.getContextPath() + "<br>"); 25out.print("getPathInfo:" + request.getPathInfo() + "<br>"); 26out.print("getServletPath:"+request.getServletPath() + "<br>"); 27out.print("getRemoteAddr:" + request.getRemoteAddr() + "<br>"); 28out.print("getRemoteHost:" + request.getRemoteHost() + "<br>"); 29out.print("getRemotePort:" + request.getRemotePort() + "<br>"); 30out.print("getLocalAddr:" + request.getLocalAddr() + "<br>"); 31out.print("getLocalName:" + request.getLocalName() + "<br>"); 32out.print("getLocalPort:" + request.getLocalPort() + "<br>"); 33out.print("getServerName:" + request.getServerName()+ "<br>"); 34out.print("getServerPort:" + request.getServerPort()+ "<br>"); 35out.print("getScheme:" + request.getScheme() + "<br>"); 36out.print("getRequestURL:" + request.getRequestURL() + "<br>"); 37} 38protected void doPost(HttpServletRequest request, 39HttpServletResponse response) throws ServletException, IOException { 40doGet(request, response); 41} 42} 启动Tomcat服务器,在浏览器的地址栏输入“http://localhost:8080/myservlet/RequestLineServlet”,向RequestLineServlet发送请求,运行结果如图310所示。 图310RequestLineServlet的运行结果 2. 获取请求头的相关方法 请求头可以用来向服务器传递附加的请求信息。如客户端可以接收的数据类型、压缩方式、语言等。为此,HttpServletRequest接口中定义了一系列用于获取HTTP请求头字段的方法,如表34所示。 表34获取请求头的相关方法 方 法 声 明说明 String getHeader(String name)获取一个指定头字段的值,如果请求消息中没有包含指定的头字段,则返回null; 如果请求消息中包含多个指定名称的头字段,则返回其中第一个头字段的值 Enumeration getHeaders(String name)获取指定名称的头字段的所有值 Enumeration getHeaderNames()获取一个包含所有头字段名称的枚举对象 long getDateHeader(String name)获取指定头字段的值,并将其按GMT时间格式转换成一个代表日期/时间的长整数,这个长整数是自1970年1月1日0时0分0秒算起的以毫秒为单位的值 int getIntHeader(String name)获取指定的头字段的值,并将其值转换为int类型。如果指定的名称不存在,则返回-1; 如果获取的字段值无法转换为int类型,则抛出NumberFormatException异常 String getContentType()获取ContentType头字段的值 int getContentLength()获取ContentLength头字段的值 String getCharacterEncoding()获取请求消息的实体部分的字符集编码,通常从ContentType头字段中进行提取 下面,通过一个案例来演示这些方法的使用。在com.example.servlet.request包中创建一个名为RequestHeaderServlet的类,该类中编写了用于获取请求头中相关信息的方法。 【例34】获取请求头信息。代码如文件36所示。 【文件36】RequestHeaderServlet.java 1package com.example.servlet.request; 2 3import java.io.IOException; 4import java.io.PrintWriter; 5import java.util.ArrayList; 6import java.util.Collections; 7import java.util.Enumeration; 8 9import jakarta.servlet.ServletException; 10import jakarta.servlet.annotation.WebServlet; 11import jakarta.servlet.http.HttpServlet; 12import jakarta.servlet.http.HttpServletRequest; 13import jakarta.servlet.http.HttpServletResponse; 14 15@WebServlet("/RequestHeaderServlet") 16public class RequestHeaderServlet extends HttpServlet { 17 18public void doGet(HttpServletRequest request, 19HttpServletResponse response) 20throws ServletException, IOException { 21response.setContentType("text/html;charset=utf-8"); 22PrintWriter out = response.getWriter(); 23//获取请求消息中的所有头字段 24Enumeration headerNames = request.getHeaderNames(); 25//使用Lambda表达式遍历所有请求头, 26//并通过getHeader()方法获取一个指定名称的头字段 27ArrayList<String> list = (ArrayList<String>) 28Collections.list(headerNames); 29list.forEach((name) -> out.write(name+" : " 30+request.getHeader(name)+"<br>")); 31} 32public void doPost(HttpServletRequest request, 33HttpServletResponse response) 34throws ServletException, IOException { 35doGet(request, response); 36} 37} 启动Tomcat服务器,打开浏览器的开发者工具窗口,在浏览器的地址栏输入 “http://localhost:8080/myservlet/RequestHeaderServlet”向RequestHeaderServlet发送请求,使用浏览器的开发者工具查看请求头信息,并与程序运行结果比对,如图311所示。 图311查看请求头信息 3. 获取请求消息体的相关方法 HttpServletRequest接口的父接口ServletRequest定义了一系列获取请求参数和请求属性的方法,如表35所示。 表35获取请求参数和请求属性的相关方法 方 法 声 明说明 String getParameter(String name)获取指定名称的请求参数的值,如果请求消息中没有指定名称的参数,则返回null; 如果指定名称的参数存在但没有设置值,则返回空串; 如果请求消息中包含多个该名称指定的参数,则返回第一个出现的参数值 String[] getParameterValues(String name)获取同一个参数名对应的所有参数值 Enumeration getParameterNames()获取请求消息中所有参数的名字 Map getParameterMap()将请求消息中的所有参数及其值封装为一个Map对象并返回该Map对象 void setAttribute(String name,Object obj)将一个对象obj和一个名字name关联后存放在ServletRequest对象中 Object getAttribute(String name)从ServletRequest对象中获取指定名称的属性对象 void removeAttribute(String name)从ServletRequest对象中删除指定名称的属性对象 Enumeration getAttributeNames()获取ServletRequest对象中所有的属性名 下面,通过一个案例来演示这些方法的使用。创建一个Servlet,用来获取用户填写的表单内容并显示。 【例35】获取请求参数信息。用户在表单中填写姓名、性别并选择爱好,并将上述信息提交给服务器(Servlet)。Servlet处理后将上述信息输出。可以按以下步骤完成此任务。 1) 创建JSP文件 在src/main/webapp文件夹下创建一个form.jsp表单文件,要求用户填写姓名、性别并选择爱好。代码如文件37所示。 【文件37】form.jsp 1<%@ page contentType="text/html; charset=UTF-8"%> 2<html> 3<body> 4<form action="RequestParamServlet" method="post"> 5姓名<input type="text" name="name" /><br> 6性别 7<input type="radio" name="gender" value="m" checked/>男 8<input type="radio" name="gender" value="f"/>女<br> 9爱好 10<input type="checkbox" name="hobby" value="0" />篮球 11<input type="checkbox" name="hobby" value="1" />足球 12<input type="checkbox" name="hobby" value="2" />游泳<br> 13<input type="submit" value="提交"/> 14</form> 15</body> 16</html> 2) 创建Serlvet 在com.example.servlet.request包中创建一个名为RequestParamServlet的类,获取请求参数,代码如文件38所示。 【文件38】RequestParamServlet.java 1package com.example.servlet.request; 2 3import java.io.IOException; 4 5import jakarta.servlet.ServletException; 6import jakarta.servlet.annotation.WebServlet; 7import jakarta.servlet.http.HttpServlet; 8import jakarta.servlet.http.HttpServletRequest; 9import jakarta.servlet.http.HttpServletResponse; 10 11@WebServlet("/RequestParamServlet") 12public class RequestParamServlet extends HttpServlet { 13 14protected void doGet(HttpServletRequest request, 15HttpServletResponse response) 16throws ServletException, IOException { 17String name = request.getParameter("name"); 18System.out.println("姓名:" + name); 19String gender = request.getParameter("gender"); 20System.out.print("性别:"); 21System.out.println(gender.equals("m")?"男":"女"); 22// 获取参数名为"hobby"的值 23String[] hobbys = request.getParameterValues("hobby"); 24System.out.print("爱好:"); 25String[] hb = {"篮球","足球","游泳"}; 26for (int i = 0; i < hobbys.length; i++) { 27System.out.print(hb[Integer.parseInt(hobbys[i])] + ","); 28} 29} 30protected void doPost(HttpServletRequest request, 31HttpServletResponse response)throws ServletException, 32IOException { 33doGet(request, response); 34} 35} 启动Tomcat服务器,在浏览器的地址栏输入“http://localhost:8080/myservlet/form.jsp”,填写表单相关信息,如图312所示。 单击“提交”按钮后,可在控制台看到Servlet的输出信息,如图313所示。 图312填写表单信息 图313Servlet在控制台输出的信息 对于文件38有以下几点说明: (1) form.jsp中使用了<form>标签封装表单数据。当表单提交时,<form>标签中封装的内容会被作为请求参数自动提交给RequestParamServlet。这些请求参数的名字正是<form>标签中定义的控件的名字。 (2) 第17行和第19行分别用request.getParameter()方法获取姓名和性别参数的值。 (3) 参数hobby的值可能有多个,因此第23行使用getParameterValues()方法获取同名参数的多个值。通过遍历返回值数组,输出每个hobby参数对应的名称。 4. 请求转发器 一个HTTP请求可以被多个Servlet处理。例如,可以用一个Servlet实现请示文件的上传,用另一个Servlet实现文件批阅并生成最终的用户响应。要实现一个请求经由多个Servlet处理,需要用到请求转发器。Servlet中的请求转发器由RequestDispatcher(jakarta.servlet.RequestDispatcher)接口定义。可以通过HttpServletRequest接口提供的getRequestDispatcher()方法获取RequestDispatcher对象。getRequestDispatcher()方法的原型如下: RequestDispatcher getRequestDispatcher(String path) 该方法返回一个RequestDispatcher对象。其中参数path用于指定目标资源的路径, 借助这个路径,请求转发器可以将当前请求转发给目标资源。如果使用相对路径,则指相对于当前Servlet的路径; 也可以使用正斜线(/)开头的路径,表示相对于当前Web应用根目录的路径。 在获取到请求转发器RequestDispatcher对象以后,可以将当前请求通过请求转发器转发给目标资源继续处理。为此,RequestDispatcher接口提供了一个forward()方法,该方法可以将当前请求转发给其他Web资源。forward()方法的原型如下: void forward(ServletRequest request, ServletResponse response) throws ServletException, IOException forward()方法可以将当前请求转发给目标资源继续处理。需要注意的是,该方法必须在响应提交给客户端之前调用,否则会抛出IllegalStateException异常。请求转发的工作原理如图314所示。 图314请求转发的工作原理 如图314所示,当Servlet 1处理请求后,并不生成响应,而是将该请求转发给其他的Web资源继续处理(图314中的Servlet 2)。当Servlet 2处理请求后,生成响应并发送给客户端。注意,这里的请求和响应包含但不局限于HTTP请求和HTTP响应。了解到请求转发的工作原理后,下面通过一个案例演示请求转发器的应用。 【例36】请求转发器的应用。 分别创建两个名为RequestForwardServlet和RequestDestServlet的类。RequestForwardServelt接收到请求后将请求转发给RequestDestServlet。代码分别如文件39和文件310所示。 【文件39】RequestForwardServlet.java 1package com.example.servlet.request; 2 3import jakarta.servlet.ServletException; 4import jakarta.servlet.annotation.WebServlet; 5import jakarta.servlet.http.HttpServlet; 6import jakarta.servlet.http.HttpServletRequest; 7import jakarta.servlet.http.HttpServletResponse; 8import java.io.IOException; 9 10@WebServlet("/RequestForwardServlet") 11public class RequestForwardServlet extends HttpServlet { 12 13protected void doGet(HttpServletRequest request, 14HttpServletResponse response) 15throws ServletException, IOException { 16System.out.println("this is servlet 1"); 17request.setAttribute("name","com.example"); 18request.getRequestDispatcher("RequestDestServlet") 19.forward(request,response); 20} 21 22protected void doPost(HttpServletRequest request, 23HttpServletResponse response)throws ServletException, 24IOException { 25doGet(request, response); 26} 27} 【文件310】RequestDestServlet.java 1package com.example.servlet.request; 2 3import jakarta.servlet.ServletException; 4import jakarta.servlet.annotation.WebServlet; 5import jakarta.servlet.http.HttpServlet; 6import jakarta.servlet.http.HttpServletRequest; 7import jakarta.servlet.http.HttpServletResponse; 8import java.io.IOException; 9import java.io.PrintWriter; 10 11@WebServlet("/RequestDestServlet") 12public class RequestDestServlet extends HttpServlet { 13 14protected void doGet(HttpServletRequest request, 15HttpServletResponse response) 16throws ServletException, IOException { 17System.out.println("this is servlet 2"); 18String str = (String)request.getAttribute("name"); 19PrintWriter out = response.getWriter(); 20out.print("name is "+str); 21} 22 23protected void doPost(HttpServletRequest request, 24HttpServletResponse response)throws ServletException, 25IOException { 26doGet(request, response); 27} 28} 如文件39所示,请求首先到达RequestForwardServlet。该Servlet处理请求后在控制台输出字符串 “this is servlet 1”(第16行)。同时,RequestForwardServlet在该请求域的范围内追加了name属性(第17行)。执行了上述处理后,RequestForwardServlet并没有针对该请求生成响应,而是创建请求转发器对象(第18行),并调用请求转发器的forward()方法将请求转发给RequestDestServlet(第19行)。文件310描述了RequestDestServlet如何继续处理请求。为显示ReqeustDestServlet开始处理请求,首先在控制台输出字符串“this is servlet 2” (第17行)。在从请求域中取出name属性的值之后(第18行),RequestDestServlet调用out对象的print()方法生成对该请求的响应(第19~20行)。至此,请求被处理完毕。向RequestForwardServlet发送请求后,通过浏览器看到的响应内容如图315所示。 图315用浏览器查看响应内容 同时,控制台输出信息可以描述请求被两个Servlet处理的过程,如图316所示。 图316控制台输出的请求处理过程 3.4.2HttpServletResponse接口 在Jakarta Servlet API中定义了一个HttpServletResponse接口,它继承自ServletResponse接口,专门用来封装HTTP响应消息。由于HTTP响应消息分为状态行、响应消息头、消息体(实体主体)3部分。因此,HttpServletResponse接口定义了向客户端发送响应状态码、响应消息头、响应消息体的方法。 1. 发送响应状态码的方法 当Servlet向客户端发送响应消息时,需要在响应消息中设置状态码。为此,HttpServletResponse接口定义了两个发送状态码的方法,具体如下所述。 1) void setStatus(int status)方法 void setStatus(int status)方法用于设置HTTP响应消息的状态码。由于响应状态行中的状态描述信息与状态码直接相关,而HTTP版本由服务器确定。因此,只要通过setStatus(int status)方法设置状态码,即可发送状态行。正常情况下,服务器会默认产生一个状态码为200的状态行。合法的状态码范围为2**,3**,4**和5**,其中*表示一位非负整数。其他范围的状态码被视为属于特定容器的。 2) void sendError(int sc)throws IOException方法 void sendError(int sc) throws IOException方法用于发送表示错误信息的状态码。例如,404状态码表示找不到客户端请求的资源。其中的sc参数表示错误信息的状态码。此外,还有一个重载的方法: void sendError(int sc, String msg) throws IOException 这个方法除了发送错误信息的状态码外,还可以增加一条用于提示说明的文本信息,该文本信息将出现在发送给客户端的正文内容中。 2. 发送响应消息头的方法 HTTP有很多响应头字段,HttpServletResponse接口定义了一系列设置HTTP响应头的方法,如表36所示。 表36设置响应头字段的方法 方 法 声 明说明 void addHeader(String name, String value) void setHeader(String name, String value)用于设置HTTP响应头字段。参数name用于指定响应头字段的名称,参数value用于指定响应头字段的值。addHeader()方法用于增加同名的响应头字段,setHeader()方法用于覆盖同名的响应头字 段 void addIntHeader(String name, int value) void setIntHeader(String name, int value)用于设置包含整数值的响应头。这样可以避免在使用addHeader()和setHeader()方法时,需要将int类型的值转换为String类型的麻烦 void setContentLength(int len)用于设置响应消息的实体主体的大小,单位: 字节。对于HTTP来说,这个方法就是设置ContentLength响应头字段的值 void setContentType(String type)设置Servlet输出内容的MIME类型。对于HTTP来说,就是设置ContentType响应头字段的值。例如,如果发送到客户端的响应内容是jpeg格式的图像,则响应头字段类型设置为“image/jpeg”。如果响应的内容是文本,则设置响应类型并指定字符编码,如“text/html;charset=UTF8” void setLocale(Locale loc)设置响应消息的本地化信息。对于HTTP来说,就是设置ContentLanguage响应头字段和ContentType头字段的字符集编码部分 void setCharacterEncoding(String charset)设置输出内容使用的字符编码。对于HTTP来说,就是设置ContentType响应头字段的字符集编码部分。如果没有设置ContentType响应头字段,setCharacterEncoding()方法这时的字符集编码不会出现在HTTP响应消息中。setCharacterEncoding()方法比setContentType()和setLocale()方法的优先级高。它的设置结果将覆盖setContentType()和setLocale()方法所设置的字符编码 3. 发送响应消息体的方法 由于在HTTP响应消息中,大量的数据都是通过响应消息体传递的。因此,ServletResponse接口遵循以I/O流形式传递数据的理念来传送响应消息体。接口中定义了两个与输出流相关的方法。 1) ServletOutputStream getOutputStream() throws IOException ServletOutputStream getOutputStream() throws IOException方法可返回一个能够在响应中写入二进制数据的ServletOutputStream(字节流)对象。由于Servlet容器不编码二进制数据,要想输出二进制格式的响应正文,就需要使用getOutputStream()方法。 2) PrintWriter getWriter() throws IOException PrintWriter getWriter() throws IOException方法返回可以向客户端发送字符文本的PrintWriter(字符流)对象。PrintWriter使用getCharacterEncoding()方法返回的字符编码。如果getCharacterEncoding()方法返回默认值ISO88591,则getWriter() 方法会将其更新为ISO88591。 【例37】向客户端发送响应消息体。 在src/main/java文件夹下创建一个名为com.example.servlet.response的包; 在包中创建一个名为ResponseMsgServlet的类。该类使用上述两个方法发送响应消息体,代码如文件311所示。 【文件311】ResponseMsgServlet.java 1package com.example.servlet.response; 2 3import jakarta.servlet.ServletException; 4import jakarta.servlet.annotation.WebServlet; 5import jakarta.servlet.http.HttpServlet; 6import jakarta.servlet.http.HttpServletRequest; 7import jakarta.servlet.http.HttpServletResponse; 8import java.io.IOException; 9import java.io.OutputStream; 10 11@WebServlet("/ResponseMsgServlet") 12public class ResponseMsgServlet extends HttpServlet { 13 14protected void doGet(HttpServletRequest request, 15HttpServletResponse response) 16throws ServletException, IOException { 17String msg = "this is response"; 18OutputStream output = response.getOutputStream(); 19output.write(msg.getBytes()); 20} 21protected void doPost(HttpServletRequest request, 22HttpServletResponse response) throws ServletException, IOException { 23doGet(request, response); 24} 25} 启动Tomcat服务器,在浏览器的地址栏输入“http://localhost:8080/myservlet/ResponseMsgServlet”,浏览器显示的结果如图317所示。 图317输出响应消息 对于上述案例,可改用字符流形式输出响应消息,将第18行和第19行分别修改为: PrintWriter output = response.getWriter(); output.write(msg); 可以得到同样的运行结果。 提示: 虽然使用字符流和字节流都可以输出响应消息。但是,这两种方式不能同时使用。 4. HttpServletResponse接口的应用 1) 解决中文乱码 计算机中的数据都是以二进制形式存储的。当传输文本时,就会发生字符和字节之间的转换。字符和字节之间的转换是通过查编码表完成的,将字符转换成字节的过程叫编码,将字节转换成字符的过程叫解码。如果编码和解码使用的码表不一致,就会产生乱码。解决中文乱码问题的思路就是在Servlet生成响应消息前,通知浏览器使用与Servlet一致的码表来对收到的响应内容进行解码。 【例38】显示中文响应消息。 在com.example.servlet.response包中创建一个名为ResponseChineseCharServlet的类,在浏览器页面显示中文响应消息。代码如文件312所示。 【文件312】ResponseChineseCharServlet.java 1package com.example.servlet.response; 2 3import java.io.IOException; 4import java.io.PrintWriter; 5 6import jakarta.servlet.ServletException; 7import jakarta.servlet.annotation.WebServlet; 8import jakarta.servlet.http.HttpServlet; 9import jakarta.servlet.http.HttpServletRequest; 10import jakarta.servlet.http.HttpServletResponse; 11 12@WebServlet("/ResponseChineseCharServlet") 13public class ResponseChineseCharServlet extends HttpServlet { 14 15protected void doGet(HttpServletRequest request, 16HttpServletResponse response) 17throws ServletException, IOException { 18response.setContentType("text/html; charset=UTF-8"); 19String msg = "这是响应消息"; 20PrintWriter out = response.getWriter(); 21out.write(msg); 22} 23protected void doPost(HttpServletRequest request, 24HttpServletResponse response) throws ServletException, IOException { 25doGet(request, response); 26} 27} 如文件312所示,在Servlet发送响应数据前(第18行),用setContentType()方法设置Servlet响应的编码,通知浏览器使用同样的编码。启动Tomcat服务器,在浏览器的地址栏输入地址“http://localhost:8080/chapter3/ResponseChineseCharServlet”,浏览器可显示出正确的中文字符,如图318所示。 图318显示正确中文字符 2) 页面自动跳转 在Web开发中,经常会遇到定时跳转页面的需求。这个功能可以利用HTTP响应头中的refresh字段实现。它可以通知浏览器在指定的时间段内自动刷新并跳转到指定页面。 【例39】页面的定时刷新与自动跳转。 在com.example.servlet.response包中创建一个名为ResponseAutoJumpServlet的类,其功能是在收到请求 3s后跳转到百度主页。代码如文件313所示。 【文件313】ResponseAutoJumpServlet.java 1package com.example.servlet.response; 2 3import java.io.IOException; 4 5import jakarta.servlet.ServletException; 6import jakarta.servlet.annotation.WebServlet; 7import jakarta.servlet.http.HttpServlet; 8import jakarta.servlet.http.HttpServletRequest; 9import jakarta.servlet.http.HttpServletResponse; 10 11@WebServlet("/ResponseAutoJumpServlet") 12public class ResponseAutoJumpServlet extends HttpServlet { 13 14protected void doGet(HttpServletRequest request, 15HttpServletResponse response) 16throws ServletException, IOException { 17response.setHeader("Refresh","3;url=http://www.baidu.com"); 18} 19protected void doPost(HttpServletRequest request, 20HttpServletResponse response) throws ServletException, IOException { 21doGet(request, response); 22} 23} 如文件313所示,调用response对象的setHeader()方法来设置refresh响应头字段的值(第17行)。其中的3表示3s后发出下一个请求; url用于指定请求的目标资源。启动Tomcat服务器,在浏览器的地址栏输入“http://localhost:8080/myservlet/ResponseAutoJumpServlet”,给Servlet发送请求,等待3s后页面自动跳转到“百度”主页,如图319所示。 图319页面自动跳转到“百度”主页 3) 重定向 重定向是指Web服务器接收到客户端的请求后,由于某些限制,无法访问请求URL所指向的Web资源,而是指定一个新的资源,指示客户端给新的资源发送请求。这一过程发生在客户端和服务器之间,用户只知道发出了请求并收到响应,重定向的整个过程对用户透明。 为实现重定向,HttpServletResponse接口定义了一个sendRedirect()方法。该方法用于通知客户端重新访问指定的URL。sendRedirect()方法的原型如下。 public void sendRedirect(String location) throws IOException 其中,参数location指定重定向资源的URL。这个URL既可以用相对地址,也可以用绝对地址。图320说明了sendRedirect()方法实现重定向的过程。当客户端访问资源1时,资源1响应此请求,发出重定向响应,指示客户端向资源2发送请求。客户端在收到重定向响应后,根据响应给出的URL向资源2发送请求,资源2收到请求后将响应发送给客户端。对用户来讲,整个过程只有步骤1和步骤4可见,即发出一个请求,收到一个响应。这个过程与一般的请求响应过程相比好像毫无差异。实际上,这个过程是两次请求响应过程。 图320重定向的过程 【例310】利用重定向实现一个简单的登录验证程序。要求如下: 如果用户名和密码正确,则跳转到欢迎页面; 如果用户名或密码错误,则跳转回登录页面。实现步骤如下。 (1) 在src/main/webapp文件夹下创建两个jsp文件,一个用户登录页面login.jsp,一个欢迎页面welcome.jsp。login.jsp代码如文件314。welcome.jsp用于输出欢迎消息,代码略。 【文件314】login.jsp 1<%@ page contentType="text/html; charset=UTF-8"%> 2<html> 3<body> 4<form action="LoginServlet" method="post"> 5用户名: <input type="text" name="username" /><br> 6密 码: 7<input type="password" name="password" /><br> 8<input type="submit" value="登录" /> 9</form> 10</body> 11</html> (2) 在com.example.servlet.response包下创建一个名为LoginServlet的类,用于处理用户的登录请求,如文件315所示。 【文件315】LoginServlet.java 1package com.example.servlet.response; 2 3import jakarta.servlet.ServletException; 4import jakarta.servlet.annotation.WebServlet; 5import jakarta.servlet.http.HttpServlet; 6import jakarta.servlet.http.HttpServletRequest; 7import jakarta.servlet.http.HttpServletResponse; 8import java.io.IOException; 9 10@WebServlet("/LoginServlet") 11public class LoginServlet extends HttpServlet { 12 13protected void doGet(HttpServletRequest request, 14HttpServletResponse response) 15throws ServletException, IOException { 16response.setContentType("text/html;charset=utf-8"); 17//用HttpServletRequest对象的getParameter()方法获取用户名和密码 18String username = request.getParameter("username"); 19String password = request.getParameter("password"); 20//假设用户名和密码分别为admin和123 21if (("admin").equals(username) && ("123").equals(password)) { 22//如果用户名和密码正确,重定向到 welcome.jsp 23response.sendRedirect("welcome.jsp"); 24} else { 25//如果用户名和密码错误,重定向到login.jsp 26response.sendRedirect("login.jsp"); 27} 28} 29protected void doPost(HttpServletRequest request, 30HttpServletResponse response) throws ServletException, IOException { 31doGet(request, response); 32} 33} 如文件315所示,第23行和第26行调用sendRedirect()方法将请求分别重定向到welcome.jsp和login.jsp。此处,这两个JSP页面的URL都使用了相对路径。 (3) 启动Tomcat服务器,在浏览器的地址栏输入地址“http://localhost:8080/myservlet/login.jsp”。当输入的用户名(假定为admin)和密码(假定为123)都正确时,浏览器显示结果如图321所示。 图321登录成功时显示的结果 如果输入的用户名或密码错误,浏览器显示结果如图322所示。 图322登录失败时显示的结果 3.4.3ServletConfig接口和ServletContext接口 1. ServletConfig接口 在Servlet运行期间,经常需要一些配置信息。如文件的编码、使用Servlet程序的共享数据等。在初始化一个Servlet时,Servlet容器会将Servlet的配置信息封装到一个ServletConfig(jakarta.servlet.ServletConfig)对象中,并通过调用Servlet的init(ServletConfig config)方法将ServletConfig对象传递给Servlet以完成初始化。ServletConfig接口定义了一系列用于获取Servlet配置信息的方法,如表37所示。 表37ServletConfig接口用于获取Servlet配置信息的方法 方 法 声 明说明 String getInitParameter(String name)根据指定的名字获取初始化参数的值 Enumeration<String> getInitParameterNames()返回一个Enumeration对象,其中包含所有的初始化参数的名字 ServletContext getServletContext()返回一个代表当前Web应用的ServletContext对象 String getServletName()返回Servlet的名字 【例311】创建一个Servlet,利用ServletConfig接口获取Servlet的初始化参数。 在src/main/java目录下创建一个名为com.example.servlet.sc的包,并在该包中创建一个名为ServletConfigDemo的类,代码如文件316所示。 【文件316】ServletConfigDemo.java 1package com.example.servlet.sc; 2 3import java.io.IOException; 4import java.io.PrintWriter; 5 6import jakarta.servlet.ServletConfig; 7import jakarta.servlet.ServletException; 8import jakarta.servlet.annotation.WebInitParam; 9import jakarta.servlet.annotation.WebServlet; 10import jakarta.servlet.http.HttpServlet; 11import jakarta.servlet.http.HttpServletRequest; 12import jakarta.servlet.http.HttpServletResponse; 13 14@WebServlet(urlPatterns="/ServletConfigDemo", 15initParams = {@WebInitParam(name="param", value="Hello")}) 16public class ServletConfigDemo extends HttpServlet { 17 18protected void doGet(HttpServletRequest request, 19HttpServletResponse response) 20throws ServletException, IOException { 21PrintWriter out = response.getWriter(); 22//获取ServletConfig对象 23ServletConfig sc = this.getServletConfig(); 24String param = sc.getInitParameter("param"); 25out.println("param is "+param); 26} 27//此处省略了doPost()方法 28} 如文件316所示,第14~15行在@WebServlet注解中使用 initParams属性配置了一个名为 param的初始化参数,并设置其值为 Hello。当前Servlet类的父类GenericServlet已定义了getServletConfig()方法用于获取与当前Servlet对应的ServletConfig对象(第23行)。第24行调用getInitParameter()方法获取初始化参数的值。启动Tomcat服务器,在浏览器的地址栏输入 “http://localhost:8080/myservlet/ServletConfigDemo”,显示效果如图323所示。 图323获取初始化参数的显示效果 2. ServletContext接口 当Servlet容器启动时,会为每个Web应用创建一个唯一的ServletContext(jakarta.servlet.ServletContext)对象用以代表当前的Web应用。ServletContext对象不仅封装了当前Web应用的所有信息,而且可以实现多个Servlet之间的数据共享。ServletContext对象就是JSP的内置对象application。下面介绍ServletContext在3个方面的应用。 1) 实现多个Servlet之间的数据共享 一个Web应用程序对应一个ServletContext对象。而一个Web应用中可以包含多个Servlet。所以,ServletContext域的属性可以被该Web应用中所有的Servlet共享。ServletContext接口定义了用于增加、删除和设置ServletContext域属性的方法,如表38所示。 表38ServletContext接口的域属性操作方法 方 法 声 明说明 Object getAttribute(String name)根据指定的域属性的名字,返回对应的属性值 Enumeration<String> getAttributeNames()返回一个Enumeration对象,其中包含了存放在ServletContext中的所有属性名 void setAttribute(String name, Object object)将对象object与名字name绑定后添加到ServletContext域中 void removeAttribute(String name)根据参数指定的域属性的名字,从ServletContext中删除对应的属性 【例312】利用ServletContext对象实现两个Servlet之间的数据共享。 创建两个Servlet类,ServletContextParamSetter和ServletContextParamGetter。这两个Servlet分别调用ServletContext接口中的方法设置和获取域属性。代码分别如文件317和文件318所示。 【文件317】ServletContextParamSetter.java 1package com.example.servlet.sc; 2 3import jakarta.servlet.ServletContext; 4import jakarta.servlet.ServletException; 5import jakarta.servlet.annotation.WebServlet; 6import jakarta.servlet.http.HttpServlet; 7import jakarta.servlet.http.HttpServletRequest; 8import jakarta.servlet.http.HttpServletResponse; 9import java.io.IOException; 10 11@WebServlet("/ServletContextParamSetter") 12public class ServletContextParamSetter extends HttpServlet { 13 14protected void doGet(HttpServletRequest request, 15HttpServletResponse response) 16throws ServletException, IOException { 17ServletContext sc = this.getServletContext(); 18sc.setAttribute("data", "this is shared data"); 19} 20//此处省略了doPost()方法 21} 【文件318】ServletContextParamGetter.java 1package com.example.servlet.sc; 2 3//import部分与文件3-17相同,此处省略 4 5@WebServlet("/ServletContextParamGetter") 6public class ServletContextParamGetter extends HttpServlet { 7 8protected void doGet(HttpServletRequest request, 9HttpServletResponse response) 10throws ServletException, IOException { 11ServletContext sc = this.getServletContext(); 12String param = (String)sc.getAttribute("data"); 13System.out.println(param); 14} 15//此处省略了doPost()方法 16} 文件317中,第18行调用ServletContext 接口的setAttribute()方法用于在ServletContext域中设置属性data。文件318中,第12行调用ServletContext 接口的getAttribute()方法用于获取ServletContext对象的属性值。第13行在控制台输出获取到的共享数据。为了验证ServletContext对象能否实现Servlet之间的数据共享,启动Tomcat服务器,在浏览器的地址栏输入 “http://localhost:8080/myservlet/ServletContextParamSetter”,首先将共享数据存入ServletContext域中,然后在浏览器的地址栏输入“http://localhost:8080/myservlet/ServletContextParamGetter”,控制台的输出结果如图324所示。从控制台的输出可以确认,利用ServletContext能够实现Servlet间的数据共享。 图324控制台的输出结果 2) 获取Web应用程序的初始化参数 在web.xml文件中,除了可以配置Servlet的初始化信息,还可以配置整个Web应用程序的初始化信息。利用web.xml文件配置Web应用程序的初始化参数的格式如下: <context-param> <param-name>参数名</param-name> <param-value>参数值</param-value> </context-param> 其中,<contextparam>元素是根元素<webapp>的直接子元素,并且<contextparam>元素可以出现多次。<contextparam>的子元素<paramname>和<paramvalue>分别用于指定初始化参数的名字和值。可以通过调用ServletContext接口的getInitParameterNames()和getInitParameter()方法获取初始化参数的名字和值。 【例313】利用ServletContext接口获取Web应用程序的初始化参数name和address的值。其中,web.xml文件的内容如文件319所示。 【文件319】web.xml 1<context-param> 2<param-name>name</param-name> 3<param-value>com.example</param-value> 4</context-param> 5<context-param> 6<param-name>address</param-name> 7<param-value>Beijing China</param-value> 8</context-param> 在包com.example.servlet.sc中创建名为ServletContextInitParam的类,用于读取Web应用程序的初始化参数,代码如文件320所示。 【文件320】ServletContextInitParam.java 1package com.example.servlet.sc; 2//import部分略 3 4@WebServlet("/ServletContextInitParam") 5public class ServletContextInitParam extends HttpServlet { 6protected void doGet(HttpServletRequest request, 7HttpServletResponse response) 8throws ServletException, IOException { 9PrintWriter out = response.getWriter(); 10ServletContext sc = this.getServletContext(); 11Enumeration paramNames = sc.getInitParameterNames(); 12ArrayList<String> names = (ArrayList<String>) 13Collections.list(paramNames); 14names.stream().forEach((name) -> out.println(name+" : " 15+sc.getInitParameter(name))); 16} 17//此处省略了doPost()方法 18} 文件320的第11行调用ServletContext接口的getInitParameterNames()方法获取Web应用程序的所有初始化参数的名字,并在第14~15行调用getInitParameter()方法根据名字获取对应参数的值。启动Tomcat服务器,在浏览器的地址栏输入“http://localhost:8080/myservlet/ServletContextInitParam”,可在浏览器界面看到获取的初始化参数的名字和值,其运行结果如图325所示。 图325运行结果 3) 获取Web应用程序的资源文件 在项目开发中,有时需要读取Web应用程序的资源文件,如图片、配置文件等。为此,ServletContext接口定义了一些获取Web资源的方法。这些方法依靠Web容器来实现。Web容器根据资源文件的相对路径,返回资源文件的IO流、资源文件在文件系统中的绝对路径等。ServletContext接口用于获取资源路径的方法如表39所示。 表39ServletContext用于获取资源路径的方法 方 法 声 明说明 Set<String> getResourcePaths (String path)返回的集合对象中包含资源目录中子目录和文件的路径名称。参数path必须以正斜线(/)开始,指定匹配资源的部分路径 String getRealPath(String path)返回资源文件服务器的文件系统上的真实路径(绝对路径)。参数path代表资源文件的虚拟路径,以正斜线(/)开始,(/)表示当前Web应用的根目录。如果Web容器不能将虚拟路径转换为文件系统的真实路径,则返回null URL getResource(String path) throws MalformedURLException返回映射到某个资源文件的URL对象,参数path必须以正斜线(/)开始 InputStream getResourceAsStream (String path)返回映射到某个资源文件的InputStream输入流,参数path的传递规则与getResource()方法完全一致 【例314】利用ServletContext接口读取Web应用程序的资源文件。具体步骤如下: (1) 在src/main/java文件夹下创建一个名为sc.properties的文件,其配置信息如下: name = com.example address = Beijing China (2) 创建一个名为ServletContextResourceFile的类,代码如文件321所示。 【文件321】ServletContextResourceFile.java 1package com.example.servlet.sc; 2 3//import部分略 4@WebServlet("/ServletContextResource") 5public class ServletContextResourceFile extends HttpServlet { 6 7protected void doGet(HttpServletRequest request, 8HttpServletResponse response) 9throws ServletException, IOException { 10PrintWriter out = response.getWriter(); 11ServletContext sc = this.getServletContext(); 12InputStream is = sc.getResourceAsStream("/WEB-INF 13/classes/sc.properties"); 14Properties pros = new Properties(); 15pros.load(is); 16out.println("name = "+pros.getProperty("name")); 17out.println("address = "+pros.getProperty("address")); 18} 19//此处省略了doPost()方法 20} 项目的资源文件会最终存放在WEBINF/classes目录下。在文件321的第12、13行指定资源文件最终的存放路径,并调用getResourceAsStream()方法获得资源文件的输入流对象。 (3) 启动Tomcat服务器,在浏览器的地址栏输入“http://localhost:8080/myservlet/ServletContextResource”,可以在浏览器页面查看资源文件中配置的参数,运行结果如图326所示。 图326运行结果 3.5会话跟踪技术 〖*2〗3.5.1会话概述 日常生活中,从电话接通到电话挂断之间的通话过程就是会话。Web应用中的会话过程类似于打电话。它是指一个客户端和Web服务器之间连续发生的一系列请求和响应过程。例如,一个客户在某电商网站上购物的过程就是会话。一般来讲,客户端和服务器间的会话会产生一些数据,特定用户会话产生的数据应从属于该用户。即A用户在会话过程中产生的数据应从属于A的会话,B用户在会话过程中产生的数据应从属于B的会话。 会话跟踪是Web应用程序开发中常用的技术。它是一种维护用户状态的方法。Web应用程序是使用HTTP传输数据的。HTTP是无状态协议,客户端每次向服务器发出的请求都会被服务器认为是新的请求。因此,服务器无法通过请求来维护用户状态以及识别特定用户。这就需要采用会话跟踪技术。常用的会话跟踪技术有Cookie和session。 3.5.2Cookie Cookie是由W3C(World Wide Web Consortium,万维网联盟)提出的一种会话跟踪技术,是一小段文本信息。它可以将会话过程中产生的数据保存到客户端(如浏览器)。当用户通过浏览器第一次给服务器发送请求时,服务器会给客户端发送一些信息,如用户标识等,这些信息会保存在Cookie中。当再次向服务器发送请求时,浏览器会将请求和Cookie一同提交给服务器。服务器检查Cookie,以此来辨别用户的状态。 服务器向客户端发送Cookie时,会在HTTP响应中增加SetCookie响应头字段。在SetCookie响应头字段中设置Cookie的案例如下: Set-Cookie : user=admin; Path = / ; 在上述示例中,user表示Cookie的名称,admin表示Cookie的值,Path表示Cookie的属性。Cookie必须以键值对的形式存在,Cookie属性可以有多个,属性之间用分号“;”和空格分隔。一般来讲,Cookie会以文件的形式存放在客户端硬盘上; 也有一种形式的Cookie是存放在客户端内存的,当用户关闭浏览器时即失效,这种Cookie称为会话Cookie 。Cookie在浏览器和服务器间的传输过程如图327所示。 图327Cookie在浏览器和服务器间的传输过程 为了封装Cookie信息,Servlet API提供了jakarta.servlet.http.Cookie类。该类包含创建Cookie和提取Cookie信息的一系列方法。Cookie类的常用方法如表310所示。 表310Cookie类的常用方法 〖XB,HT5”SS;Y2<续表>〗 方 法 声 明说明 public Cookie(String name,String value)构造方法。参数name用于指定Cookie的名字,参数value用于指定Cookie的值 public void setComment(String purpose)用于设置Cookie的注释部分 public String getComment()用于返回Cookie的注释部分 public void setMaxAge(int expiry)设置Cookie在浏览器客户机上的生存时间,单位: s。参数expiry取大于0的整数,表示Cookie的最大生存时间; 取0表示删除该Cookie; 取负数表示不存储该Cookie,关闭浏览器时即删除Cookie(会话Cookie) public int getMaxAge()返回Cookie在浏览器客户机上的生存时间,单位: s public void setPath(String uri)设置Cookie的有效路径 public String getPath()返回Cookie的有效路径 public void setSecure(boolean flag)设置该Cookie是否只能使用安全协议(如HTTPS或SSL)传送 public boolean getSecure()返回该Cookie是否只能使用安全协议传送 public String getName()返回Cookie的名称 public void setValue(String newValue)为Cookie设置新的值 public String getValue()返回Cookie的值 public int getVersion()返回此Cookie遵守的协议版本 public void setVersion(int v)设置该Cookie采用的协议版本 【例315】利用Cookie记录用户的登录IP。如果用户是第一次登录,显示欢迎消息,并将其IP地址写入Cookie中。如果用户在Cookie有效期内再次登录,显示欢迎消息和上次登录时的IP地址。 实现思路: 在客户端给Servlet发送请求时,Servlet记录客户端的IP地址,并将IP地址以Cookie的形式发送给客户端。当客户端再次访问Servlet时,Servlet在获取客户端的全部Cookie后,查找自己需要的Cookie并从该Cookie中取出IP地址。 在src/main/java目录下创建名为com.example.servlet.cookie的包,并在该包中创建名为CookieDemoServlet的类,代码如文件322所示。 【文件322】CookieDemoServlet.java 1papackage com.example.servlet.cookie; 2//import部分略 3 4@WebServlet("/CookieDemoServlet") 5public class CookieDemoServlet extends HttpServlet { 6 7protected void doGet(HttpServletRequest request, 8HttpServletResponse response) 9throws ServletException, IOException { 10PrintWriter out = response.getWriter(); 11Cookie[] cookies = request.getCookies(); 12response.setContentType("text/html;charset=UTF-8"); 13boolean flag = false; 14//判断能否获取到Cookie 15if(cookies!=null) { 16for(Cookie c:cookies) { 17if("last".equals(c.getName())) { 18out.write("欢迎回来,上次访问的IP为: "+c.getValue()); 19flag = true; 20break; 21} 22} 23} 24if(!flag) { 25out.write("第一次访问,欢迎"); 26String ipAddress = request.getRemoteAddr(); 27Cookie cookie = new Cookie("last",ipAddress); 28cookie.setMaxAge(3*60); 29//将Cookie发送到客户端 30response.addCookie(cookie); 31} 32} 33//此处省略了doPost()方法 34} 如文件323所示,第11行通过request对象获取客户端所有的Cookie,注意返回类型是一个Cookie数组。如果客户端已来访,Servlet从客户端提交的Cookie中查找名为last的Cookie(第17行)。在找到需要的Cookie后,Servlet将Cookie中存储的IP地址取出并生成响应(第18行)。如果客户端第一次访问服务器,服务器无法获取到需要的Cookie。因此,会输出欢迎消息(第25行),获取客户端IP地址(第26行),生成Cookie并发送到客户端(第27~30行)。本例中设置的Cookie的生存时间是3min。 启动Tomcat服务器,在浏览器的地址栏输入“http://127.0.0.1:8080/myservlet/CookieDemoServlet”。由于是第一次访问,可以在浏览器中看到“第一次访问,欢迎”字样,如图328所示。刷新浏览器页面后,是第二次访问,可以看到 图329所示的显示结果。由于本例设置的Cookie的生存时间是3min。因此,在发送第二次请求后等待3min,再次刷新浏览器页面,此时Cookie已经被移除,会看到图328所示的显示效果。 图328第一次运行的结果 图329第二次运行的结果 3.5.3session Cookie技术可以将用户的信息保存在各自的客户端,并且可以在请求域内实现数据共享。但是,随着传输信息量的增加,服务器端的负载也会增加。为此,Servlet提供了另一种会话跟踪技术——session。session是服务器维护的一个数据结构,可以来跟踪用户的状态。 当浏览器访问Web应用时,Web容器会创建一个session对象,其中包括了session对象的标识——id属性。当客户端后续访问服务器时,只要将id传递给服务器,服务器就能判断出该请求是哪个客户端发送的,从而选择与之对应的session对象为其服务。session对象的工作原理如图330所示。 图330session对象的工作原理 如图330所示,浏览器第一次向Servlet发送请求时,Web容器会创建一个与当前请求相关的session对象,用以保存客户端信息。同时,服务器会将session对象的id属性以Cookie的形式(SetCookie: JSESSIONID=***)发送给客户端,其中***表示id的值。客户端保存此id。当再次向Servlet发送请求时,浏览器会自动在请求消息头中将Cookie(Cookie:JSESSIONID=***)信息发送给服务器,服务器根据id属性查找对应的session对象。如果无法找到session,则创建一个新的session对象。 关于session的其他说明如下。 1. session的创建 session对象是在客户端第一次访问服务器时创建的。准确来讲,只有当客户端访问JSP、Servlet等动态资源时才会创建session。此外,也可以调用HttpServletRequest接口的getSession()方法创建session对象。只访问HTML、图片等静态资源时不会创建session。 2. session的生存时间 为避免session过多,大量占用服务器的内存空间,需要设置session的生存时间。超过该时间,session对象会自动从Web容器的内存中删除。Tomcat默认的session超时时间是30min,即从用户最后一次发出请求开始计时,30min内没有再次发出请求,则判断session超时。超时的session会成为垃圾对象,等待垃圾回收器将其从内存中彻底清除。设置session对象的超时时间共有 3种方式。 (1) 在部署描述文件web.xml中设置,这种方式只对当前Web应用程序有效。例如,设置session的超时时间为2min,代码如下: <session-config> <session-timeout>2</session-timeout> </session-config> (2) 修改Web容器的默认设置,这种方式对部署在Web容器上的所有Web应用程序都有效。以Tomcat为例,修改Tomcat的conf目录下的web.xml文件中sessionconfig标签的值,默认值为30min。 <session-config> <session-timeout>30</session-timeout> </session-config> (3) 调用HttpSession接口的setMaxInactiveInterval()方法。 3. session的销毁 只要满足以下两个条件之一,session对象就会被销毁: ① session超时。 ② 程序中调用了HttpSession接口的invalidate()方法。 此外,如果采用会话Cookie来保存sessionid,当用户关闭浏览器时,Cookie消失,sessionid也随之消失。这样浏览器再次连接服务器时无法找到原来的session对象,但并不意味着原来的session对象消失。 session是与每个请求消息密切相关的。为此,HttpServletRequest接口定义了用于获取session对象的getSession()方法。具体如下: ① public HttpSession getSession(boolean create) ② public HttpSession getSession() 上面两个重载方法都可用于返回与当前请求相关的HttpSession对象。不同的是,第一个getSession()方法根据传递的参数判断是否创建新的HttpSession对象。如果参数为true,则在相关的HttpSession对象不存在时创建并返回新的HttpSession对象。如果参数为false,不创建新的HttpSession对象,而是返回null。第二个getSession()方法相当于第一个方法的参数为true时的情况,在相关的HttpSession对象不存在时总是创建新的HttpSession对象。需要注意的是,由于getSession()方法可能会产生发送会话标识号(sessionid)的Cookie头字段,所以,必须在发送任何响应内容之前调用getSession()方法。 要使用HttpSession对象管理会话,还需要了解HttpSession接口的相关方法。HttpSession接口中的常用方法如表311所示。 表311HttpSession接口的常用方法 方 法 声 明说明 void setAttribute(String name, Object value)将一个对象value与名称name绑定,并存放到HttpSession对象中 Object getAttribute(String name)从当前HttpSession对象中返回指定名称的属性值 void removeAttribute(String name)从当前HttpSession对象中删除指定名称的属性 String getId()返回与当前HttpSession对象关联的会话标识号 long getLastAccessedTime()返回客户端最后一次发送请求的时间,这个时间是发送请求的时间与1970年1月1日00:00:00之间的差,单位: ms void setMaxInactiveInterval(int interval)设置当前HttpSession对象可空闲的最长时间间隔,即修改当前会话的默认超时时间,单位: s long getCreationTime()返回HttpSession对象的创建时间,这个时间是与1970年1月1日00:00:00之间的时间差,单位: ms boolean isNew()判断当前HttpSession对象是否是新创建的 void invalidate()强制使HttpSession对象无效 ServletContext getServletContext()返回当前HttpSession对象所属的Web应用程序对象,即代表当前Web应用程序的ServletContext对象 【例316】完善例310的用户登录程序,用HttpSession对象保存用户的登录信息。如果可以成功登录,则将用户名存放到HttpSession对象中; 如果登录失败,在给出错误提示信息3s后,跳转到登录页。 其中,登录页面login.jsp内容与例310的内容相同。文件welcome.jsp的<body>部分改为: 欢迎<%= session.getAttribute("name") %>,登录成功! <a href="LogoutServlet">退出</a> 对于LoginServlet,改名为SessionDemoServlet,修改后的代码如文件323所示。 【文件323】SessionDemoServlet.java 1package com.example.servlet.cookie; 2 3//import部分此处略 4@WebServlet("/SessionDemoServlet") 5public class SessionDemoServlet extends HttpServlet { 6 7protected void doPost(HttpServletRequest request, 8HttpServletResponse response) 9throws ServletException, IOException { 10response.setContentType("text/html;charset=utf-8"); 11//用HttpServletRequest对象的getParameter()方法 12//获取用户名和密码 13String username = request.getParameter("username"); 14String password = request.getParameter("password"); 15//假设用户名和密码分别为: admin和123 16if (("admin").equals(username)&&("123").equals(password)) { 17//获取HttpSession对象 18HttpSession session = request.getSession(); 19//将用户名追加到session中 20session.setAttribute("name", username); 21//如果用户名和密码正确,重定向到 welcome.jsp 22response.sendRedirect("welcome.jsp"); 23} else { 24PrintWriter out = response.getWriter(); 25out.print("用户名或密码错误,返回<a href=\'login.jsp\'>登录</a>"); 26//如果用户名和密码错误,重定向到login.jsp 27response.setHeader("refresh", "3;url=login.jsp"); 28} 29} 30//此处省略了doGet()方法 31} 同时,编写LogoutServlet,用于实现用户的注销功能。代码如文件324所示。 【文件324】LogoutServlet.java 1package com.example.servlet.cookie; 2 3//import部分略 4@WebServlet("/LogoutServlet") 5public class LogoutServlet extends HttpServlet { 6 7protected void doGet(HttpServletRequest request, 8HttpServletResponse response) 9throws ServletException, IOException { 10HttpSession session = request.getSession(); 11//移除session中保存的属性name 12session.removeAttribute("name"); 13//强制作废session对象 14session.invalidate(); 15//重定向到login.jsp 16response.sendRedirect("login.jsp"); 17} 18//此处省略了doPost()方法 19} 启动Tomcat服务器,在浏览器的地址栏输入“http://localhost:8080/myservlet/login.jsp”访问login.jsp,填写用户名(admin)和密码(123)后,可以看到登录后的结果如图331所示。单击“退出”超链接,页面会返回登录页。 图331登录成功的页面显示 如果输入的用户名或密码错误,登录失败,浏览器显示结果如图332所示。在此消息显示3s后,页面会自动返回登录页。 图332登录失败的页面显示