第5章
请求和响应






在客户端浏览器发送一个请求后,服务器执行一系列操作,并作出一个响应,发送给客户端,即完成一次Web访问过程。客户端浏览器的请求被封装成一个HttpServletRequest对象,简称request对象。服务器端对客户端浏览器作出的响应被封装成一个HttpServletResponse 对象,简称response对象。request对象和 response 对象起到了服务器端与客户端之间的信息传递作用。request 对象用于接收客户端浏览器提交的数据,而 response 对象的功能则是将服务器端的数据发送到客户端浏览器。
通过本章的学习,您可以: 
(1) 掌握HttpServletResponse接口及其常用方法的使用; 
(2) 掌握HttpServletRequest接口及其常用方法的使用; 
(3) 掌握请求和响应过程中出现中文乱码问题的解决方法; 
(4) 掌握请求转发和请求重定向的使用。
浏览器并不是直接与Servlet通信的,而是通过Web服务器和Servlet容器与Servlet通信。针对Servlet的每次请求,Servlet容器在调用service()方法之前,都会创建request对象和response对象; 然后调用相应的 Servlet 程序。在 Servlet 程序运行时,它首先会从request对象中读取数据信息,再通过service()方法处理请求消息,并将处理后的响应数据写入到 response 对象中; 最后,Web服务器会从response对象中读取到响应数据,并发送给浏览器。浏览器访问Servlet的过程示意如图51所示。



图51浏览器访问Servlet的过程示意


注意: 在 Web 服务器运行阶段,每个Servlet都会创建一个实例对象,针对每次的HTTP 请求,Web 服务器都会调用所请求 Servlet 实例的 service(HttpServletRequest request,HttpServletResponse response)方法,并重新创建一个 request 对象和一个 response 对象。
5.1HttpServletResponse接口及其应用
5.1.1HttpServletResponse接口



视频讲解


在Servlet API中定义了一个HttpServletResponse接口,它位于javax.servlet.http包中,继承自javax.servlet.ServletResponse接口,主要用于封装HTTP响应消息。由于HTTP响应消息分为响应状态行、响应消息头和响应消息体3部分,因此在HttpServletResponse接口中定义了向客户端发送响应状态码、响应消息头和响应消息体的方法。








1. 发送与状态码相关的方法
当 Servlet 向客户端回送响应消息时,需要在响应消息中设置响应状态行。响应状态行由HTTP版本、状态码和状态描述信息3部分构成。由于状态描述信息直接与状态码相关,而 HTTP 版本由服务器确定,因此,只要设置状态码即可。HttpServletResponse接口定义了setStatus()和sendError()两种发送状态码的方法。
1) setStatus()方法
该方法用于设置 HTTP 响应消息的状态码,并生成响应状态行。需要注意的是,在正常情况下,Web 服务器会默认产生一个状态码为 200 的状态行。完整的语法格式如下: 



void setStatus(int sc)




整型参数sc为状态码。
【例51】发送状态码302。
创建项目ch5_demo,创建包com.yzpc.servlet,在包中创建Servlet,命名为TestSetvlet,设置虚拟路径为“/TestSetvlet”,重写Servlet的doGet()方法,在doGet()方法中调用 setStatus()方法。TestSetvlet.java文件代码如文件51所示。
文件51TestSetvlet.java文件代码



1package com.yzpc.servlet;

2import java.io.IOException;

3import javax.servlet.ServletException;

4import javax.servlet.annotation.WebServlet;

5import javax.servlet.http.HttpServlet;

6import javax.servlet.http.HttpServletRequest;

7import javax.servlet.http.HttpServletResponse;

8@WebServlet("/TestServlet")

9public class TestServlet extends HttpServlet {

10private static final long serialVersionUID = 1L;

11protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

12response.setStatus(302);//发送状态码302

13}

14}




发布项目,启动服务器。打开Chrome浏览器,打开开发者工具。在浏览器地址栏中输入http://localhost: 8080/ch5_demo/TestServlet。观察开发者工具窗口,可见响应状态行信息为HTTP/1.1 302。setStatus()方法运行结果如图52所示。



图52setStatus()方法运行结果


2) sendError()方法
该方法用于发送表示错误信息的状态码。response对象提供了两个重载的发送错误信息的状态码的方法,代码如下: 



1public void sendError(int code) throws java.io.IOException

2public void sendError(int code,String message)throws java.io.IOException




在上面重载的两种方法中,第一种方法只发送错误信息的状态码,而第二种方法除了发送状态码以外,还可以增加一条用于提示说明的文本信息,该文本信息将出现在发送给客户端的正文内容中。 
用如下代码替换文件51中第12行代码。



response.sendError(404);




运行后显示404错误提示页面,对应的响应状态行为HTTP/1.1 404。sendError()方法运行结果如图53所示。



图53sendError()方法运行结果


sendError()方法适用于报错且存在对应的报错页面配置作为输出显示的情况,如404、500等错误页面的发送; 而setStatus()方法适用于正常响应的情况,仅仅可以改变响应状态码。
3) 响应状态码对应的常量
HttpServletResponse接口中定义了很多状态码的常量,当需要向客户端发送响应状态码时,可以使用这些常量,避免了直接写数字。常见响应状态码的常量如表51所示。


表51常见响应状态码的常量




静 态 常 量状态码表 示 意 义
static int SC_CONTINUE100指示客户端可以继续
static int SC_OK200指示请求正常
static int SC_MULTIPLE_CHOICES300指示所请求的资源对应于一组表示中的任何一个,每个表示具有其自己的特定位置
static int SC_FOUND302指示资源暂时驻留在不同的URI下
static int SC_NOT_MODIFIED304指示条件GET操作发现资源可用且未被修改
static int SC_NOT_FOUND404指示所请求资源不可用
static int SC_INTERNAL_SERVER_ERROR500指示HTTP服务器内部的错误,从而阻止其履行请求

2. 发送与响应消息头相关的方法
Servlet向客户端发送的响应消息中包含响应头字段,由于 HTTP的响应头字段有很多种,因此,HttpServletResponse 接口定义了一系列设置 HTTP 响应头字段的方法,既有通用的设置响应头字段的方法,也有设置特定响应头字段的方法。下面介绍几种常用的方法。
1) 通用的设置响应头字段的方法



1void setHeader(String name, String value); 

2void addHeader(String name, String value);

3void setIntHeader(String name, int value); 

4void addIntHeader(String name, int value);




说明: 
(1) setHeader()方法和addHeader()方法用于设定HTTP的响应头字段,参数name用于指定响应头字段的名称,String类型; 参数value用于指定响应头字段的值, String类型。
(2)  setIntHeader()方法和addIntHeader()方法与setHeader()方法和addHeader()方法的功能相似。不同的是,这两种方法只适用于响应头字段的值为int类型时的响应消息头的设置。
(3) addHeader()方法和addIntHeader()方法允许增加同名的响应头字段的值; 而setHeader()方法和setIntHeader()方法则会覆盖同名的响应头字段的值。
设置refresh头字段可以实现页面的自动刷新,也可以设置定时跳转网页。
【例52】refresh头字段的应用。
(1) 在项目ch5_demo的com.yzpc.servlet包中创建RefreshServlet.java类,设置虚拟路径“/RefreshServlet”,重写doGet()方法。在doGet()方法中,为了便于观察刷新功能,设计一个计数器,刷新一次,计数器值便增加1。RefreshServlet.java类的doGet()方法如文件52所示。
文件52RefreshServlet.java类的doGet()方法



1protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

2PrintWriter out=response.getWriter();








3//利用ServletContext的域属性存储计数器count

4ServletContext context=this.getServletContext();//获取ServletContext对象

5Integer count=(Integer)context.getAttribute("count");//读取域属性count的值

6//计数,如果count为null,设count初始值为1,否则count增加1 

7if( count==null) {count=1;}else {count++;}

8context.setAttribute("count", count); //存储当前计数次数到域属性中

9out.print(count+"times. <br/>");//输出刷新次数

10out.print("vocational skills competition");

11//设置refresh头字段的值为3,表示3秒后刷新当前页面

12response.setHeader("refresh","3"); 

13}




在文件52中,利用ServletContext的域属性存储计数器变量count的值,第5行读取域属性值,第一次读的时候,域属性count是不存在的,返回值为null。第7行判断count变量的值是否为null,若为null,则说明是第一次访问页面,就设count变量初值为1,否则将count变量增加1。第8行将count变量的新值存储到域属性count中。第12行调用setHeader()方法,设置refresh头字段的值为3秒,表示每隔3秒刷新当前页面一次。
注意: count变量一定要使用包装类型,如Integer,不能是int。
每次刷新页面,都会发出一次新的HTTP请求,由于是当前页面刷新,因此会再次执行doGet()方法。ServletContext域属性的生命期是整个Web运行期间,因此在以后执行doGet()方法时,ServletContext域中已存储有count属性的有效值了。如此,通过ServletContext域属性实现了页面刷新计数的功能。
(2) 在WebContent根目录创建名为refresh.html的页面文件。其代码如下: 



1<!DOCTYPE html>

2<html>

3<head>

4<meta charset="GB18030">

5<title>刷新并跳转</title>

6</head>

7<body>

8<h2 align="center">职业技能大赛 </h2>

9<center>第一届职业技能大赛2020年12月10日在广东省广州市开幕。</center>

10</body> 

11</html>




(3) 启动服务器,在地址栏中输入地址http://localhost: 8080/ch5_demo/RefreshServlet运行RefreshServlet。页面每隔3秒就会刷新,页面上会显示计数刷新次数,且显示文本为文件52中发送的响应消息体。统计页面的刷新次数页面如图54所示。



图54统计页面的刷新次数页面


(4) 修改程序代码,实现定时跳转。
修改文件52的代码。修改后的代码如下: 



1{protected void doGet(HttpServletRequest request, HttpServletResponse response)

2PrintWriter out=response.getWriter();

3out.print("vocational skills competition");

4//设置refresh头字段的值为"3,URI=refresh.html",表示3秒后刷新当前页面,并跳转到refresh.html页面

5response.setHeader("refresh","3, URI=refresh.html");

6}




(5) 重启服务器,在地址栏中输入地址http://localhost: 8080/ch5_demo/RefreshServlet。首先显示RefreshServlet页面,3秒后跳转到refresh.html页面。定时跳转页面如图55所示。


图55定时跳转页面


注意: 当页面跳转时,浏览器地址栏中显示的是refresh.html,而不是输入的URL。
2) setContentType()方法
该方法用于设置发送到客户端的响应的内容类型,对应HTTP的contentType头字段。语法格式如下: 



void setContentType(String type)




字符串类型参数type指定内容类型,若发送到客户端的内容是.jpeg格式的图像数据,则使用“response.setContentType("image/jpeg");”语句; 若发送的是文本数据,字符编码规范为GB18030,则使用“response.setContentType("text/html; charset=GB18030");”语句。
3) setCharacterEncoding()方法
该方法用于设置输出内容使用的字符编码,对应HTTP的contentType头字段的字符集编码部分。如果没有设置contentType头字段,那么该方法设置的编码字符集不会出现在HTTP响应头中。语法格式如下: 



void setCharacterEncoding(String charset)




String类型参数charset指定编码字符集,如GB18030、GB2312、UTF8等。
setCharacterEncoding()方法和setContentType()方法都可用于设置字符编码,setCharacterEncoding()方法的优先权较高,它的设置结果将覆盖setContentType()所设置的字符编码集。这两种设置字符编码的方法可以有效解决出现中文乱码的问题。
注意: 这两种方法都必须在getWriter()方法之前调用,输出的内容才会按照指定的内容类型设置。
3. 发送与响应消息体相关的方法
由于在 HTTP 响应消息中,大量的数据都是通过响应消息体传递的,因此,HttpServletResponse接口的ServletResponse父接口中遵循以 I/O 流传递大量数据的设计理念。在发送响应消息体时,定义了两种与输出流相关的方法。
(1) getOutputStream() 方法。该方法所获取的字节输出流对象为 ServletOutputStream 类型。由于 ServletOutputStream是OutputStream 的子类,它可以直接输出字节数组中的二进制数据。因此,要想输出二进制格式的响应正文,就需要使用 getOutputStream() 方法。
(2) getWriter() 方法。该方法所获取的字符输出流对象为PrintWriter类型。由于PrintWriter类型的对象可以直接输出字符文本内容,因此,要想输出内容全部为字符文本的网页文档,就需要使用 getWriter()方法。
【例53】发送响应消息体。
在项目ch5_demo中创建Servlet并命名为MsgServlet,设置虚拟路径为“/MsgServlet”。
① 使用PrintWriter字符流发送响应消息体。重写MsgServlet的doGet()方法。MsgServlet.java的代码如文件53所示。
文件53MsgServlet.java的代码



1package com.yzpc.servlet;

2import java.io.IOException;

3import java.io.PrintWriter;//导入PrintWriter类对应的包

4import javax.servlet.ServletException;

5import javax.servlet.annotation.WebServlet;

6import javax.servlet.http.HttpServlet;

7import javax.servlet.http.HttpServletRequest;

8import javax.servlet.http.HttpServletResponse;

9@WebServlet("/MsgServlet")//配置虚拟路径

10public class MsgServlet extends HttpServlet {

11private static final long serialVersionUID = 1L;

12protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

13PrintWriter out=response.getWriter(); //获取PrintWriter对象

14out.print("Where there is a will, there is a way.");//输出

15}

16}




在文件53中使用response对象的getWriter()方法获得PrintWriter对象out,需要导入对应的包java.io.PrintWriter。
启动Tomcat服务器,在浏览器地址栏中输入http://localhost:8080/ch5_demo/MsgServlet,在页面上显示“Where there is a will, there is a way.”。发送消息体运行结果如图56所示。



图56发送消息体运行结果


② 使用OutputStream字节流发送响应消息体。重写文件53中的doGet()方法,代码如下: 



1protected void doGet(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException {

2String message="Where there is a will, there is a way.";

3OutputStreamout=response.getOutputStream();//获取OutputStream对象

4out.write(message.getBytes());

5}




上述代码中第3行使用response对象的getOutputStream()获取OutputStream对象。注意,需要导入包java.io.OutputStream; 第4行调用write()方法输出响应消息体; 由于参数message是字符串类型,第4行使用getBytes()方法将字符串转换成字节数组。
重启Tomcat服务器后,刷新浏览器,输出结果与PrintWriter流输出响应消息体一样,如图56所示。
③ 同时使用PrintWriter字符流和OutputStream字节流发送响应消息体。将文件53的doGet()方法修改为如下代码:



1protected void doGet(HttpServletRequest request, HttpServletResponse response)

2throws ServletException, IOException {

3String message="Where there is a will, there is a way.";

4OutputStreamout=response.getOutputStream();//获取OutputStream对象

5out.write(message.getBytes());

6PrintWriter out1=response.getWriter(); //获取PrintWriter对象

7out1.print("Where there is a will, there is a way.");//输出

8}




上述代码中第4行调用了response.getOutputStream()方法,第6行调用了response.getWriter()方法。重启服务器,刷新浏览器后,在控制台上显示IllegalStateException 异常。IllegalStateException 异常如图57所示。



图57IllegalStateException 异常


图57中发生异常的原因,是因为在Servlet调用response.getWriter()方法之前已经调用了response.getOutputStream()方法。虽然这两种方法都可以发送响应消息体,但它们之间互相排斥,不可同时使用; 如果同时使用,则会发生IllegalStateException异常。
5.1.2HttpServletResponse应用
1. 请求重定向



视频讲解



请求重定向是指 Web 服务器在接收到客户端的请求后,可能由于某些条件的限制,不能访问当前请求 URL 所指向的 Web 资源,而是指定了一个新的资源路径,须由客户端重新发送请求。在某些情况下,针对客户端的请求,一个Servlet类可能无法完成全部工作,可以使用请求重定向来继续完成这一工作。
为了实现请求重定向,HttpServletResponse 接口定义了一个 sendRedirect()方法,该方法用于生成302响应码和Location响应头,从而通知客户端重新访问Location响应头中指定的 URL。sendRedirect() 方法的完整语法如下: 



public void sendRedirect(String location) throws IOException




在上述方法代码中,参数 location 可以使用相对 URL,Web 服务器会自动将相对 URL 翻译成绝对 URL,再生成 Location 头字段。sendRedirect()方法的工作原理如图58所示。



图58sendRedirect()方法的工作原理


在图58中,当客户端访问Servlet1时,由于在Servlet1中调用了sendRedirect()方法将请求重定向到Servlet2,因此,浏览器收到Servlet1的响应消息后,立刻向Servlet2发送请求,Servlet2对请求处理完毕后,再将响应消息回送给客户端浏览器并显示。在这个过程中客户端与服务器端之间发生了两次请求,两次响应。
【例54】用户登录。
用户在登录页面输入用户名和密码,当用户名和密码都正确时,显示欢迎页面,否则返回到登录页面。
分析: 本例中需要创建一个登录页面login.html,一个欢迎页面welcome.html,一个判断用户名和密码是否正确的LoginServlet.java。步骤如下所述。
(1) 在项目ch5_demo的WebContent文件夹下,创建login.html文件。login.html代码如文件54所示。
文件54login.html代码



1<!DOCTYPE html>

2<html>

3<head>








4<meta charset="GB18030">

5<title>登录页面</title>

6</head>

7<body>

8<p>登录</p>

9<form action="login" method="POST">

10<label>姓名:</label> <input type="text" name="userName" /><br/>

11<label>密码:</label><input type="password" name="password"/></br>

12<input type="submit" value="提交"/>

13<input type="reset" value="重置"/>

14</form>

15</body>

16</html>




(2) 在WebContent文件夹下,创建welcome.html文件。<body>标签代码如下: 



1<body>

2<h2 align="center">全国职业院校技能大赛</h2>

3<p>全国职业院校技能大赛(简称大赛)是教育部发起并牵头,联合国务院有关部门以及有关行业、人民团体、学术团体和地方共同举办的一项公益性、全国性职业院校学生综合技能竞赛活动。每年举办一届。</p>

4</body>




(3) 在com.yzpc.servlet包中创建LoginServlet.java文件,设其虚拟路径为“/login”,重写doGet()方法。其代码如下: 



1public class LoginServlet extends HttpServlet {

2private static final long serialVersionUID = 1L;

3protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

4//用 HttpServletRequest 对象的 getParameter() 方法获取用户名和密码

5String username = request.getParameter("userName");

6String password = request.getParameter("password");

7//假设用户名和密码分别为 admin 和 123456

8if ("admin".equals(username) && ("123456").equals(password)) {

9//如果用户名和密码正确,重定向到 welcome.html

10response.sendRedirect("welcome.html");

11} else {

12//如果用户名和密码错误,重定向到 login.html

13response.sendRedirect("login.html");

14}

15}}




sendRedirect()方法的参数可以使用相对路径,也可以使用绝对路径。
使用相对路径重定向到当前目录下的资源welcome.html。其代码如下: 



response.sendRedirect("welcome.html")




如上述代码的第10行和第13行都使用了相对路径。
如果使用绝对路径,第10行代码可以改写为如下代码。



response.sendRedirect(this.getServletContext().getContextPath()+"/welcome.html");




其中this.getServletContext().getContextPath()表示取得当前项目的上下文路径,即“/ch5_demo”。本例中当前目录就是根目录,因此,使用相对路径和绝对路径两种用法其效果是一样的。
(4) 发布项目并运行,在浏览器地址栏中输入访问路径http://localhost: 8080/ch5_demo/login.html。用户登录运行结果如图59所示。


图59用户登录运行结果


当用户名和密码正确时,会重定向到welcome.html页面,否则重定向到login.html页面。地址栏中会显示当前实际请求资源的地址。
注意: 在Eclipse的Web项目中,WebContent文件夹的文件在发布到服务器时,直接发布到根目录文件夹下。打开Tomcat安装目录下的webapps文件夹,找到ch5_demo项目,可以看到有welcome.html和login.html文件。ch5_demo项目发布目录如图510所示。



图510ch5_demo项目发布目录


2. 输出中文乱码
计算机中的数据都是以二进制形式存储的,因此,当传输文本数据时,会发生字符和字节之间的转换。字符与字节之间的转换是通过查码表完成的,将字符转换成字节的过程称为编码,将字节转换成字符的过程称为解码。如果编码和解码使用的码表不一致,就会出现乱码问题。
1) 输出中文乱码的显示
在项目ch5_demo的com.yzpc.servlet包中创建ChineseServlet类,在doGet()方法中添加如下代码。



response.getWriter.print("生态文明");




编译运行后,浏览器的页面显示乱码——四个“?”,而不是显示“生态文明”四个字。输出中文乱码问题界面如图511所示。



图511输出中文乱码问题界面


2) 输出中文乱码原因及解决方案
HttpServletResponse对象在输出时默认使用ISO88591码表,不支持中文,而客户端浏览器默认使用中文编码,因此出现中文乱码。输出中文乱码示意如图512所示。



图512输出中文乱码示意


出现中文乱码问题的主要原因是服务器端和客户端使用的编码方式不一致造成的。下面分别介绍使用OutputStream流和PrintWriter流对于输出中文乱码问题的解决方案。
(1) 使用OutputStream流向客户端浏览器输出中文数据。
在服务器端,响应消息体的数据是以哪个码表编码的,就要控制客户端浏览器以相应的码表解码。比如: outputStream.write("中国".getBytes("GB18030")); 使用OutputStream流向客户端浏览器输出中文,以GB18030码表编码输出,此时就要控制客户端浏览器以GB18030码表解码,否则显示的时候就会出现中文乱码。在服务器端控制客户端浏览器的编码字符集,可以通过设置HTTP响应头字段contentType来实现。其代码如下: 



response.setHeader("Content-Type", "text/html;charset= GB18030");




通过设置ContentType响应头控制浏览器以GB18030的编码显示数据。
使用OutputStream流向客户端浏览器输出中文数据的步骤如下所述。
第1步,用response.setHeader("ContentType", "text/html; charset= GB18030")方法指定客户端的编码字符集,如不指定,将使用当前浏览器默认的字符集,可能会出现乱码。
第2步,使用response.getOutputStream()方法获取输出流对象。
第3步,使用getBytes()方法将字符串转换成字节数组。
第4步,调用outputStream.write()方法发送响应消息体。
注意: 第1步指定的字符集与第3步中编码使用的字符集要一致。
【例55】使用OutputStream字节流输出中文。
① 重写ChineseServlet.java类的doGet()方法,定义一个中文字符串,调用OutputStream流输出。其代码如下: 



1protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

2String data="生态文明";








3//1.通过设置响应头控制浏览器以GB18030的编码显示数据,如果不加这语句,那么浏览器显示的将是乱码

4response.setHeader("content-Type", "text/html;charset=GB18030");

5//2.获取OutputStream输出流

6OutputStream out=response.getOutputStream();

7//3.将字符转换成字节数组,指定以GB18030编码进行转换与第1步中指定的编码一致

8byte[] dataByteArr = data.getBytes("GB18030");

9//4.使用OutputStream流向客户端输出字节数组

10out.write(dataByteArr);

11}




② 启动服务器。运行结果能正确显示中文数据,如图513所示。



图513运行结果


(2) 使用PrintWriter流向客户端浏览器输出中文数据。
PrintWriter流是字符流,使用PrintWriter流向客户端浏览器输出中文数据,其步骤如下。
第1步,使用response.setCharacterEncoding("GB18030")设置字符以什么样的编码输出到浏览器,若不指定,默认使用ISO88591编码,该编码不兼容中文。
第2步,使用response.setHeader("ContentType", "text/html; charset=GB18030") 通知浏览器使用的解码字符集。
第3步,使用response.getWriter()方法; 获取PrintWriter输出流。
第4步,调用PrintWriter对象的write()方法或print()方法发送响应消息体。
注意:
(1) 第1步与第2步中指定的字符集需一致; 
(2) 第3步一定要在前两步执行后再执行,否则不能解决乱码问题; 
(3) 通过方法response.setContentType("text/html; charset=GB18030")可以同时实现第1步与第2步的功能。
【例56】使用PrintWriter字符流输出中文。
重写例55中ChineseServlet的doGet()方法,用字符流输出中文字符。其代码如下: 



1protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

2//使用PrintWriter流输出中文

3response.setCharacterEncoding("GB18030");

4response.setHeader("Content-Type", "text/html;charset=GB18030");

5String data="网络空间";

6PrintWriter out=response.getWriter();

7out.print(data);

8}




启动服务器,刷新浏览器,运行结果能正确显示中文数据,如图514所示。



图514PrintWriter字符流输出中文


5.2HttpServletRequest接口及其应用
5.2.1HttpServletRequest接口



视频讲解


在Servlet API中定义了一个HttpServletRequest接口,它位于javax.servlet.http包中,继承自javax.servlet.ServletRequest接口,其主要作用是封装HTTP请求消息。由于HTTP请求消息分为请求行、请求消息头和请求消息体3部分,因此,在HttpServletRequest接口中定义了获取请求行、请求头和请求消息体的相关方法。
1. 获取请求行信息的相关方法
当访问 Servlet 时,所有HTTP请求消息将被封装到 HttpServletRequest 对象中,HTTP请求消息的请求行中包含请求方式、请求资源名和请求路径等信息。为了获取这些信息,HttpServletRequest 接口定义了一系列方法,如表52所示。


表52获取请求行信息的方法




方 法 声 明功 能 描 述
String getMethod()用于获取HTTP请求消息中的请求方法
String getRequestURI()用于获取客户端发出请求时的URI
String getQueryString()用于获取请求行中的参数部分,也就是资源路径后面问号(?)以后的所有内容
String getContextPath()用于获取请求URL中属于Web应用程序的路径,这个路径以“/”开头,表示相对于整个Web站点的根目录,路径结尾不含“/”。如果请求URL 属于Web站点的根目录,那么返回结果为空字符串("")
StringBuffer getRequestURL()用于获取客户端发出请求时的完整的URL,包括协议、服务器名、端口号、资源路径等信息,但不包括后面的查找参数部分。注意,该方法返回的结果是StringBuffer类型而不是string类型,这样更便于对结果进行修改
String getPathInfo() 方法返回请求URL中的额外路径信息。额外路径信息是请求URL中的位于Servlet的路径之后和查询参数之前的内容,它以“/”开头
String getRemoteAddr() 方法返回发出请求的客户端的IP地址
String getRemoteHost() 方法返回发出请求的客户端的完整主机名
String getRemotePort() 方法返回客户端所使用的网络端口号
String getLocalAddr() 方法返回Web服务器的IP地址
String getLocalName() 方法返回Web服务器的主机名

常用的是前5种方法,特别是getRequestURL()方法、getRequestURI()方法、getContextPath()方法,它们常用于文件路径的设置。下面举例说明这5种方法的使用。
【例57】获取请求行信息。
(1) 在项目ch5_demo中,创建ReqServlet类,重写doGet()方法。其代码如下: 



1protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

2response.setContentType("text/html;charset=UTF-8");

3PrintWriter pw = response.getWriter();

4//获得请求行的相关信息

5pw.println("请求的方法:"+request.getMethod()+"<br/>");//获取请求的方法

6pw.println("请求URI:"+request.getRequestURI()+"<br/>"); //获取URI

7pw.println("请求行中的参数:"+request.getQueryString()+"<br/>"); 
//获取请求行中的参数

8pw.println("Web应用程序的路径:"+request.getContextPath()+"<br/>"); 
//Web应用程序的路径

9pw.println("完整的URL:"+request.getRequestURL()+"<br/>"); //完整的URL

10}




(2) 测试运行结果。启动服务器,在浏览器地址栏中输入http://localhost:8080/ch5_demo/ReqServlet。运行结果如图515(a)所示。


图515获取请求行信息运行结果 


在图515(a)中,请求行参数为null,这是因为输入的地址中没有带参数。重新输入如下地址http://localhost:8080/ch5_demo/ReqServlet?a=123。运行结果如图515(b)所示。

2. 获取请求消息头的相关方法
当浏览器发送 Servlet 请求时,需要通过请求消息头向服务器端传递附加信息,例如客户端可以接收的数据类型、压缩方式、语言等。为此,在 HttpServletRequest 接口中定义了一系列用于获取 HTTP 请求头字段的方法,如表53 所示。


表53获取请求头字段的方法



方 法 声 明功 能 描 述
String getHeader(String name)根据请求头字段的名称获取对应的请求头字段的值,只返回满足条件的第1个值,如果请求消息中不包含指定的头字段,就返回null
Enumeration getHeaders(String name)根据请求头字段的名称获取对应的请求头字段的所有值,存储到一个Enumeration对象中
Enumeration getHeaderNames()用于获取一个包含所有请求头字段名称的Enumeration 对象
int getIntHeader(String name)根据请求头字段的名称获取对应的请求头字段的值,并转换为int类型。如果指定头字段不存在,返回1; 如果获取到的头字段不能转换为int,将发生NumberFormatException异常
long getDateHeader(String name)根据请求头字段的名称获取对应的请求头字段的值,并将其按GMT时间格式转换为一个代表日期/时间的长整数,这个长整数是自1970年1月1日0时0分0秒算起的以毫秒为单位的时间值
String getContentType()获取ContentType头字段的值
String getCharacteEncoding()用于返回请求消息的实体部分的字符集编码,通常从ContentType头字段中进行提取

下面通过一个案例,介绍使用getHeaderNames()方法和getHeader()方法获取头字段及其值的实现。
【例58】通过request对象获取客户端所有请求头信息。
(1) 在项目ch5_demo中,创建HeadServlet类,重写doGet()方法。其代码如下: 



1public void doGet(HttpServletRequest request, HttpServletResponse response)

2throws ServletException, IOException {

3//通过设置响应头控制浏览器以UTF-8的编码显示数据

4response.setContentType( "text/html;charset=UTF-8");

5PrintWriter out = response.getWriter();

6Enumeration<String>headNames = request.getHeaderNames();//获取所有的请求头

7out.write("获取到的客户端所有的请求头信息如下:");

8out.write("<hr/>");

9while (headNames.hasMoreElements()) {

10String headName = (String) headNames.nextElement();

11//根据请求头的名字获取对应的请求头的值

12String headValue = request.getHeader(headName); 

13out.write(headName+":"+headValue);

14out.write("<br/>");

15}

16out.write("<br/>");

17}




(2) 测试运行结果。启动服务器,在浏览器的地址栏中输入http://localhost:8080/ch5_demo/headServlet。运行结果如图516所示。



图516获取请求头信息的运行结果


3. 获取请求消息体的相关方法
当使用POST请求方式时,请求消息包含请求消息体,在请求消息体中封装了POST请求的参数。
在实际开发过程中,经常需要从 HttpServletRequest 中读取HTTP请求的消息体的内容,在 HttpServletRequest 接口的ServletRequest父接口中定义了一系列读取消息体的方法,如表54所示。


表54获取请求参数消息体的相关方法




方 法 声 明功 能 描 述
String getParameter(String name)该方法用于获取某个指定名称的参数值,如果请求消息中没有包含指定名称的参数,getParameter()方法返回null; 如果指定名称的参数存在但没有设置值,返回一个空串; 如果请求消息中包含有多个该指定名称的参数,getParameter()方法返回第一个出现的参数值
String[] getParameterValues(String name)HTTP请求消息中可以有多个相同名称的参数(通常由一个包含有多个同名的字段元素的FORM表单生成),如果要获得HTTP请求消息中的同一个参数名所对应的所有参数值,那么就应该使用getParameterValues()方法,该方法用于返回一个String类型的数组
Enumeration getParameterNames()该方法用于返回一个包含请求消息中所有参数名的Enumeration对象,在此基础上,可以对请求消息中的所有参数进行遍历处理
Map getParameterMap()个体Parameter Map()方法用于将请求消息中的所有参数名和值装入一个Map对象中并返回
BufferedReader getReader()获取字符输入流,只能操作字符数据
ServletInputStream getInputStream()获取字节输入流,可以操作所有类型的数据

getParameter()方法、getInputStream()方法和getReader()方法都是从Servlet中的request对象得到提交的数据,但是用途不同,因此要根据表单提交数据的编码方式选择不同的方法,这里不再赘述。在默认情况下使用getParameter()方法获取表单字段及其值。
4. 通过 Request 对象传递数据
Request对象不仅可以获取一系列数据,还可以通过属性传递数据。ServletRequest 接口中定义了一系列操作属性的方法。
(1) setAttribute()方法。该方法用于将一个对象与一个名称关联后存储到 ServletRequest 对象中,其完整语法代码如下: 



public void setAttribute(String name,Object o);




注意: 如果 ServletRequest 对象中已经存在指定名称的属性,那么setAttribute() 方法将会先删除原来的属性,然后再添加新的属性。如果传递给 setAttribute()方法的属性值对象为 null,就删除指定名称的属性,这时的效果等同于removeAttribute()方法。
(2) getAttribute()方法。该方法用于从 ServletRequest 对象中返回指定名称的属性对象,其完整的语法代码如下: 



public Object getAttribute(String name);




(3) removeAttribute()方法。该方法用于从 ServletRequest 对象中删除指定名称的属性,其完整的语法代码如下: 



public void removeAttribute(String name);




(4) getAttributeNames()方法。该方法用于返回一个包含 ServletRequest 对象中的所有属性名的 Enumeration 对象,在此基础上,可以对 ServletRequest 对象中的所有属性进行遍历处理。getAttributeNames()方法的完整语法代码如下: 



public Enumeration getAttributeNames();




注意: 只有属于同一个请求中的数据才可以通过 ServletRequest 对象传递数据。


视频讲解


5.2.2HttpServletRequest应用
1. 获取请求参数

在实际开发中,经常需要获取用户提交的表单数据,例如用户名和密码等,为了方便获取表单中的请求参数,在 HttpServletRequest 接口的ServletRequest父接口中定义了一系列获取请求参数的方法,如表54所示。下面举例说明如何使用这些方法来获取用户提交的表单数据。
【例59】学生选课。

创建图517所示的“学生选课”对话框,输入学号、姓名、性别和课程信息,单击“提交”按钮,显示选课成功页面,并在页面上显示学生的相关信息。本例中分别介绍getParameter()方法、getParameterValues()方法、getParameterNames()方法及getParameterMap()方法的使用。具体步骤如下所述。


图517“学生选课”对话框


(1) 在项目ch5_demo的WebContent文件夹下新建selCourse.html页面。该页面中的表单代码如下: 



1<form action="SelCourseServlet" method="POST" >

2<label> 学号:</label> <input type="text"name="id"/><br>

3<label> 姓名:</label><input type="text"name="name"/><br>

4<label>性别:</label><input type="radio" name="sex" value="男" checked="checked">男

5<input type="radio" name="sex" value="女">女<br>

6<label> 课程:</label> <input type="checkbox" name="courses" value=" C++">C++

7<input type="checkbox" name="courses" value=" Java Web ">Java Web

8<input type="checkbox" name="courses" value="英语">英语<br>

9<input type="submit" value="提交"> <input type="reset" value="重置">

10</form>




第4和第5行的两个radio输入框的name属性取相同值,表示是同一组单选按钮,一次只能取一个值,value属性值唯一。同样,第6~8行的三个checkbox输入框的name属性值相同,表示一组复选框,value属性值有多个,系统将以字符串数组存储。
(2) 在项目ch5_demo的com.yzpc.servlet包中创建SelCourseServlet.java类文件。
(3) 使用getParameter()方法和getParameterValues()方法接收表单参数。 SelCourseServlet.java类的doGet()方法代码如文件55所示。
文件55SelCourseServlet.java类的doGet()方法代码



1protected void doGet(HttpServletRequest request, HttpServletResponse response) throws 
ServletException, IOException {

2request.setCharacterEncoding("GB18030");

3//设置客户端浏览器以GB18030编码解析数据

4response.setContentType("text/html;charset=GB18030");

5PrintWriter out=response.getWriter();

6/*

7使用getParameter()方法getParameterValues()方法接收表单参数并输出到客户端

8*/

9//获取填写的编号,id是文本框的名字,<input type="text" name="id">

10String id = request.getParameter("id");

11String name = request.getParameter("name");//获取填写的姓名

12String sex = request.getParameter("sex"); //获取选中的性别

13//获取选中的课程,因为可以选中多个值,所以获取到的值是一个字符串数组,因此需要使用getParameterValues()方法来获取

14String[] courses = request.getParameterValues("courses");

15String courseStr="";

16//获取数组数据的技巧,可以避免courses数组为null时引发的空指针异常错误

17for (int i = 0; courses!=null && i < courses.length; i++) {

18if (i == courses.length-1) {courseStr+=courses[i];}else {courseStr+=courses[i]+",";}

19}

20String htmlStr = "<table>" +

21"<tr><td>学号:</td><td>{0}</td></tr>" +

22"<tr><td>姓名:</td><td>{1}</td></tr>" +

23"<tr><td>性别:</td><td>{2}</td></tr>" +

24"<tr><td>选中的课程:</td><td>{3}</td></tr>" +

25"</table>";

26htmlStr = MessageFormat.format(htmlStr, id,name,sex,courseStr);


27out.write(htmlStr); //输出htmlStr里面的内容到客户端浏览器显示

28}




第10~12行分别读取学号、姓名和性别,这几个参数的值都是唯一的,使用getParameter()方法读取。第14行读取选中的课程名称,由于课程的值会有多个,使用getParameterValues()方法读取,结果存放在字符串数组courses中; 如果没有选中的课程,getParameterValues()方法返回null。为了便于输出,第17~19行将字符串数组courses转换为一个字符串courseStr。
注意: getParameter()方法的参数要与提交表单中对应输入框的name属性保持一致,否则读取错误。
启动服务器,在浏览器地址栏中输入http://localhost:8080/ch5_demo/selCoursse.html,显示学生选课页面,如图517所示。在页面上显示的学生选课表单中填写数据,然后提交到SelCourseServlet由Servlet进行处理,运行结果如图518所示。


(4) 若在服务器端使用getParameterNames()方法接收表单参数,则改写文件55中的6~28行。 其代码如下: 



1/*

2使用getParameterNames() 方法接收表单参数并输出到客户端

3*/









4Enumeration<String> paramNames = request.getParameterNames();//获取所有的参数名

5while (paramNames.hasMoreElements()) {

6String name = paramNames.nextElement(); //得到参数名

7String value = request.getParameter(name); //通过参数名获取对应的值

8out.println(MessageFormat.format("{0}:{1}<br>", name,value));

9}






图518getParameter()方法和getParameterValues()方法的运行结果


说明在代码中通过getParameterNames()方法获取所有请求参数并存储到枚举对象paramNames中,通过对paramNames迭代取得参数名,再通过getParameter()方法根据参数名获得参数值,最后调用MessageFormat.format()方法按格式输出参数和参数值。MessageFormat类在java.text包中。使用getParameterNames()方法的运行结果如图519所示。



图519使用getParameterNames()方法的运行结果


在图519所示中,只显示了一门课程名称。这是由于第7行代码在读取参数值时,使用getParameter()方法,因此只能显示第一门选中的课程名称。
(5) 在服务器端使用getParameterMap()方法接收表单参数,改写文件55中的6~28行。其代码如下: 



1/*

2使用getParameterMap()方法接收表单参数并输出到客户端 

3*/

4Map<String, String[]> paramMap = request.getParameterMap();

5for(Map.Entry<String, String[]> entry :paramMap.entrySet()){

6String paramName = entry.getKey();

7String paramValue = "";

8String[] paramValueArr = entry.getValue();

9for (int i=0; paramValueArr!=null && i < paramValueArr.length; i++) {

10if (i==paramValueArr.length-1) {

11paramValue+=paramValueArr[i];

12}else {

13paramValue+=paramValueArr[i]+",";









14}

15}

16out.println(MessageFormat.format("{0}:{1}<br>", paramName,paramValue));

17}




使用getParameterMap()方法得到了每个参数及其所有值,存储到Map<String,String[]>类型对象paramMap中。通过对paramMap.entrySet()迭代,从每个Map.Entry对象中取得参数的名称paramName(第6行)及该参数的所有值paramValueArr(第8行); 第9~15行通过循环读出paramValueArr的所有值并放到变量paramValue中; 第16行输出该参数的名称和值。使用getParameterMap()方法的运行结果如图520所示。


2. HTTP请求的中文乱码问题
1) 以POST方式提交表单中中文参数出现的乱码问题

在文件55中,将第2行“request.setCharacterEncoding("GB18030");”删除。重新启动服务器,根据图517所示输入表单内容,表单数据提交后,运行结果如图521所示。


图520使用getParameterMap()方法的运行结果




图521删除文件55中第2行内容后的运行结果



2) POST方式提交中文数据乱码产生的原因和解决办法
在图521所示中,第一列中文正常显示,第二列内容中的中文则显示为乱码。这是因为当用户提交表单数据时,其中的数据是以GB18030编码的,在服务器端request对象读取参数时默认使用ISO88591解码,由于编码与解码字符集不一致,所以读取的参数值就是乱码了。
HttpServletRequest与HttpServletResponse在默认情况下都是使用ISO88591码表,在本章的5.1.2节中分析了在中文输出乱码问题后,在Servlet中可以使用setContentType()方法解决响应输出的中文乱码问题,因此,在输出第一列时能够正常输出; 而第二列是request对象获取的请求参数的值,由于request对象没有能正确解码,得到的是乱码,因此发送到响应消息体的也是乱码。中文乱码问题示意如图522所示。


图522中文乱码问题示意图


由图522所示可以看到,之所以会产生乱码,就是因为服务器端HttpServletRequest对象和客户端浏览器使用的编码不一致造成的。因此,解决的办法是在客户端和HttpServletRequest对象之间设置一个统一的编码,之后就按照此编码进行数据的传输和接收。
由于客户端浏览器是以GB18030字符编码将表单数据传输到服务器端的,因此服务器端也需要设置以GB18030字符编码进行接收,要想完成此操作,服务器端可以直接使用从ServletRequest接口继承而来的setCharacterEncoding(charset)方法进行统一的编码设置。
因此,文件55中的第2行代码请勿删除,否则会产生乱码。
3) 以GET方式提交表单中文参数的乱码问题
在例59中,将selCourse.html文件中表单的method属性改为GET,其他不变。
启动服务器运行,根据图517输入表单内容,表单数据提交后,页面依然产生乱码,显然,setCharacterEncoding(charset)方法不能解决GET请求的中文乱码问题。运行结果如图523所示。



图523以GET方式的运行结果


4) 以GET方式提交中文数据乱码产生的原因和解决办法
(1) 产生的原因。对于以GET方式传输的数据,request即使设置了以指定的编码接收数据也是无效的,默认的还是使用ISO88591这个字符编码来接收数据。客户端以GB18030的编码传输数据到服务器端,而服务器端的request对象使用的是ISO88591这个字符编码来接收数据,服务器和客户端沟通的编码不一致,因此才会产生中文乱码。
(2) 解决办法。在接收到数据后,先获取request对象以ISO88591字符编码接收到的原始数据的字节数组,然后通过字节数组以指定的编码构建字符串,即可解决乱码问题。其代码如下: 



1String name=request.getParameter("name");//接收数据

2name =new String(name.getBytes("ISO-8859-1"), "GB18030") ; //重新编码




5) 以超链接形式传递中文参数的乱码问题
客户端想传输数据到服务器端,可以通过表单提交的形式,也可以通过超链接后面加参数的形式,例如: 



<a href="SelCourseServlet?id=1101&name=李磊">单击</a>




单击超链接,数据是以GET的方式传输到服务器的,所以接收中文数据时也会产生中文乱码问题,而解决中文乱码问题的方式与上述的以GET方式提交表单中文数据乱码处理问题的方式一致。
综上所述,HTTP请求参数的提交方式有GET和POST两种,对应地,处理中文乱码问题也有以下两种不同的方式。
(1) 如果提交方式为POST,那么只需要在服务器端使用setCharacterEncoding()方法设置request对象的编码即可,客户端以哪种编码提交的,服务器端的request对象就以对应的编码接收。
(2) 如果提交方式为GET,那么只能在接收到数据后再手工转换。其代码如下: 



1String data = request.getParameter("paramName");//获取客户端提交上来的数据

2byte[] source = data.getBytes("ISO-8859-1");//查找ISO-8859-1码表,得到客户端
//提交的原始数据的字节数组

3data = new String(source, "GB18030");//通过字节数组以指定的编码构建字
//符串,解决乱码




通过字节数组以指定的编码构建字符串,这里指定的编码是根据客户端提交数据时使用的字符编码来定的,如果是GB2312,那么就设置成data=new String(source, "GB2312"),如果是UTF8,那么就设置成data=new String(source, "UTF8")
无论使用哪种方式处理中文乱码,最终都要使服务器端与客户端中文编码方式保持一致。


视频讲解



5.3RequestDispatcher接口及其应用
5.3.1RequestDispatcher接口

当一个 Web 资源收到客户端的请求后,如果希望服务器通知另一个资源处理请求,那么这时可以通过 RequestDispatcher 接口的实例对象实现。ServletRequest 接口中定义了一个获取 RequestDispatcher 对象的方法。其代码如下: 



public RequestDispatcher getRequestDispatcher(String path);




该方法返回一个RequestDispatcher对象,该对象充当给定路径的资源,资源可以是动态的也可以是静态的。参数path是指定资源路径名的字符串,可以是相对路径。如果路径名以“/”开头,用于表示当前Web应用的根目录。
注意: WEBINF目录中的内容对RequestDispatcher也是可见的。因此,传递给RequestDispatcher()方法的资源可以是WEBINF目录中的内容。
ServletContext接口中也定义了同样的方法,和ServletRequest接口定义的getRequestDispatcher()方法唯一的区别就是,ServletContext接口的getRequestDispatcher()方法的参数path必须以“/”开头。以下三条语句都可以获取资源welcome.html的RequestDispatcher对象。



1ServletContext context=this.getServletContext();

2RequestDispatcher rsd=context.getRequestDispatcher("/welcome.html");//参数必须以"/"开头

3RequestDispatcher rsd=request.getRequestDispatcher("welcome.html"); //参数不以"/"开头

4RequestDispatcher rsd=request.getRequestDispatcher("/welcome.html"); //参数以"/"开头




获取到 RequestDispatcher 对象后,最重要的工作就是通知其他 Web 资源处理当前的 Servlet 请求,为此,RequestDispatcher 接口定义了两个相关方法。其代码如下: 



1public void forward(ServletRequest request, ServletResponse response)

2public void include(ServletRequest request, ServletResponse response)




其中,forward()方法可以实现请求转发,include()方法可以实现请求包含。
注意: 当Servlet源组件调用RequestDispatcher的forward()方法或include()方法时,都要把当前的ServletRequest对象和ServletResponse对象作为参数传给forward方法()或include()方法,这就使得源组件和目标组件共享同一个ServletRequest对象和ServletResponse对象,就实现了多个Servlet协同处理同一个请求。
5.3.2RequestDispatcher应用
1. 请求转发

1) 请求转发的基本概念
在 Servlet 中,请求转发是指一个Web资源在接收到客户端请求后,通知服务器去调用另一个Web资源进行处理,即将原页面的request对象和response对象传入新的页面,这就使新旧页面拥有相同的request对象和response对象。请求转发的工作原理如图524所示。



图524请求转发的工作原理


从图524所示中可以看出,当客户端访问 Servlet1 时,可以通过forward()方法将请求转发给其他 Web 资源,其他 Web 资源处理完请求后,直接将响应结果返回到客户端。在这个过程中,客户端和服务器端发生一次请求和一次响应。
【例510】演示 forward()方法的使用。
(1) 在项目 ch5_Demo 的com.yzpc.servlet包中创建一个名为ForwardServlet.java 的Servlet类,该类使用forword()方法将请求转发到一个新的welcome.html页面。ForwardServlet.java代码如文件56所示。
文件56ForwardServlet.java代码



1package com.yzpc.servlet;

2import java.io.IOException;

3import javax.servlet.RequestDispatcher;

4import javax.servlet.ServletException;

5import javax.servlet.annotation.WebServlet;

6import javax.servlet.http.HttpServlet;

7import javax.servlet.http.HttpServletRequest;

8import javax.servlet.http.HttpServletResponse;

9@WebServlet("/ForwardServlet")

10public class ForwardServlet extends HttpServlet {

11private static final long serialVersionUID=1L;

12protected void doGet(HttpServletRequest request, HttpServletResponse response) 

13throws ServletException, IOException {








14RequestDispatcher rsd=this.getServletContext().getRequestDispatcher("/welcome.html");

15rsd.forward(request, response);

16}

17protected void doPost(HttpServletRequest request, HttpServletResponse response) 

18throws ServletException, IOException {

19doGet(request,response);

20}

21}




(2) 在 ForwardServlet 中,通过调用ServletContext的getRequestDispatcher(String path)方法,返回一个RequestDispatcher对象rsd,再调用rsd的forward()方法将当前 Servlet 的请求转发到 welcome.html 页面。
启动服务器后,在浏览器地址栏中输入http://localhost:8080/ch5_demo/ForwardServlet,运行结果如图525所示。显示welcome.html页面的内容,但地址栏没有变化,这是因为,对于客户端来说,它只发出了一次请求,所以请求地址不变。



图525请求转发的运行结果


如果将文件56第14行代码中getRequestDispatcher()方法的参数的“/”去掉,重新运行后,发生IllegalArgumentException异常,要求参数必须以“/”开头。IllegalArgumentException异常如图526所示。



图526IllegalArgumentException异常


如果通过request对象提供的getRequestDispatche(String path)方法,获取RequestDispatcher对象,比较它们的使用有何不同。
将第14行代码改为“RequestDispatcher rsd=request.getRequestDispatcher("welcome.html");”或者“RequestDispatcher rsd=request.getRequestDispatcher("/welcome.html");”,重新编译后运行结果都如图525所示。
说明: request对象的getRequestDispatcher()可以使用绝对路径,也可以使用相对路径。其他效果与ServletContext对象的getRequestDispatcher()方法一样。
2) 请求转发可以传递数据
request对象同时也是一个域对象(Map容器),开发人员通过request对象在实现转发时,把数据通过request对象带给其他Web资源处理。
【例511】使用请求转发传递数据。
(1) 在 com.yzpc.servlet包中创建Servlet类SendDataServlet,设置虚拟路径为“/send”。重写doGet()方法,在doGet()方法中调用request.setAttribute()方法存储域数据,然后将请求转发给ResultServlet类。重写doGet()方法的代码如下: 



1protected void doGet(HttpServletRequest request, HttpServletResponse response) 

2throws ServletException, IOException {

3response.setContentType("text/html;charset=GB18030");

4request.setAttribute("country", "中国");

5request.setAttribute("city", "扬州");

6RequestDispatcher rsd=request.getRequestDispatcher("ResultServlet");

7rsd.forward(request, response);

8}




(2) 在 com.yzpc.servlet包中创建一个名为 ResultServlet 的 Servlet 类,设置虚拟路径为“/ ResultServlet”,该类用于获取SendDataServlet类中存储在request对象中的数据并输出。ResultServlet 类的代码如下: 



1protected void doGet(HttpServletRequest request, HttpServletResponse response) 

2throws ServletException, IOException {

3PrintWriter out=response.getWriter();

4response.setContentType("text/html;charset=GB18030");


5String country=(String)request.getAttribute("country");

6String city=(String)request.getAttribute("city");

7out.print(country+"<br>"+city);

8}




启动 Tomcat 服务器,在浏览器的地址栏中输入地址 http://localhost:8080/ch5_demo/send 访问 SendDataServlet,浏览器的显示结果如图527所示。



图527request对象传递数据的运行结果


从图527所示中可以看出,地址栏中显示的仍然是 ForwardServlet 的请求路径,但是浏览器却显示出了 ResultServlet 中要输出的内容。这是因为,请求转发是发生在服务器内部的行为,从 RequestForwardServlet 到 ResultServlet 属于一次请求,在一次请求中是可以使用 request 属性进行数据共享的。
3) 请求重定向和请求转发的区别
(1) RequestDispatcher.forward()方法只能将请求转发给同一个Web应用中的组件; 而HttpServletResponse.sendRedirect()方法还可以重定向到同一个站点上的其他应用程序中的资源,甚至是使用绝对URL重定向到其他站点的资源。
(2) 如果传递给HttpServletResponse.sendRedirect()方法的相对URL以“/”开头,它是相对于整个Web站点的根目录; 如果创建RequestDispatcher对象时指定的相对URL以“/”开头,它是相对于当前Web应用程序的根目录。
(3) 调用HttpServletResponse.sendRedirect()方法重定向的访问过程结束后,浏览器地址栏中显示的URL会发生改变,由初始的URL地址变成重定向的目标URL; 调用RequestDispatcher.forward()方法的请求转发过程结束后,浏览器地址栏保持初始的URL地址不变。
(4) RequestDispatcher.forward()方法的调用者与被调用者之间共享相同的request对象和response对象,它们属于同一个访问请求和响应过程; 而HttpServletResponse.sendRedirect()方法调用者与被调用者使用各自的request对象和response对象,它们属于两个独立的访问请求和响应过程。
2. 请求包含
1) 请求包含的基本概念
请求包含指的是使用include()方法将Servlet(源组件)请求转发给其他资源(目标组件)进行处理,并将生成的响应结果包含到源组件的响应结果中。
包含与转发相比,源组件与被包含的目标组件的输出数据都会被添加到响应结果中,在目标组件中对响应状态代码或者响应头所做的修改都会被忽略。
【例512】include()方法的使用。
(1) 在项目ch5_demo的com.yapc.servlet包中创建IncludeServlet.java文件。重写doGet()方法代码如下: 



1protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

2response.setContentType("text/html;charset=GB18030");

3PrintWriter out=response.getWriter();

4out.print("IncludeServlet: 故人西辞黄鹤楼<br>");

5//调用include()方法包含IncludedServlet

6request.getRequestDispatcher("IncludedServlet").include(request, response);

7out.print("IncludeServlet: 烟花三月下扬州<br>");

8}




上述代码中,第2行指定响应消息体的编码方式,第4行代码输出一行文本,第6行执行请求包含方法,将IncludedServlet包含到IncludeServlet中。
(2) 在com.yapc.servlet包中创建IncludedServlet.java文件。重写doGet()方法代码如下: 



1protected void doGet(HttpServletRequest request, HttpServletResponse response)

2throws ServletException, IOException {

3response.getWriter().print("IncludedServlet: 孤帆远影碧空尽,唯见长江天际流。");

4}




(3) 启动服务器。在浏览器地址栏中输入地址: http://localhost:8080/ch5_demo/IncludeServlet。请求包含运行结果如图528所示。



图528请求包含运行结果


由图528所示看出,运行结果既包含了IncludeServlet中的输出文本,又包含了IncludedServlet中的输出文本。并且IncludedServlet中的输出文本插在IncludeServlet中的输出文本的两句中间输出,输出次序与代码书写次序一致。
在IncludedServlet中没有指定响应消息体的编码方式,结果也没出现乱码。这是因为浏览器在请求IncludeServlet时已经创建了response对象,并且指定了编码方式,当客户端对接收到的数据进行解码时,Web服务器会继续保持调用response对象中的信息,从而使IncludedServlet中的内容不会发生乱码。
即使IncludedServlet中指定响应消息体的编码方式,也会被服务器忽略。如将IncludeServlet中doGet()方法的第2行代码删除,在IncludedServlet中doGet()方法的第1和第3行代码之间添加“response.setContentType("text/html; charset=GB18030");”语句,运行结果输出中文乱码,如图529所示。



图529修改后的请求包含运行结果


2) 请求转发和请求包含的区别
(1) 相同点。
请求转发和请求包含都是在处理一个相同的请求,多个Servlet之间使用同一个 request 对象和 response 对象。
(2) 不同点。
① 如果在AServlet中请求转发到BServlet,那么在AServlet中不允许再输出响应体,即不能使用response.getWriter() 和response.getOutputStream() 向客户端输出,这一工作交由BServlet来完成; 如果是由AServlet请求包含BServlet,就没有这个限制。
② 请求转发不能设置响应消息体,但是可以设置响应消息头,简单来说,就是“留头不留体”。例如: response.setContentType("text/html; charset=GB18030") 是可以留下来的; 请求包含不仅可以设置响应消息头,还可以设置响应消息体,简单来说就是“留头又留体”。
③ 请求转发大多应用在Servlet中,转发目标大多是JSP页面; 请求包含大多应用在JSP页面中,完成多页面的合并。
5.4本 章 小 结
本章介绍了HttpServletRequest和HttpServletResponse接口及其常用方法。分析了HTTP请求参数和响应消息体中出现中文乱码问题的原因及解决方案,几种常见的页面重定向的技术包括刷新并跳转、请求重定向、请求转发及这几种方法之间的异同及其应用。