请求重定向、请求转发、请求包含的特点与区别详解

​ 在前几篇博文中我们学习了Servlet的基础知识,包括HttpServletRequest和HttpServletResponse对象,解析了Servlet是如何通过这一对黄金搭档来实现自身作为Controller的功能的。今天我们就来看看这两"兄弟"还可实现的另一功能。

​ 在某些情况下,对于客户端的某些请求,这个Servlet不想进行响应或者自己无法处理,就可以通过重定向、转发或包含来将此次请求转交给其他Serlvet来处理,我们通过下面一个小场景先来简单的了解下这三者的区别。

一个小栗子:在企业A中,领导大黄有一个开发任务要分配下去,于是他喊来员工小李,说:“我这里有个需求…巴拉巴拉…”,那小李收到需求后,不想做或者做不了,那要他怎么处理呢?

​ 我们来看一看小李的几种处理方案。

1.直接拒绝,推荐他人顶替(请求重定向)

请求重定向、请求转发、请求包含的特点与区别详解_第1张图片

​ 从图中我们通过对话(箭头)可以看到,当领导大黄把任务分配给小李时,小李直接告诉大黄自己没空,让他去找小龙;于是大黄又找了小龙,并且把刚才给小李说过的话重新说了一遍,小龙收到需求后,加班加点把功能完成后,并直接向领导汇报,工作完成了。

​ 这也是重定向的思想,服务器告诉浏览器一个新的请求地址,浏览器再向新地址发起一次Http请求,即发起了两次Http请求(需求说了两遍);因为是浏览器重新发起一次请求,浏览器上的URL也发生变化,显示第二次请求的url(小龙);因为发起了两次Http请求,Serlvet容器也相应的分别为之创建ServletRequest和ServletResponse,因此两次请求收到的request、response是不同的

​ 下面我们来一起来看下在Servlet中是如何实现请求重定向的。我们新建个Servlet,命名为JumpTestServlet,其中的doGet()方法中代码如下:

@WebServlet("/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()方法也进行了修改,代码如下:

@WebServlet("/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(小龙)。

请求重定向、请求转发、请求包含的特点与区别详解_第2张图片

​ 为了探究两次请求(小李+小龙)给浏览器(领导大黄)的响应,我们来分别查看下这两次网络请求,首先下图是JumpTestServlet(小李),我们可以看到其响应状态码为302,响应头中增加了一个Location的header,通过Location来告知大黄去找小龙。但是注意绿框部分,返回的消息体的长度为0,即表示小李给领导大黄说的"我没空,你去找小龙吧!",领导大黄并没有接收到。

请求重定向、请求转发、请求包含的特点与区别详解_第3张图片

​ 第二次对AnswerServlet(小龙)的访问就比较熟悉了,熟悉的绿灯泡旁的200字样,熟悉的Servlet的给出的响应被接收(Content-Length: 37)。

请求重定向、请求转发、请求包含的特点与区别详解_第4张图片

​ 我们在再分析这两次请求的请求头、请求体,可以发现这两次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.
     *
     * 

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向客户端写入数据了。

​ 对于这里的相对、绝对的概念,我们不必深究,因为相对和绝对也都是个相对的概念。你可以有两种理解:

  1. 相对路径为没有以’’\’‘开头,即以"./“或”…/"开头的location;绝对路径为以’’\’'开头的location;以"http"或"https"开头的location为外部路径。这其中的相对就可以理解为相对于当前请求
  2. 所有没有以"http"或"https"开头的location为相对路径,而以"http"或"https"开头的location为绝对路径。这其中的相对就可以理解为相对于整个Servlet容器的。

​ 这两种方式只要理解一种即可,不必过多的纠结。

2.外包出去,做个中间商(请求转发)

请求重定向、请求转发、请求包含的特点与区别详解_第5张图片

​ 从图中我们通过对话可以看到,当领导大黄把任务分配给小李时,小李自己不想做,但是他没有告诉大黄,而是转头找到了小龙,并且把刚才大黄说过的话重新给小龙说了一遍(转述),小龙收到需求后,加班加点把功能完成后,然后告知领导大黄,工作已经完成了。注意,这里有点需要注意,图中所示是小龙这里直接告诉的领导,工作完成了,但是因为这项工作是小李委托给小龙来做的,领导大黄并不知情,所以大黄只会拍着小李的肩膀对他说:“小伙子,干的不错,有前途!”。至于小龙,只能呵呵了。

​ 我们将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,运行结果如下:

请求重定向、请求转发、请求包含的特点与区别详解_第6张图片

​ 从上图可以看到,浏览器中的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.
     *
     * 

For a RequestDispatcher obtained via * getRequestDispatcher(), the ServletRequest * object has its path elements and parameters adjusted to match * the path of the target resource. * *

forward 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 IllegalStateException. * Uncommitted output in the response buffer is automatically cleared * before the forward. * *

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. * *

This method sets the dispatcher type of the given request to * DispatcherType.FORWARD. * * @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.找来外援,"强"强联合(请求包含)

请求重定向、请求转发、请求包含的特点与区别详解_第7张图片

​ 从图中我们通过对话可以看到,当领导大黄把任务分配给小李时,小李自己一个人无法完成所有的工作,但是他没有告诉大黄,而是转头找到了小龙,告诉他这个需求,并且自己已经完成了一部分,剩下的咱们一起来做吧,小龙听后,欣然接受,于是两人合力,加班加点把功能完成,然后告知领导大黄,工作已经完成了。注意,这里和请求转发一样,工作完成了,但是因为这项工作是小李私下邀请小龙来帮忙的,领导大黄并不知情,所以大黄只会再次拍着小李的肩膀对他说:“小伙子,干的不错,有前途!”。至于小龙,又尴尬了一波。

​ 我们将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,运行结果如下:

请求重定向、请求转发、请求包含的特点与区别详解_第8张图片

​ 从上图可以看到,浏览器中的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.
     *
     * 

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. * *

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. * *

This method sets the dispatcher type of the given request to * DispatcherType.INCLUDE. * * @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容器中运行的示意图来看一下几种跳转方式的执行过程。

​ 请求重定向的执行过程如下:

请求重定向、请求转发、请求包含的特点与区别详解_第9张图片

​ 请求转发的执行过程如下:

请求重定向、请求转发、请求包含的特点与区别详解_第10张图片

​ 请求包含执行过程如下:

请求重定向、请求转发、请求包含的特点与区别详解_第11张图片

​ 上面也讨论了这么多了,下面我们对三种方式的区别来进行一个总结,结果如下表所示:

表现 请求重定向 请求转发 请求包含
浏览器中的url是否会发生变化 ←同左
浏览器是否知道第二次请求是谁执行的 ←同左
客户端发起了几次Http请求 2次以上(含2次) 1次 ←同左
两个Servlet间可以共享request和response么 不可以 可以 ←同左
可以跳转的资源范围 当前应用的其他资源、当前tomcat上的其他应用、其他主机上的应用 当前应用的其他资源 ←同左
支持的location的格式 \xxx,.\xxx, ..\xxx,http://,https:// \xxx,.\xxx, ..\xxx ←同左
localtion以\开头相对的位置 当前serlvet容器的根目录(到端口号后) 当前Servlet上下文的根目录(到项目名) ←同左
执行跳转的Servet可以输出信息到客户端么 不可以 不可以 可以,两个Servlet会将输出合并为一个响应给客户端

5.总结

​ 看完本文,是不是对小李觉得特别气,当然这里只是举了这么一个小栗子,能帮助我们理解重定向、转发、包含之间的区别就好。这篇也不是什么鸡汤文,还是实实在在的技术问哈。

​ 这几种方式没有什么绝对的好坏之分,使用转发能稍微提高一丢丢的性能,因为客户端只发起了一次请求,对于客户端来讲也更省心;但是当需要跳转到应用之外时,就必须要使用重定向了。具体使用哪种跳转方式,还需大家自己评估。


​ 又到了分隔线以下,本文到此就结束了,本文内容全部都是由博主自己进行整理并结合自身的理解进行总结,如果有什么错误,还请批评指正。

​ Java web这一专栏会是一个系列博客,喜欢的话可以持续关注,如果本文对你有所帮助,还请还请点赞、评论加关注。

​ 有任何疑问,可以评论区留言。

你可能感兴趣的:(Java,Web)