在前几篇博文中我们学习了Servlet的基础知识,包括HttpServletRequest和HttpServletResponse对象,解析了Servlet是如何通过这一对黄金搭档来实现自身作为Controller的功能的。今天我们就来看看这两"兄弟"还可实现的另一功能。
在某些情况下,对于客户端的某些请求,这个Servlet不想进行响应或者自己无法处理,就可以通过重定向、转发或包含来将此次请求转交给其他Serlvet来处理,我们通过下面一个小场景先来简单的了解下这三者的区别。
一个小栗子:在企业A中,领导大黄有一个开发任务要分配下去,于是他喊来员工小李,说:“我这里有个需求…巴拉巴拉…”,那小李收到需求后,不想做或者做不了,那要他怎么处理呢?
我们来看一看小李的几种处理方案。
1.直接拒绝,推荐他人顶替(请求重定向)
从图中我们通过对话(????箭头)可以看到,当领导大黄把任务分配给小李时,小李直接告诉大黄自己没空,让他去找小龙;于是大黄又找了小龙,并且把刚才给小李说过的话重新说了一遍,小龙收到需求后,加班加点把功能完成后,并直接向领导汇报,工作完成了。
这也是重定向的思想,服务器告诉浏览器一个新的请求地址,浏览器再向新地址发起一次Http请求,即发起了两次Http请求(需求说了两遍);因为是浏览器重新发起一次请求,浏览器上的URL也发生变化,显示第二次请求的url(小龙);因为发起了两次Http请求,Serlvet容器也相应的分别为之创建ServletRequest和ServletResponse,因此两次请求收到的request、response是不同的。
下面我们来一起来看下在Servlet中是如何实现请求重定向的。我们新建个Servlet,命名为JumpTestServlet,其中的doGet()方法中代码如下:
"/JumpTestServlet")(
public class JumpTestServlet extends HttpServlet {
//...
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置返回客户端的contentType
response.setContentType("text/html;charset=utf-8");
//获取PrintWriter输出对象
PrintWriter writer = response.getWriter();
writer.println("我没空,你去找小龙吧!");
//相对路径
response.sendRedirect("./AnswerServlet");
//绝对路径
//response.sendRedirect("/FirstProject/AnswerServlet");
//外部路径
//response.sendRedirect("http://localhost:8080/FirstProject/AnswerServlet");
}
//doPost()
//...
}
为了更好的模拟场景,我们对AnswerServlet的doGet()方法也进行了修改,代码如下:
"/AnswerServlet")(
public class AnswerServlet extends HttpServlet {
//...
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置返回客户端的contentType
response.setContentType("text/html;charset=utf-8");
获取PrintWriter输出对象
PrintWriter out = response.getWriter();
out.println("好的,功能已经开发完毕!");
}
//doPost()
//...
}
浏览器地址栏中输入http://localhost:8080/FirstProject/JumpTestServlet,运行结果为下图所示,我们可以看到,浏览器中url已经发生了改变,显示的是JumpTestServlet(小李)给其指定的url----AnswerServlet(小龙)。
为了探究两次请求(小李+小龙)给浏览器(领导大黄)的响应,我们来分别查看下这两次网络请求,首先下图是JumpTestServlet(小李),我们可以看到其响应状态码为302,响应头中增加了一个Location的header,通过Location来告知大黄去找小龙。但是注意绿框部分,返回的消息体的长度为0,即表示小李给领导大黄说的"我没空,你去找小龙吧!",领导大黄并没有接收到。
第二次对AnswerServlet(小龙)的访问就比较熟悉了,熟悉的绿灯泡旁的200字样,熟悉的Servlet的给出的响应被接收(Content-Length: 37)。
我们在再分析这两次请求的请求头、请求体,可以发现这两次Http请求除了请求行不一样外,其他都是相同的,也就是领导大黄对着两个人说了两遍一模一样的话。
为了加深理解,我们来看下HttpServletResponse接口中sendRedirect()源码中的解释,代码如下:
/**
* Sends a temporary redirect response to the client using the
* specified redirect location URL and clears the buffer. The buffer will
* be replaced with the data set by this method. Calling this method sets the
* status code to {@link #SC_FOUND} 302 (Found).
* This method can accept relative URLs;the servlet container must convert
* the relative URL to an absolute URL
* before sending the response to the client. If the location is relative
* without a leading '/' the container interprets it as relative to
* the current request URI. If the location is relative with a leading
* '/' the container interprets it as relative to the servlet container root.
*
* <p>If the response has already been committed, this method throws
* an IllegalStateException.
* After using this method, the response should be considered
* to be committed and should not be written to.
*
* @param location the redirect location URL
* @exception IOException If an input or output exception occurs
* @exception IllegalStateException If the response was committed or
if a partial URL is given and cannot be converted into a valid URL
*/
public void sendRedirect(String location) throws IOException;
通过上面部分,我们可以看到,sendRedirect会使用指定的重定向location(入参)的URL向客户端发送临时重定向响应,并清除缓冲区(这也是为什么小李说的"我没空,你去找小龙吧!"没有传到客户端)。sendRedirect方法会将状态代码设置为 302(SC_FOUND)。
sendRedirect可以接受相对路径和绝对路径,如果是一个相对路径,在发送至客户端前,Servlet容器会将相对的URL转换为绝对的URL。如果location是相对的而没有以’’\’‘开头,则Servlet容器会将其解释为相对于当前请求URI的相对位置,如果location有以’’\’'开头,则容器将其解释为相对于Servlet容器的根,即解释为"localhost:8080/",注意,此处没有项目名了,和ServletContext的根路径不同。sendRedirect还可接收一个以"http"或"https"开头的location,表明其可以重定向到外部应用中。
这里需要注意,在调用sendRedirect方法前,response如果已经被提交,此方法就会抛出一个IllegalStateException,这也和一个Http请求只能有一个对应的Http响应吻合。并且,在调用了sendRedirect后,Servlet中就不应该在通过response向客户端写入数据了。
对于这里的相对、绝对的概念,我们不必深究,因为相对和绝对也都是个相对的概念。你可以有两种理解:
- 相对路径为没有以’’\’‘开头,即以"./“或”…/"开头的location;绝对路径为以’’\’'开头的location;以"http"或"https"开头的location为外部路径。这其中的相对就可以理解为相对于当前请求。
- 所有没有以"http"或"https"开头的location为相对路径,而以"http"或"https"开头的location为绝对路径。这其中的相对就可以理解为相对于整个Servlet容器的。
这两种方式只要理解一种即可,不必过多的纠结。
2.外包出去,做个中间商(请求转发)
从图中我们通过对话可以看到,当领导大黄把任务分配给小李时,小李自己不想做,但是他没有告诉大黄,而是转头找到了小龙,并且把刚才大黄说过的话重新给小龙说了一遍(转述),小龙收到需求后,加班加点把功能完成后,然后告知领导大黄,工作已经完成了。注意,这里有点需要注意,图中所示是小龙这里直接告诉的领导,工作完成了,但是因为这项工作是小李委托给小龙来做的,领导大黄并不知情,所以大黄只会拍着小李的肩膀对他说:“小伙子,干的不错,有前途!”。至于小龙,只能呵呵了。
我们将JumpTestServlet的doGet()方法修改如下,来看下请求转发的执行,代码如下:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置返回客户端的contentType
response.setContentType("text/html;charset=utf-8");
//获取PrintWriter输出对象
PrintWriter writer = response.getWriter();
//这里暂且不提,我们后面再分析,为什么浏览器上没有显示这句话
writer.println("我就是想说句话,看看浏览器能接收到么!");
//相对路径
request.getRequestDispatcher("./AnswerServlet").forward(request, response);
//绝对路径
//request.getRequestDispatcher("/AnswerServlet").forward(request, response);
}
浏览器地址栏中输入http://localhost:8080/FirstProject/JumpTestServlet,运行结果如下:
从上图可以看到,浏览器中的url并未发生变化, 对于领导大黄来讲,他一直都以为是小李在做这项工作;而最后小龙加班加点后完成的工作(“好的,功能已开发完毕!”)全部归功于小李。
总结下请求转发,对于客户端(浏览器,领导大黄)来讲,他们不知道自己的Http请求被转发出去了,归根的原因就是在转发时,其将Serlvet容器为这次Http请求创建的HttpServletRequest、HttpServletResponse传递给了下个Servlet。
为了加深理解,我们来看下接口RequestDispatcher源码中forward()的解释,代码如下:
/**
* Forwards a request from
* a servlet to another resource (servlet, JSP file, or
* HTML file) on the server. This method allows
* one servlet to do preliminary processing of
* a request and another resource to generate
* the response.
*
* <p>For a <code>RequestDispatcher</code> obtained via
* <code>getRequestDispatcher()</code>, the <code>ServletRequest</code>
* object has its path elements and parameters adjusted to match
* the path of the target resource.
*
* <p><code>forward</code> should be called before the response has been
* committed to the client (before response body output has been flushed).
* If the response already has been committed, this method throws
* an <code>IllegalStateException</code>.
* Uncommitted output in the response buffer is automatically cleared
* before the forward.
*
* <p>The request and response parameters must be either the same
* objects as were passed to the calling servlet's service method or be
* subclasses of the {@link ServletRequestWrapper} or
* {@link ServletResponseWrapper} classes
* that wrap them.
*
* <p>This method sets the dispatcher type of the given request to
* <code>DispatcherType.FORWARD</code>.
*
* @param request a {@link ServletRequest} object that represents the
* request the client makes of the servlet
*
* @param response a {@link ServletResponse} object that represents
* the response the servlet returns to the client
*
* @throws ServletException if the target resource throws this exception
*
* @throws IOException if the target resource throws this exception
*
* @throws IllegalStateException if the response was already committed
*
* @see ServletRequest#getDispatcherType
*/
public void forward(ServletRequest request, ServletResponse response)
throws ServletException, IOException;
上面内容很多哈,我们对其中的重点进行解释。forward方法只允许Servlet将请求转发到当前应用中的另一个资源,包括Servlet、Jsp、Html。forward方法在调用前一样不允许Response已经被提交,否则也会抛出异常,并且在调用forward会自动清空Response中未提交的输出数据,因此在调用forward中的Servlet中只可对request进行初步的处理,而不能向客户端发出响应(如果发了要么被清空,要么就会抛出异常)。
3.找来外援,"强"强联合(请求包含)
从图中我们通过对话可以看到,当领导大黄把任务分配给小李时,小李自己一个人无法完成所有的工作,但是他没有告诉大黄,而是转头找到了小龙,告诉他这个需求,并且自己已经完成了一部分,剩下的咱们一起来做吧,小龙听后,欣然接受,于是两人合力,加班加点把功能完成,然后告知领导大黄,工作已经完成了。注意,这里和请求转发一样,工作完成了,但是因为这项工作是小李私下邀请小龙来帮忙的,领导大黄并不知情,所以大黄只会再次拍着小李的肩膀对他说:“小伙子,干的不错,有前途!”。至于小龙,又尴尬了一波。
我们将JumpTestServlet的doGet()方法修改如下,来看下请求包含的执行,代码如下:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置返回客户端的contentType
response.setContentType("text/html;charset=utf-8");
//获取PrintWriter输出对象
PrintWriter writer = response.getWriter();
//这里暂且不提,我们后面再分析,为什么浏览器上没有显示这句话
writer.println("我就是想说句话,看看浏览器能接收到么!");
//相对路径
request.getRequestDispatcher("./AnswerServlet").include(request, response);
//绝对路径
//request.getRequestDispatcher("/AnswerServlet").include(request, response);
}
浏览器地址栏中输入http://localhost:8080/FirstProject/JumpTestServlet,运行结果如下:
从上图可以看到,浏览器中的url并未发生变化,因此领导大黄仍是不知内部的变化。不过请求包含和请求转发还是有些诧异的,比如上图中,JumpTestServlet中输出的"我就是想说句话,看看浏览器能接收到么!",和AnswerServlet中输出的"好的,功能已经开发完毕!"一同返回给了客户端。
请求转发和请求包含为何会有如此的区别呢?这是因为在请求转发前,会自动的将所有的response中输出缓冲区中未提交的数据全部清空。而请求包含呢,则会保留缓冲区中未提交的输出数据。
为了加深理解,我们来看下接口RequestDispatcher源码中include()的解释,代码如下:
/**
*
* Includes the content of a resource (servlet, JSP page,
* HTML file) in the response. In essence, this method enables
* programmatic server-side includes.
*
* <p>The {@link ServletResponse} object has its path elements
* and parameters remain unchanged from the caller's. The included
* servlet cannot change the response status code or set headers;
* any attempt to make a change is ignored.
*
* <p>The request and response parameters must be either the same
* objects as were passed to the calling servlet's service method or be
* subclasses of the {@link ServletRequestWrapper} or
* {@link ServletResponseWrapper} classes that wrap them.
*
* <p>This method sets the dispatcher type of the given request to
* <code>DispatcherType.INCLUDE</code>.
*
* @param request a {@link ServletRequest} object that contains the
* client's request
*
* @param response a {@link ServletResponse} object that contains the
* servlet's response
*
* @throws ServletException if the included resource throws this
* exception
*
* @throws IOException if the included resource throws this exception
*
* @see ServletRequest#getDispatcherType
*/
public void include(ServletRequest request, ServletResponse response)
throws ServletException, IOException;
因为include和forward同属于RequestDispatcher中的方法,因此他俩差别不大。include方法同样只允许Servlet将请求转发到服务上的另一个资源;但是include方法跳转的Servlet无法修改resonse的响应状态(改了也不会生效);不过此方法会保留缓冲区中未提交的输出数据。
4.几种方式的区别
下面我们在通过几个在Serlvet容器中运行的示意图来看一下几种跳转方式的执行过程。
请求重定向的执行过程如下:
请求转发的执行过程如下:
请求包含执行过程如下:
上面也讨论了这么多了,下面我们对三种方式的区别来进行一个总结,结果如下表所示:
表现 | 请求重定向 | 请求转发 | 请求包含 |
浏览器中的url是否会发生变化 | 是 | 否 | ←同左 |
浏览器是否知道第二次请求是谁执行的 | 是 | 否 | ←同左 |
客户端发起了几次Http请求 | 2次以上(含2次) | 1次 | ←同左 |
两个Servlet间可以共享request和response么 | 不可以 | 可以 | ←同左 |
可以跳转的资源范围 | 当前应用的其他资源、当前tomcat上的其他应用、其他主机上的应用 | 当前应用的其他资源 | ←同左 |
支持的location的格式 | | | ←同左 |
localtion以 | 当前serlvet容器的根目录(到端口号后) | 当前Servlet上下文的根目录(到项目名) | ←同左 |
执行跳转的Servet可以输出信息到客户端么 | 不可以 | 不可以 | 可以,两个Servlet会将输出合并为一个响应给客户端 |
5.总结
看完本文,是不是对小李觉得特别气,当然这里只是举了这么一个小栗子,能帮助我们理解重定向、转发、包含之间的区别就好。这篇也不是什么鸡汤文,还是实实在在的技术问哈????。
这几种方式没有什么绝对的好坏之分,使用转发能稍微提高一丢丢的性能,因为客户端只发起了一次请求,对于客户端来讲也更省心;但是当需要跳转到应用之外时,就必须要使用重定向了。具体使用哪种跳转方式,还需大家自己评估。