1. 转发与包含
Servlet对象由容器创建,并且Servlet对象的service()方法也由容器调用。一个Servlet对象可否直接调用另一个Servlet对象的service()方法呢?答案是否定的,因为一个Servlet对象无法获得另一个Servlet对象的引用。
- 请求转发:Servlet(源组件)先对客户请求做一些预处理操作,然后把请求转发给其他Web组件(目标组件)来完成包括生成相应结果在内的后续操作。
- 包含: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协议规定了一种重定向机制。重定向的运作流程如下:
- 用户在浏览器输入特定URL,请求访问服务器端的某个组件。
- 服务器端的组件返回一个状态代码为302的响应结果,该响应结果的含义为:让浏览器端再请求访问另一个Web组件。在响应结果中提供了另一个Web组件的URL。另一个组件有可能在同一个Web服务器上,也有可能不在。
- 当浏览器收到这种响应结果后,再立即自动请求访问另一个Web组件。
- 浏览器端接收到来自另一个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页面(注意,为了避免服务器产生异常,不要在重定向的源组件中提交响应结果)。