Tomcat与JavaWeb 2.4 Servlet的转发、包含、重定向

1.    转发与包含

Servlet对象由容器创建,并且Servlet对象的service()方法也由容器调用。一个Servlet对象可否直接调用另一个Servlet对象的service()方法呢?答案是否定的,因为一个Servlet对象无法获得另一个Servlet对象的引用。

  • 请求转发:Servlet(源组件)先对客户请求做一些预处理操作,然后把请求转发给其他Web组件(目标组件)来完成包括生成相应结果在内的后续操作。
  • 包含:Servlet(源组件)把其他Web组件(目标组件)生成的响应结果包含到自身的响应结果中。
Web应用在响应客户端的一个请求时,有可能响应过程很复杂,需要多个Web组件共同协作,才能生成响应结果。尽管一个Servlet对象无法直接调用另一个Servlet对象的service()方法,但Servlet规范为Web组件之间的协作提供了两种途径。

请求转发与包含具有以下共同特点:

  • 源组件和目标组件处理的都是同一个客户请求,源组件和目标组件共享同一个ServletRequest对象和ServletResponse对象。
  • 目标组件都可以为Servlet、JSP或HTML文档。
  • 都依赖javax.servlet.RequestDispatcher接口。

javax.servlet.RequestDispatcher接口表示请求分发器,它有两个方法:

  • forward(ServletRequest req,ServletResponse resp):把请求转发给目标组件。
  • include(ServletRequest req,ServletResponse resp):包含目标组件的响应结果。

当Servlet源组件调用RequestDispatcher的forward()或include()方法时,都要把当前的ServletRequest和ServletResponse对象作为参数传给该方法,这使得源组件和目标组件共享同一个ServletRequest和ServletResponse对象。

Servlet可通过两种方式得到RequestDispatcher对象:

  • 调用ServletContext的getRequestDispatcher(String path)方法,path参数指定目标组件的路径。
  • 调用ServletRequest的getRequestDispatcher(String path)方法,path参数指定目标组件的路径。

以上两种方式的区别在于,前者的path参数必须为绝对路径,而后者的path参数可以为相对路径。所谓绝对路径就是指以符号"/"开头的路径,"/"表示当前Web应用的URL入口。所谓相对路径就是指相对于当前源Servlet组件的路径,不以符号"/"开头。

1.1    请求转发

下面例程的CheckServlet类与OutputServlet类之间互为请求转发关系。在web.xml中,为CheckServlet和OutputServlet映射的URL分别是"/check","/output"。

CheckServlet.java

import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;

public class CheckServlet extends GenericServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException,IOException{
        //读取用户名
        String username = servletRequest.getParameter("username");
        String message = null;
        if (username == null){
            message = "Please input username.";
        }else {
            message = "Hello,"+username;
        }
        //在request对象中添加msg属性
        servletRequest.setAttribute("msg",message);

        //把请求转发给OutputServlet
        ServletContext context = getServletContext();
        RequestDispatcher dispatcher = context.getRequestDispatcher("/output"); //正确的用法
        //RequestDispatcher dispatcher = context.getRequestDispatcher("output"); // 错误的用法
        //RequestDispatcher dispatcher = servletRequest.getRequestDispatcher("output"); //正确的用法

        PrintWriter out = servletResponse.getWriter();
        out.println("Output from CheckServlet before forwarding request.");
        System.out.println("Output from CheckServlet before forwarding request.");
        //out.close();     //如果在dispatch.forwar()之前关闭输出流,那么客户端会显示该信息。

        dispatcher.forward(servletRequest,servletResponse);

        out.println("Output from CheckServlet after forwarding request.");
        System.out.println("Output from CheckServlet after forwarding request.");
    }
}
OutputServlet.java
import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class OutputServlet extends GenericServlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException,IOException{

        //读取CheckServlet存放在请求范围内的消息
        String messaga = (String )servletRequest.getAttribute("msg");
        PrintWriter out = servletResponse.getWriter();

        out.println(messaga);
        out.close();
    }
}

运行应用,访问localhost:8080/check?username=Tomcat,会看到页面出现"Hello,Tomcat",而并未出现CheckServlet的out所输出的内容。由此可见,在Servlet源组件中调用dispatch.forward()方法之后,代码虽然会被执行(因为控制台确实打印出了对应的信息),但是生成的响应结果不会被发送到客户端。

此时,修改CheckServlet的代码,在CheckServlet类调用dispatch.forward()之前先关闭输出流:

        System.out.println("Output from CheckServlet before forwarding request.");
        out.close();     //如果在dispatch.forwar()之前关闭输出流,那么客户端会显示该信息。

        dispatcher.forward(servletRequest,servletResponse);
此时再运行应用,访问相同的地址,会发现浏览器端仅接收到了由CheckServlet输出的内容:
Output from CheckServlet before forwarding request.

这是因为CheckServlet的out.close()方法先把CheckServlet输出的内容提交给客户端,然后再关闭输出流。接下来调用dispatch.forward(request,response)方法会抛出异常,在日志中记录下来。

1.2    包含

下面的MainServlet类把header.html的内容、GreetServlet生成的响应正文、以及foot.html的内容都包含到自己的响应结果中。也就是说,MainServlet返回给客户度端的HTML文档是由MainServlet本身、header.html、GreetServket、foot.html共同产生的。

MainServlet.java

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class MainServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        /*输出HTML文档*/
        PrintWriter out = resp.getWriter();
        out.println("<html><head><title>MainServletPage</title></head>");
        out.println("<body>");
        ServletContext context = getServletContext();
        RequestDispatcher headDispatcher = context.getRequestDispatcher("/header.html");
        RequestDispatcher greetDispatcher = context.getRequestDispatcher("/greet");
        RequestDispatcher footDispatcher = context.getRequestDispatcher("/footer.html");

        //包含三个目标组件
        headDispatcher.include(req,resp);
        greetDispatcher.include(req,resp);
        footDispatcher.include(req,resp);

        out.println("</body></html>");
        out.close();
    }
}
GreetServlet.java
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class GreetServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        out.println("Hi,"+req.getParameter("username")+"<P>");
    }
}
header.html
Welcome to ABC Inc.(From header.html)
<hr width="50%" align="left" >
footer.html
Welcome to ABC Inc.(From header.html)
<hr width="50%" align="left" >
然后在web.xml中把两个Servlet的URL映射分别配置为"/main"和"/greet",通过浏览器访问localhost:8080/main?username=Tomcat,得到的HTML页面如图所示:

现在我们只是简单地学习包含关系的使用,后面会通过JSP的例子来介绍如何利用包含关系来提高代码的可重性。

1.3    请求范围

1.3节中我们学习了Web应用范围的概念。Web应用范围是指整个Web应用的生命周期,在具体实现上,Web应用范围与ServletContext对象的生命周期对应,Web应用范围内的共享数据作为ServletContext对象的属性而存在,因此Web组件只要共享同一个ServletContext对象,也就能共享Web应用范围内的共享数据。

类似地,请求范围是指服务器端响应一次客户请求的过程,从Servlet容器接收到一个客户请求开始,到返回响应结果结束。在具体实现上,请求范围与ServletRequest对象以及ServletResponse对象的生命周期对应。

Servlet容器每次接收到一个客户请求,都会先创建一个针对于该请求的ServletRequest对象和ServletResponse对象,然后把这两个对象作为参数传给相应Servlet的服务方法。当容器把本次响应结果返回给客户,ServletRequest对象和ServletResponse对象就结束生命周期。

ServletRequest接口中也提供了getAttribute()和setAttribute()方法。因此请求范围内的共享数据可作为ServletRequest对象的属性而存在。Web组件只要共享同一个ServletRequest对象,因此源组件和目标组件能共享请求范围内的共享数据。

本章2.1节的转发过程就利用了请求范围,CheckServlet把消息存放在请求范围内:req.setAttribute("msg",message) ,接着CheckServlet把请求转发给了OutputServlet,而OutputServlet再从请求范围内取出消息:String message = req.getAttribute("msg") 。  对于客户端的每次要求访问CheckServlet的请求,Servlet容器都会创建一个ServletRequest对象,接着CheckServlet创建针对于当前请求的消息,把它作为属性与ServletResponse对象关联,当OutputServlet生成的响应结果被提交给客户时,ServletRequest对象结束生命周期,与ServletRequest对象关联的消息也不复存在。如果客户端再次发出要求访问CheckServlet的请求,服务器端又会开启一次新的轮回。

2.    重定向

HTTP协议规定了一种重定向机制。重定向的运作流程如下:

  1. 用户在浏览器输入特定URL,请求访问服务器端的某个组件。
  2. 服务器端的组件返回一个状态代码为302的响应结果,该响应结果的含义为:让浏览器端再请求访问另一个Web组件。在响应结果中提供了另一个Web组件的URL。另一个组件有可能在同一个Web服务器上,也有可能不在。
  3. 当浏览器收到这种响应结果后,再立即自动请求访问另一个Web组件。
  4. 浏览器端接收到来自另一个Web组件的响应结果。

在Java Script API中,HttpServletResponse接口的sendRedirect(String location)方法用于重定向。Check1Servlet能够把请求重定向到Output1Servlet。

Check1Servlet.java

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.Principal;

public class Check1Servlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        PrintWriter out = resp.getWriter();
        String  username = req.getParameter("username");
        String message = null;
        if (username ==null){
            message = "Please input username.";
        }else {
            message = "Hi,"+username;
        }
        req.setAttribute("msg",message);
        out.println("Output from Check1Servlet before redirecting.");
        System.out.println("重定向之前的Check1Servlet的输出。");
        //请求重定向
        resp.sendRedirect("/output1?msg="+message);   // 形式: /context/output1?msg=message ,由于使用IDEA进行开发,Context默认为空
                                    // ,因此就只需要 /output?msg=.... ,如果以"/"开头,表示相对于当前服务器根路径的URL。
        //resp.sendRedirect("http://localhost:8080/output1?msg="+message);   // 像这样的绝对路径也是完全OK的
        //resp.sendRedirect("www.baidu.com");          //不需要是同一个Web服务器上的组件,别的服务器上的也可以进行重定向

        out.println("Output from Check1Servlet after redirecting.");
        System.out.println("重定向之后的Check1Servlet的输出。");
    }
}
Output1Servlet.java
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

public class Output1Servlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //读取请求范围内的消息
        String messsage = (String )req.getAttribute("msg");
        System.out.println("请求范围内的消息:"+messsage);
        //读取msg请求参数
        messsage = req.getParameter("msg");
        System.out.println("请求参数中的消息:"+messsage);

        PrintWriter out = resp.getWriter();

        out.println(messsage);
        out.close();
    }
}
分别将Check1Servlet和Output1Servlet的URL映射为"/check1""/output1"。运行应用,访问localhost:8080/check1?username=Tomcat,会看到地址成了:http://localhost:8080/output1?msg=Hi,Tomcat 。再回过头来看控制台:

通过这个我们可以看出两点:第一,进行重定向的源组件的重定向语句前后的代码仍然会执行,不会因为重定向而打断,这一点和转发的情况是一样的。第二,我们虽然在Check1Servlet中设置了req.setAttribute()请求范围,但是在Output1Servlet中打印出的请求范围内的消息确实null,这是因为重定向的时候相当于客户端又重新向重定向的URL发送了请求,因此两个Servlet中的req并不是同一个,所以在Output1Servlet的请求范围中并没有我们在Check1Servlet中设定的属性。

注意:sendRedirect()方法是在HttpServletResponse接口中定义的,而在ServletResponse接口中没有该方法,这是因为重定向机制是由HTTP协议规定的。

浏览器端实际上发出了两次请求,第一次请求访问Check1Servlet,第二次请求访问Output1Servlet,最终显示的是Output1Servlet生成的HTML页面(注意,为了避免服务器产生异常,不要在重定向的源组件中提交响应结果)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值