06.请求和响应
Servlet最主要的作用就是处理客户端请求,并向客户端做出响应。
为此,针对Servlet的每次请求,Web服务器在调用service()方法之前,都会创建两个对象,分别是HttpServletRequest和HttpServletResponse。
其中,HttpServletRequest用于封装HTTP请求消息,简称request对象。
HttpServletResponse用于封装HTTP响应消息,简称response对象。
request对象和response对象在请求Servlet过程中至关重要,接下来,通过一张图来描述浏览器访问Servlet的交互过程,如图所示。
需要注意的是,在Web服务器运行阶段,每个Servlet都只会创建一个实例对象。然而,每次HTTP请求,Web服务器都会调用所请求Servlet实例的service(HttpServletRequest request,HttpServletResponse response)方法,重新创建一个request对象和一个response对象。
接下来,我们将针对request对象和response对象进行详细地讲解。
一、响应HttpServletResponse对象
在Servlet API中,定义了一个HttpServletResponse接口,它继承自ServletResponse接口,专门用来封装HTTP响应消息。由于HTTP响应消息分为状态行、响应消息头、响应消息体(响应正文)三部分,因此,在HttpServletResponse接口中定义了向客户端发送响应状态码、响应消息头、响应消息体的方法。
response对象是Servlet.service()方法的一个参数,在客户端发出每个请求时,服务器都会创建一个response对象,并传入给Servlet.service()方法。response对象是用来对客户端进行响应的,这说明在service()方法中使用response对象可以完成对客户端的响应工作。
response对象的应用分为以下五种:
- 发送状态码;
- 设置响应头信息;
- 设置响应正文;
- 重定向;
- 网页定时刷新并跳转。
1、发送状态码
当Servlet向客户端回送响应消息时,需要在响应消息中设置状态码。为此,在HttpServletResponse接口中,定义了两类发送状态码的方法。
(1)setStatus(int status)方法
该方法用于发送成功的状态码200,也可以用来发送302(重定向)。setStatus(int status)方法仅可以改变响应状态码而已。
另外,HttpServletResponse接口中原来还有一个setStatus(int sc, String sm)方法,现在已经废弃使用了。
(2)sendError(int sc)和sendError(int sc, String msg)方法
该方法用于发送表示错误信息的状态码,例如,404状态码表示找不到客户端请求的资源。
在response对象中,提供了两个重载的sendError()方法,具体如下:
public void sendError(int sc) throws IOException;
public void sendError(int sc, String msg) throws IOException;
在上面重载的两个方法中,第一个方法只是发送错误信息的状态码,而第二个方法除了发送状态码外,还可以增加一条用于提示说明的文本信息,该文本信息将出现在发送给客户端的正文内容中。
我们通过实例来学习一下如何发送404错误状态码。
创建一个名为RequestResponse的Dynamic Web Project(动态网页项目),创建一个名为com.sdbi.servlet的包,在包中建立一个AServlet类,继承于HttpServlet抽象类。编写代码如下:
public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.sendError(404, "您访问的资源存在,就是不给你看!"); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
将项目发布到Tomcat上,运行该Servlet,打开浏览器输入网址http://localhost:8080/RequestResponse/AServlet,会看到以下画面。
常用的状态码在HttpServletResponse接口中定义了一些响应状态码常量,我们可以通过查看HttpServletResponse接口源码或者JavaEE API帮助文档获得。
2、设置响应头信息
由于HTTP协议的响应头字段有很多种,例如:Content-Type、Refresh、Location等字段,为此,在HttpServletResponse接口中,定义了一系列设置HTTP响应头字段的方法。
响应头就是一个键值对,可能会存在一个头字段(一个键,一个值),也可能会存在一个头字段(一个键,多个值)。
- void setHeader(String name, String value):适用于单值的响应头,例如:
response.setHeader("aaa", "AAA");
- void addHeader(String name, String value):适用于多值的响应头,例如:
response.addHeader("aaa", "A"); response.addHeader("aaa", "AA"); response.addHeader("aaa", "AAA");
- void setIntHeader(String name, int value):适用于单值的int类型响应头,例如:
response.setIntHeader("Content-Length", 88);
- void addIntHeader(String name, int value):适用于多值的int类型响应头,例如:
response.addIntHeader("num", 56); response.addIntHeader("num", 78); response.addIntHeader("num", 99);
- void setDateHeader(String name, long date):适用于单值的毫秒类型的响应头,例如:
response.setDateHeader("Expires", 1000 * 60 * 60 * 24); // 设置页面过期时间为24小时
- void addDateHeader(String name, long date):适用于多值的毫秒类型的响应头。
- void setContentLength(int len):适用于设置响应实体内容的大小,例如:
response.setContentLength(88); // 等同于调用response.setIntHeader("Content-Length", 88);
- void setContentType(String type):适用于设置输出内容的MIME类型,例如:
response.setContentType("text/html;charset=utf-8");
// 等同于调用response.setHeader("Content-Type","text/html;charset=utf-8");
- void setLocale(Locale loc):适用于设置语系,Locale是Java SE中定义的类,该类包含两个信息,一个是语言,一个是地区,这两者共同组成Locale(语系)。Locale的构造器:Locale(String language, String country); 国际规范要求language和country都是特定的字符串,都是两个字符,并且language要求为小写,country要求为大写,比如zh-CN,其中语言zh表示中文(zh即“中文”的“中”字的拼音的头两个字母,而CN则是China的缩写,即中文-大陆地区,还比如zh-TW,即中文-台湾地区;因此可以这样构造:Locale locale = new Locale("zh", "CN");当然,Locale类也提供了几个预定义的常静态对象,比如Locale.CHINA就等价于Locale("zh", "CN")等。例如:
response.setLocale(Locale.CHINA); // 指定响应的语系为中文-大陆
- void setCharacterEncoding(String charset):适用于设置字符流的字符编码,例如:
response.setCharacterEncoding("utf-8"); // 保证输出给客户端的字符都是使用UTF-8编码的
但客户端浏览器并不知道响应数据是什么编码的!如果希望通知客户端使用UTF-8来解读响应数据,那么还是使用response.setContentType("text/html;charset=utf-8")方法比较好,因为这个方法不只会调用response.setCharacterEncoding("utf-8"),还会设置Content-Type响应头,客户端浏览器会使用Content-Type头来解读响应数据。
我们使用F12开发者工具可以查看指定的响应头,如下图所示。
3、设置响应正文
response是响应对象,向客户端输出响应正文(响应体)可以使用response的响应流,repsonse一共提供了两个响应流对象:
- PrintWriter out = response.getWriter():获取字符流;
- ServletOutputStream out = response.getOutputStream():获取字节流;
当然,如果响应正文内容为字符,那么使用response.getWriter();如果响应内容是字节,例如下载时,那么可以使用response.getOutputStream()。
注意,在一个响应中,不能同时使用这两个流!
也就是说,要么使用repsonse.getWriter(),要么使用response.getOutputStream(),但不能同时使用这两个流。
不然会抛出java.lang.IllegalStateException异常(非法语句异常)。
(1)response.getWriter()
public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String data = "Lihuawei"; PrintWriter out = response.getWriter(); out.println(data); } }
(2)response.getOutputStream()
public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String data = "Lihuawei"; ServletOutputStream out = response.getOutputStream(); out.write(data.getBytes()); } }
中文乱码问题
但是如果使用response.getWriter()方法输出中文时,就会出现乱码问题。因为计算机中的数据都是以二进制形式存储的,当传输文本时,就会发生字符和字节之间的转换。字符与字节之间的转换是通过查码表完成的,将字符转换成字节的过程称为编码,将字节转换成字符的过程称为解码。如果编码和解码使用的码表不一致,就会导致乱码问题。
为了解决这个问题,我们就需要用到response.setContentType("text/html;charset=utf-8")来告诉浏览器应该用哪个码表解码。修改代码如下:
public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); String data = "李华伟"; PrintWriter out = response.getWriter(); out.println(data); } }
4、重定向
重定向是Web服务器收到请求后,通知浏览器去访问另一个地址,即再发出另一个请求。
重定向的步骤,不分先后顺序。
(1)设置302状态码,setStatus(302)。
(2)设置Location响应头:“/项目名/Servlet路径”或“Servlet路径”(前面没有/),称为请求URL。
我们创建一个BServlet,在doGet()方法中添加一行输出语句,代码如下:
public class BServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("BServlet:doGet()..."); } }
回到AServlet中,在doGet()方法中修改代码,如下所示:
public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("AServlet:doGet()..."); response.setStatus(302); // 设置302状态码 response.setHeader("Location", "/RequestResponse/BServlet"); // 设置Location } }
发布运行程序,在浏览器地址栏里输入http://localhost:8080/RequestResponse/AServlet
点开第一个URL,查看“详细信息”中的“响应标头”,你会发现响应码为302,并且Location为设置好的新URL。
点开第一个URL,查看“详细信息”中的“响应标头”,你会发现响应码为200。
查看控制台输出。
快捷的重定向方式:
response.sendRedirect()方法会设置响应头为302,并设置Location响应头。例如:
response.sendRedirect("/RequestResponse/BServlet");
5、网页定时刷新并跳转
在Web开发中,有时会遇到定时跳转页面的需求。
在HTTP协议中,定义了一个Refresh头字段,它可以通知浏览器在指定的时间内自动刷新并跳转到其它页面。
(1)定时自动跳转
在AServlet中完成下面的代码:
public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setHeader("Refresh", "3;URL=http://www.sdbi.edu.cn"); // response.setHeader("Refresh", "3;URL=BServlet") } }
(2)定时自动刷新
在AServlet中完成下面的代码:
@WebServlet("/AServlet") public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setHeader("Refresh", "3"); // 设置时间间隔3秒钟 response.setContentType("text/html;charset=utf-8"); // 要在字符流输出之前调用 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); response.getWriter().println("当前时间:" + sdf.format(new java.util.Date())); } }
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
转换的格式:yyyy是完整的西元年,MM是月份,dd是日期, 至於HH:mm:ss.SSS 时分秒
注意:有的格式大写,有的格式小写,那是怕避免混淆。
例如,MM是月份,mm是分;
HH是24小时制,而hh是12小时制;
ss是秒,SSS是毫秒。
二、请求HttpServletRequest对象
在Servlet API中,定义了一个HttpServletRequest接口,它继承自ServletRequest接口,专门用来封装HTTP请求消息。由于HTTP请求消息分为请求行、请求消息头、请求消息体(请求正文,如果是GET请求,那么就没有正文)三部分,因此,在HttpServletRequest接口中定义了获取请求消息头、请求消息体(请求正文)的方法。
request是Servlet.service()方法的一个参数,类型为javax.servlet.http.HttpServletRequest。在客户端发出每个请求时,服务器都会创建一个request对象,并把请求数据封装到request中,然后在调用Servlet.service()方法时传递给service()方法,这说明在service()方法中可以通过request对象来获取请求数据。
request的功能可以分为以下几种:
- 请求行;
- 请求头数据;
- 请求正文数据,如果是GET请求,那么就没有正文;
- request提供了请求转发和请求包含功能。
- request是一个域对象,可以把它当成Map来添加获取数据;
1、获取请求行
当访问Servlet时,会在请求消息的请求行中,包含请求方法、资源名称和请求路径、协议等信息。
- String getMethod():获取请求方式
- String getRequestURI():获取请求资源名称,即Http请求行中间的那部分
例如:GET /RequestResponse/CServlet HTTP/1.1,也就是项目名+Servlet路径 - StringBuffer getRequestURL():获取URL(网址),包含协议、主机名、端口号、资源路径,即问号?之前的部分,不包含问号?
- String getQueryString():获取参数部分,即问号?之后的部分,不包含问号?
- String getContextPath():获取上下文路径,即项目名
- String getPathInfo():获取额外路径
- String getPathTranslated():获取额外路径所对应资源的真实路径
- String getServletPath():获取Servlet路径
- String getProtocol():获取请求协议和版本
- String getScheme():获取协议名
- String getServerName():获取主机名
- int getServerPort():获取端口号
【案例】在页面输出请求行中的数据。
新建一个名为CServlet的Servlet,继承于HttpServlet,我们使用response.getWriter()方式在页面上输出信息,调用上述的方法查看输出内容,具体代码如下:
@WebServlet("/CServlet") public class CServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); PrintWriter out = response.getWriter(); // 从响应中获取字符流对象 out.println("getMethod = " + request.getMethod() + "<br />"); out.println("getRequestURI = " + request.getRequestURI() + "<br />"); out.println("getRequestURL = " + request.getRequestURL() + "<br />"); out.println("getQueryString = " + request.getQueryString() + "<br />"); out.println("getProtocol = " + request.getProtocol() + "<br />"); out.println("getContextPath = " + request.getContextPath() + "<br />"); out.println("getPathInfo = " + request.getPathInfo() + "<br />"); out.println("getPathTranslated = " + request.getPathTranslated() + "<br />"); out.println("getServletPath = " + request.getServletPath() + "<br />"); out.println("getScheme = " + request.getScheme() + "<br />"); out.println("getServerName = " + request.getServerName() + "<br />"); out.println("getServerPort = " + request.getServerPort()); } }
发布运行程序,浏览器地址栏输入http://localhost:8080/RequestResponse/CServlet?username=lihuawei,显示如下:
2、获取请求头
request与请求头相关的方法有:
- Enumeration<String> getHeaderNames():获取所有请求头名称枚举;
- String getHeader(String name):获取指定名称的单值请求头;
- int getIntHeader(String name):获取值为int类型的单值请求头。
- long getDateHeader(String name):获取值为毫秒数的单值请求头
- String getContentType():获取Content-Type头字段值,请求类型,如果请求是GET,那么这个方法返回null;如果是POST请求,那么默认为application/x-www-form-urlencoded,表示请求体内容使用了URL编码;
- int getContentLength():获取Content-Length头字段值,请求体的字节数,GET请求没有请求体,没有请求体返回-1;
- String getCharacterEncoding():获取请求实体的字符集,如果没有setCharacterEncoding(),那么返回null,表示使用ISO-8859-1编码;
- void setCharacterEncoding(String code):设置请求编码,只对请求体有效!注意,对于GET而言,没有请求体!!!所以此方法只能对POST请求中的参数有效。
- Locale getLocale():获取当前客户端浏览器的Locale。java.util.Locale表示国家和言语,这个东西在国际化中很有用;
- String getRemoteAddr():获取当前客户端的IP地址;
- String getRemoteHost():获取当前客户端的主机名,但这个方法还是获取IP地址;
修改CServlet代码
@WebServlet("/CServlet") public class CServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { PrintWriter out = response.getWriter(); …… Enumeration<String> headerNames = request.getHeaderNames(); //请求头名称枚举 while (headerNames.hasMoreElements()) { String headerName = (String) headerNames.nextElement(); out.println(headerName + " = " + request.getHeader(headerName) + "<br>"); } out.println("getContentType = " + request.getContentType() + "<br>"); out.println("getContentLength = " + request.getContentLength() + "<br>"); out.println("getCharacterEncoding = " + request.getCharacterEncoding() + "<br>"); // 获取请求实体的字符集 request.setCharacterEncoding("UTF-8"); // 设置字符集UTF-8 out.println("getCharacterEncoding = " + request.getCharacterEncoding() + "<br>"); // 再次获取请求实体的字符集 out.println("getRemoteAddr = " + request.getRemoteAddr() + "<br>"); // 获取当前客户端的IP地址 out.println("getRemoteHost = " + request.getRemoteHost() + "<br>"); // 获取当前客户端的主机名 out.println("getLocale = " + request.getLocale() + "<br>"); // 获取当前客户端浏览器的Locale } }
发布运行程序,浏览器地址栏输入http://localhost:8080/RequestResponse/CServlet?username=lihuawei,显示如下:
【案例】利用request.getRemoteAddr()实现封IP的功能
可以使用request.getRemoteAddr()方法获取客户端的IP地址,然后判断IP是否为禁用IP。
核心功能代码如下:
String ip = request.getRemoteAddr(); response.setContentType("text/html;charset=utf-8");// 设置输出内容的MIME类型 System.out.println(ip); if (ip.equals("127.0.0.1")) { response.getWriter().println("您的IP已被禁止!"); } else { response.getWriter().println("Hello!"); }
注意,response.setContentType("text/html;charset=utf-8")语句要放在response.getWriter()之前,用于设置响应的内容类型,防止中文乱码。
运行发布程序,我们来测试一下。
使用localhost或者127.0.0.1访问,http://localhost:8080/RequestResponse/CServlet
使用IP地址访问,http://10.11.130.71:8080/RequestResponse/CServlet
3、获取请求参数(请求正文)
GET方式是没有请求正文的,POST方式的请求正文就是提交的请求参数。
所以我们这里主要是学习一下,这两种方式如何获取请求参数。
我们知道,最常用的客户端传递参数方式有三种:
- 浏览器地址栏直接输入:一定是GET请求;
- 超链接:一定是GET请求;
- 表单:可以是GET,也可以是POST,这取决于<form>的method属性值;
GET请求和POST请求的区别:
- GET请求:
- 请求参数会在浏览器的地址栏中显示,所以不安全;
- 请求参数长度限制长度在1K之内;
- GET请求没有请求体,无法通过request.setCharacterEncoding()来设置参数的编码;
- POST请求:
- 请求参数不会显示浏览器的地址栏,相对安全;
- 请求参数长度没有限制;
下面我们通过实例来学习一下这两种请求方式是如何传递参数的。
(1)创建一个名为hello.html的页面用于表单提交,代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>获取参数</title> </head> <body> <a href="./DServlet?p1=v1&p2=v2">超链接</a> <hr /> <form action="./DServlet" method="post"> 参数1:<input type="text" name="p1" /><br> 参数2:<input type="text" name="p2" /><br> <input type="submit" value="提交" /> </form> </body> </html>
(2)创建一个名为DServlet的Servlet,用于处理请求,代码如下:
@WebServlet("/DServlet") public class DServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String v1 = request.getParameter("p1"); String v2 = request.getParameter("p2"); response.getWriter().println("p1 = " + v1 + "<br>p2 = " + v2); System.out.println("p1 = " + v1); System.out.println("p2 = " + v2); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String v1 = request.getParameter("p1"); String v2 = request.getParameter("p2"); response.getWriter().println("p1 = " + v1 + "<br>p2 = " + v2); System.out.println("p1 = " + v1); System.out.println("p2 = " + v2); } }
部署发布项目,浏览器输入http://localhost:8080/RequestResponse/hello.html,可见到下面的页面。
点击“超链接”,由于咱们超链接的路径是“./DServlet?p1=v1&p2=v2"”,可以在控制台看到以下输出,并且页面也输出了参数信息,说明已经获取到了GET方式传递过来的参数。
地址栏输入http://localhost:8080/RequestResponse/DServlet?p1=def&p2=456,也可以获取GET方式传递过来的参数。
在“参数1”和“参数2”中分别输入“abc”和“123”,点击“提交”按钮。
可以在控制台看到以下输出,并且页面也输出了参数信息,说明已经获取到了POST方式传递过来的参数。
下面是从request获取请求参数的一些方法。大家注意,和请求参数相关的方法只有getXXX()方法,而没有setXXX()方法。因为不允许修改请求中的参数。
- String getParameter(String name):通过指定名称获取参数值;
- String[] getParameterValues(String name):当多个参数名称相同时,可以使用方法来获取;
String[] array1 = request.getParameterValues("p1");
System.out.println(java.util.Arrays.toString(array1));
response.getWriter().println("<br>p1 = " + java.util.Arrays.toString(array1));
将html中的超链接改为<a href="./DServlet?p1=v1&p1=vv1&p1=vvv1">超链接</a>
- Enumeration<String> getParameterNames():获取所有参数名字的集合;
Enumeration<String> names = request.getParameterNames(); while (names.hasMoreElements()) { String name = names.nextElement(); System.out.println(name); response.getWriter().println(name+"<br>"); }
将html中的超链接改为<a href="./DServlet?p1=v1&p2=v2&p3=v3">超链接</a>
- Map<String, String[]> getParameterMap():获取所有参数封装到Map中,其中key为参数名,value为参数值,因为一个参数名称可能有多个值,所以参数值是String[],而不是String。
Map<String, String[]> paramMap = request.getParameterMap(); for (String name : paramMap.keySet()) { String[] values = paramMap.get(name); System.out.println(name + " = " + java.util.Arrays.toString(values)); response.getWriter().println( name + " = "+java.util.Arrays.toString(values) + "<br>"); }
【案例】接下来我们结合之前学习的response重定向和获取request参数的编写一个用户登录的案例。
(1)编写用户登录的界面login.html和登录成功的界面welcome.html。
login.html代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>登录</title> </head> <body> <form action="./LoginServlet" method="POST"> 用户名:<input type="text" name="username" /><br> 密码:<input type="password" name="password" /><br> 兴趣:<input type="checkbox" name="interest" value="film" />看电影 <input type="checkbox" name="interest" value="code" />敲代码 <input type="checkbox" name="interest" value="game" />玩游戏<br> <input type="submit" value="登录" /> </form> </body> </html>
welcome.html代码如下:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>欢迎</title> </head> <body> 欢迎你,登录成功! </body> </html>
(2)编写处理用户登录请求的LoginServlet,代码如下:
@WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 获取请求中的所有参数并输出 Map<String, String[]> map = request.getParameterMap(); for (String name : map.keySet()) { String[] values = map.get(name); System.out.println(name + " : " + java.util.Arrays.toString(values)); } // 判断输入的用户名和密码 response.setContentType("text/html;charset=utf-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); System.out.println("username = " + username + ";password = " + password); if ("lihuawei".equals(username) && "123".equals(password)) { response.sendRedirect("./welcome.html"); // 跳转成功页面 } else { response.sendRedirect("./login.html"); // 返回登录页面 } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
(3)发布运行程序,在浏览器地址栏输入http://localhost:8080/RequestResponse/login.html,在登录界面,输入正确的用户名和密码,跳转到登录成功界面;如果用户名和密码错误,跳转至登录界面。
4、请求转发和请求包含
当我们的一个Servlet无法完成客户端的一个请求时,我们就需要使用请求转发或者请求包含,找其他的Servlet来一起完成这个请求。无论是请求转发还是请求包含,都表示由多个Servlet共同来处理同一个请求。例如,AServlet来处理请求,然后AServlet又转发给BServlet来继续处理这个请求。
要实现请求转发或者请求包含,我们需要用到RequestDispatcher接口,这个接口的功能就是将请求转发到另一个资源,另一个资源可以是HTML、Servlet或JSP等。需要注意的是,不管是请求转发或者请求包含,都是多个Servlet共享使用一个ServletRequest和一个ServletResponse。
RequestDispatcher接口中定义了两个方法:
- void forward(ServletRequest request, ServletResponse response)
- void include(ServletRequest request, ServletResponse response)
如何获取RequestDispatcher接口的对象呢?
我们需要使用ServletRequest对象的getRequestDispatcher(String path)方法就可以得到RequestDispatcher接口的对象,代码如下:
RequestDispatcher rd = request.getRequestDispatcher("/BServlet");
(1)请求转发
如果在AServlet中请求转发到BServlet,那么在AServlet中就不允许再输出响应体,即不能再使用response.getWriter()和response.getOutputStream()向客户端输出,这一工作应该由BServlet来完成,AServlet可以设置响应头。
在AServlet中,把请求转发到BServlet,代码如下:
@WebServlet("/AServlet") public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("AServlet:doGet()..."); response.setHeader("aaa", "AAA"); // 设置响应头 response.getWriter().println("AServlet"); // 设置响应体 request.getRequestDispatcher("/BServlet").forward(request, response); //请求转发 } }
BServlet中输出信息,并设置响应体。代码如下:
@WebServlet("/BServlet") public class BServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("BServlet:doGet()..."); response.getWriter().println("BServlet"); // 设置响应体 } }
发布运行程序,在浏览器地址栏里输入http://localhost:8080/RequestResponse/AServlet,会发现,地址栏路径部分不会变为/BServlet,但是页面显示为BServlet处理后的响应页面,虽然我们在AServlet中也使用了response.getWriter()来输出响应体,但是不起作用。另外,我们通过response.setHeader("aaa", "AAA")设置的响应头,可以从F12开发者工具--“响应标头”中看到。如下所示:
(2)请求包含
如果在AServlet中使用请求包含BServlet,那么是由这两个Servlet可以共同来完成响应体,相当于在AServlet中包含了BServlet的功能。
在AServlet中,把请求包含到BServlet,代码如下:
@WebServlet("/AServlet") public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("AServlet:doGet()..."); response.setHeader("aaa", "AAA"); // 设置响应头 response.getWriter().println("AServlet"); // 设置响应体 request.getRequestDispatcher("/BServlet").include(request, response); //请求包含 } }
BServlet中输出信息,并设置响应体。代码如下:
@WebServlet("/BServlet") public class BServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("BServlet:doGet()..."); response.getWriter().println("BServlet"); // 设置响应体 } }
发布运行程序,在浏览器地址栏里输入http://localhost:8080/RequestResponse/AServlet,会发现,地址栏路径部分不会变为/BServlet,但是这时客户端页面显示为AServlet和BServlet共同处理后的响应页面。如下所示:
【注意1】我们对于AServlet中的语句调整一下顺序,如果将AServlet中的设置响应体的语句放到请求包含之后,会发现响应体先由BServlet完成,再由AServlet完成。代码如下:
@WebServlet("/AServlet") public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("AServlet:doGet()..."); request.getRequestDispatcher("/BServlet").include(request, response); response.setHeader("aaa", "AAA"); // 设置响应头 response.getWriter().println("AServlet"); // 设置响应体 } }
【注意2】对于请求转发,如果在AServlet中设置了响应体,Web服务器也不会执行AServlet中设置响应体的语句。但是,如果在AServlet中设置的响应体比较多,我们可以从浏览器中看到AServlet中设置的响应体,但是程序也会抛出异常。例如以下代码:
@WebServlet("/AServlet") public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("AServlet:doGet()..."); response.setHeader("aaa", "AAA"); for (int i = 0; i < 1024 * 24 + 1; i++) { // 24k + 1字节:一个a一个字节,1024字节*24,再加一个字节 response.getWriter().print("a"); // 设置响应体 } request.getRequestDispatcher("/BServlet").forward(request, response); } }
这里需要大家记住,如果AServlet中可以做很多工作,就不需要再转发给BServlet来帮忙一起完成响应了。
(3)请求转发与请求包含比较
- 如果在AServlet中请求转发(forward)到BServlet,那么在AServlet中就不允许再输出响应体,即不能再使用response.getWriter()和response.getOutputStream()向客户端输出,这一工作应该由BServlet来完成,AServlet可以设置响应头。例如:response.setContentType(”text/html;charset=utf-8”);(留头不留体);
- 如果是使用请求包含(include),那么没有这个限制,两个Servlet可以共同来完成响应体(留头又留体);
- 无论是请求转发还是请求包含,都在一个请求范围内,使用同一个request和response。
(4)请求转发与重定向比较
- 请求转发是一个请求一次响应,而重定向是两次请求两次响应;
- 请求转发地址栏不变化,而重定向会有变化,重定向会显示后一个请求的地址,因为重定向是两个请求;
- 请求转发的目标只能是本项目中的资源,重定向的目标不仅可以是本项目中的其他资源还可以是其他项目中的资源,甚至于可以是其他服务器上的资源;
- 请求转发对AServlet和BServlet的请求方法是相同的,即要么都是GET,要么都是POST,因为请求转发是一个请求;
- 重定向的第二次请求一定是GET,第一次请求不确定,可以是GET,也可以是POST;
- 请求转发是服务器端行为,只需给出转发的Servlet路径,而重定向是客户端重新请求,需要给出requestURI,即包含项目名!
- 请求转发与重定向相比,请求转发的效率高!因为是一个请求!
5、request域
JavaWeb四大域对象:PageContext、ServletRequest、HttpSession、ServletContext都有如下三个方法:
- void setAttribute(String name, Object value)
- Object getAttribute(String name)
- void removeAttribute(String name)
在同一请求范围内,可以使用request.setAttribute()、request.getAttribute()来传值。前一个Servlet调用setAttribute()保存值,后一个Servlet调用getAttribute()获取值。
我们修改AServlet,使用request.setAttribute()方法向request域中设置要传递的数据。
@WebServlet("/AServlet") public class AServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("AServlet:doGet()..."); request.setAttribute("name", "lihuawei"); request.getRequestDispatcher("/BServlet").forward(request, response); } }
修改BServlet代码,使用request.getAttribute()方法从request域中获取传递过来的数据。
@WebServlet("/BServlet") public class BServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("BServlet:doGet()..." + request.getAttribute("name")); response.getWriter().println("BServlet"); } }
运行发布程序,浏览器访问http://localhost:8080/RequestResponse/AServlet,我们可以看到,在BServlet中获得了AServlet传递过来的数据。
下面就是请求转发时设置数据的示意图。