RequestDispatcher.forward方式应用细节
请求重定向(sendRedirect)与请求转发(forward)的区别
HTTP响应消息分为三们部分:状态行、响应消息头、消息实体。
HttpServletResponse定义了若干与HTTP响应状态码对应的常量,以SC(Status Code)开头。
响应状态行:HTTP版本 + 状态代码 + 提示信息
HTTP/1.1 200 OK
用于设置HTTP响应消息的状态码,并生成响应状态行。正常情况下的响应码为200,Web服务器默认自动
产生这种正常情况下的响应状态行,所以,通常情况下的Servlet不需要调用该方法来指定状态代码和产生响应状态行。只有在HTTP响应消息中使用特殊的状态码时,才需要调用这个方法
sendError(int sc)
sendError(int sc, java.lang.String msg)
用于发送表示错误信息的状态码(如404,找不到资源)。msg用于提示说有出错的原因,该文本信息将出现在发送给客户端的正文本内容中,注,该消息不是用来替换HTTP响应状态行中提示信息的,它只是出现在实体部分。
response.sendError(405,"xxxxx"); 返回的响应状态行如下:
HTTP/1.1 405 Method Not Allowed
http://www.ietf.org/rfc/rfc2047.txt
addHeader(java.lang.String name, java.lang.String value)
setHeader(java.lang.String name, java.lang.String value)
如果已经设置过同名的响应并没有,setHeader方法将用新的设置值取代原来的设置值,而addHeader方法则是增加一个同名的响应头。HTTP响应消息中允许同一名称的头字段出现多次。
addIntHeader(java.lang.String name, int value)
setIntHeader(java.lang.String name, int value)
与addHeader与setHeader不同的是它们传进的值可以直接是一个整数值。一般很少用这两个方法,使用前面两个完全一样。
response.setHeader("Refresh", "2"); 响应头加上了Refresh头如下:
Refresh: 2
注,头的值一般不会使用双引号引起来的。
addDateHeader(java.lang.String name, long date)
setDateHeader(java.lang.String name, long date)
这两个方法专门用于设置包含日期值的响应头的方法。HTTP响应头中的日期通常都是GMT格式。它们避免了将毫秒数转换成GMT格式时间的麻烦。
setContentLength(int len)
setContentLength方法用于设置响应消息的实体内容的大小,单位为字节。对于HTTP协议来说,这个方法就是设置Content-Length响应头字段的值。因为浏览器与Web服务器之间使用持久(Keep-alive的HTTP连接,如果Web服务器没有采用chunked传输编码方式,那么它必须在每个应答中发送一个Content-Length的响应头来表示各个实体内容的长度,以便客户端能够分辨出上一个响应内容的结束位置。一般来说,Setvlet程序不必调用setContentLength方法来设置Content-Length头字段,因为Servlet引擎在向客户端实际输出响应内容时,它可以自动设置Content-Length头字段或采用chunked传输编码方式。
设置响应实体的MIME类型,即对Content-Type头的设置。如响应的实体为jpeg图片,则需要将响应内容的类型设置为“image/jpeg”;如果是xml,则要设置为“text/xml”。
页网的类型通常是“text/html”,所以如果以是Servlet程序的方式输出网页内容,则需要显试地调用 setContentType 方法来指定,否则响应头中不存在Content-Type并没有,那么浏览器会将内容以“text/plain”内型来解释,即以原文本的形式展示。当然如果请求的资源不是Servlet,比如是xx.html,则Web容器会根据你请求的资源的文件的扩展外来在conf/web.xml找相应的已配置的MIME类型,然后自动加上这个头信息,并设置成相应的MIME类型。
在MIME类型后面还可以指定响应内容所使用的字符集类型,如,“text/html; charset=GB2312”,如果没有指定,在Tomcat5.x产生的响应头的MIME类型后默认为ISO8859-1的字符集编码,而Tocmat4.x将不会自动加上。
注,一般charset全小写,但有的厂商是 charSet
setCharacterEncoding是在2.4规范中新增的方法。用于设置输出内容的MIME声明中的字符集编码,对于HTTP来说,即设置Content-Type头字段中的字符集编码部分。
注,如果没有设置Content-Type头字段,该方法的设置的编码不会出现在响应头中(因为实体内容的编码方式只能存放在在Content-Type的后部分),但是它的设置结果仍然决定了输出内容的编码方式(即直接使用out内置对象输出字符串时会采用该方法设置的编码方式,当然如果使用的是response.getOutputStream时,就谈不上编码方式了,因为OutputStream输出不涉及到流)。另外,该方法的比setContentType和setLocale方法的优先权高,它的设置会覆盖这两个方法所设置的字符集编码。
setLocale(java.util.Locale loc)
用于设置响应消息的本地化信息,对HTTP来说,它将设置Content-Language响应头和Content-Type头中的字符集编码部分。如果没有设置Content-Type头,该方法设置的字符集不会出现在响应头中(因为实体内容的编码方式只能存放在在Content-Type的后部分),但是它的设置仍然决定了输出内容的编码方式(即直接使用out内置对象输出字符串时会采用该方法设置的编码方式,当然如果使用的是response.getOutputStream时,就谈不上编码方式了,因为OutputStream输出不涉及到流)。
注,如果调用了setCharacterEncoding或setContentType方法已指定了响应内容的字符集,则该方法将不再起效。
由于Local对象中只包含了语言和国家的地区信息,并没有包含字符集编码的信息,所以需要在web.xml中来配置:
<locale-encoding-mapping-list>
<locale-encoding-mapping>
<locale>zh_CN</locale>
<encoding>GB2312</encoding>
</locale-encoding-mapping>
</locale-encoding-mapping-list>
boolean containsHeader(java.lang.String name)
containsHeader用于检查某个名称的头是否已经被设置
// response.setHeader("Refresh", "2");
response.setHeader("Refresh", "2;URL=ServletContextTest");
// response.setHeader("Refresh","2;URL=http://localhost:8080/myapp/ServletContextTest");
有三个HTTP响应头可以禁止浏览器缓存当前页面:
response.setDateHeader("Expires", 0);
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
不是所有的浏览器都能完全支持上面的三个响应头,因此最好是同时使用上面的三个响应头,只要浏览器能支持其中任何一种形式,就能禁止缓存页面。
上面我们是通过response内置对象来设置响应头的,这只能在Servlet与jsp里使用,如果编写的是静态的html时,我们如果要设置响应消息头,则只能借助于html标签<meta>的 http-equiv 属性来实现。
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
在静态的HTML里我们一般显示地设置页面的编码方式:
<meta http-equiv="Content-Type" content="text/html; charset=GB2312">
注,如果在Servlet或Jsp页面里使用了<meta>来设置编码方式,而又使用也服务器端的设置方式,则以服务器方式的为准,也就是以响应头里的Content-Type 信息为准。
ServletOutputStream getOutputStream() throws java.io.IOException
java.io.PrintWriter getWriter() throws java.io.IOException
ServletOutputStream 为OutputStream的子类。
getOutputStream返回的是字节流,不涉及到编码
getWriter返回的是字符流,涉及编码方式
这两个方法互相排斥,调用了其中任何一个方法后,就不能再调用另一方法。在Servlet里写以下两行:
21 OutputStream o =response.getOutputStream();
22 Writer w = response.getWriter();
运行时抛出异常:
java.lang.IllegalStateException: getOutputStream() has already been called for this response
org.apache.catalina.connector.Response.getWriter(Response.java:607)
org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:196)
HttpServletResponseTest.getOutputStreanWriter(HttpServletResponseTest.java:22)
...
转换上面两行代码:
21 Writer w = response.getWriter();
22 OutputStream o =response.getOutputStream();
运行时抛出异常:
java.lang.IllegalStateException: getWriter() has already been called for this response
org.apache.catalina.connector.Response.getOutputStream(Response.java:576)
org.apache.catalina.connector.ResponseFacade.getOutputStream(ResponseFacade.java:181)
HttpServletResponseTest.getOutputStreanWriter(HttpServletResponseTest.java:22)
...
注,如果使用Servlet输出响应时,一般要将设置头信息的一些代码行放置在向浏览器输出实体内容动作之前。
Servlet的service方法结束后,Servlet引擎将检查getWriter或getOutputStream方法返回的输出流对象是否已经调用close方法,如果没有,Servlet引擎将调用close方法关闭该输出流对象。所以,即使在Servlet的service方法中没有调用输出流对象的close方法,也不会发生输出流未关闭的问题,但是最好还是自己关闭以便尽快释放相关资源。
使用getOutputStream还是getWriter?
a) 使用ServletOutputStream对象也能输出内容全为文本字符的网页文档,但是,如果网页文档内容是在Servlet程序内部使用文本字符串动态拼凑和创建出来的,则需要先将字符文本转换成字节数组后输出,这时就不如使用grtwrirer方法返回的PrintWriter对象直接对字符文本进行输出方便。
b) 如果一个网页文档内容全部为字符文本,但是这些内容可以直接从一个字节输入流中读取出来,然后再原封不动地物出到客户端,那么就应该便用ServletOutputStream对象直接进行输出,而不要使用PrintWriter对象进行输出。因为采用PrintWriter对象进行输出时,还需要将这些内容以字符文本的形式读取到程序中,这样在读取 时涉及了宇节到字符的转换过程,在输出时又涉及了字符到字节的转换过程,显然存在着效率低下的问题,并且由于多了一些额外的过程后,更容易出现字符编码转换的错误。
另一个response.getWriter().print()与println()的区别,其实这两个方法都是I/O流中PrintWriter类的两个方法,所以换不换行对象网页是没有引影响的,但它们的区别又是仅在于换行啊,是的,只不过它的换行仅表示在生成HTML的源码里体现。
Servlet程序输出的HTTP消息的响应正文不是直接发送到客户端的,而是首先被写入到了Servlet引擎提供的一个缓冲区中,直到输出缓冲区被填满或者Servlet程序已经写入了所有响应内容,缓冲区中的内容才会被Servlet引擎发送到客户端。使用输出缓冲区后,Servlet引擎就可以将响应状态行、各响应头和响应正文严格按照HTTP消息的位置进行调整后再输出到客户端,特别是可以自动对Content-Length头字段进行设置和调整。
如果在将响应输出到客户端前,输出缓冲区中已经装入了所有的响应内容,Servlet引擎将计算响应正文部分的大小并自动设置Content-Length头字段;如果在将响应输出到客户端前,输出缓冲区已满但只是全部响应内容的一部分,那么Servlet引擎将无法再计算Content-Length头的值,它将使用HTTP/1.1的chunked编码方式(通过设置Transfer-Encoding头字段来指定)传输响应内容,这样就不用设置Content-Length头了。
注意,缓冲区不包括头字段所占用的空间大小,它纯指响应实体内容所占用大小。
什么时候将缓冲区里的数据发送给client端呢?
(1)当对来自client的request处理完,并把所有数据输出到缓冲区。
(2)当缓冲区满。
(3)在程序中调用缓冲区的输出方法out.flush()或response.flushbuffer(),web container才将缓冲区中的数据发送给client。
ServletResponse定义了以下一个与缓冲区相关的方法:
a) setBufferSize(int size):用于设置输出缓冲区大小,Servlet引擎实现使用的缓冲区的大小不一定等于该设置值,但不会小于该设置值。一定要在设置头与输出正文前调用。
b) getBufferSize:返回Servlet引擎实际使用的缓冲区大小。
c) flushBuffer:将输出缓冲区的内容强制输出到客户端,如果这是当前响应的第一次向客户端实际输出数据,响应状态行和各个响应头也会被输出到客户端。
d) reset:清空输出缓冲区中的内容,以及设置的响应状态码和各个响应头。如果当前响应已经向客户输出过部分内容,这个方法将抛出IllegalStateException异常。
e) isCommitted:判断是否已经提交了部分响应内容到客户端,如果已经提交了,则返回true,否则,返回false。
示例:
private void bufferTest(HttpServletResponse response) throws IOException {
//8192 = 8k
System.out.println("defualt buff size = " + response.getBufferSize());
response.setBufferSize(0);
int len = response.getBufferSize();
System.out.println("after set buff size = " + len);//8192
PrintWriter out = response.getWriter();
//让输出内容正好填满整个缓冲区。注,不包括头字段所在的空间
for (int i = 0; i < len; i++) {
out.print("w");
}
}
linux-7qez /home/fpcsmp> telnet 192.168.1.94 8080
Trying 192.168.1.94...
Connected to 192.168.1.94.
Escape character is '^]'.
GET /myapp/HttpServletResponseTest HTTP/1.1
Host:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
Date: Tue, 13 Jul 2010 08:38:51 GMT
2000 ----十六进制数,等于8192,即8K大小
wwww ... www ----一共有8192个“w”,这里已省略
0
----end
从上面的输出结果可以看到,Servlet引擎没有设置Content-Length头,它采用的是chunked编码方式来传输响应内容,其中的2000是一个十六进制的数据,对应十进制数为 8192 ,恰好等程序中输出的缓冲区大小的值。因为这里写入的内容正好充满了缓冲区,Servlet引擎贲要将缓冲区中的内容发送给客户端,而它此时无法断定是否还会继续写入内容,所以无法断定整个响应内容的大小,所以它只能采用chunked编码方式来传输缓冲区的内容。如果将上面的循环次数减一,则又会出现Content-Length头,响应如下:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Length: 8191
Date: Tue, 13 Jul 2010 08:58:19 GMT
www ... www ----end
如果将原程序的循环加一次,则响应如下:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
Date: Tue, 13 Jul 2010 09:00:54 GMT
2000
www ... www ----一共有8192个“w”
1 ----循环次数多一次,则多出一个“w”
w
0 ----chunked方式传输时,都会以零结束,后面还有一个空行
----end
静态的文件,如 xx.zip 的下载很容易,只需要将一个链接指向Web服务上的这个文件即可,但要注意的是不能将这个要下载的文件放在WEB-INF下。
但是如果要下载的文件并不真正存在于Web服务器的文件系统中时,该如何做?
1) 通过HttpServletResponse.setContentType方法设置Content-Type头的值为浏览器无法使用某种方式或不能激活某个程序来处理MIME类型,例如,“application/octet-stream”或“application/x-msdownload”等。
2) 需要通过HttpServletResponse.setHeader方法设置Content-Disposition头的值为“attachment;filename=文件名”。
3) 通过。HttpServletResponse.getOutputStream方法返回的ServletOutputStream来向客户端输出附件内容。
示例:
private void downloadTest(HttpServletResponse response) throws IOException {
response.setContentType("application/octet-stream");
response.addHeader("Content-Disposition", "attachemtn;filename=xx.txt");
OutputStream os = response.getOutputStream();
os.write("test".getBytes());
os.close();
}
响应如下:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Disposition: attachment;filename=xx.txt
Content-Type: application/octet-stream
Content-Length: 4
Date: Tue, 13 Jul 2010 09:28:50 GMT
test----end
<img>标签的src属性指向的URL不一定某个真正存在于服务文件系统中的图片文件,也可以是一个Servlet程序。
示例:
private void imgDownload(HttpServletResponse response) throws IOException {
// 设置MIME类型为 image/jpeg,浏览器才将收到的数据当做图像来解析
response.setContentType("image/jpeg");
// 不要缓存该图片
response.setDateHeader("Expires", 0);
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
ServletOutputStream sos = response.getOutputStream();
/*
* 在内存中创建一图像,并使用Graphics来绘制它,图像默认
* 背景色为黑色,但可以通过Graphics来修改
* BufferedImage(int width,int height,int imageType)
*
* 在内存中创建一个RGB类型的图片
*/
BufferedImage image = new BufferedImage(300, 200,
BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();//画笔
g.setColor(Color.WHITE);
g.fillRect(10, 10, 280, 180);//填充矩形区域 width=76,height=16
/*
* Font(String name, int style, int size)
* name - 字体名称,如果 name 为 null,则将新 Font 的 逻辑字体名
* 称设置为 "Default"
*/
g.setFont(new Font(null, Font.ITALIC | Font.BOLD, 150));
g.setColor(getColor());
/*
* drawString(String str,int x,int y)
* 字符的基线位于此图形上下文坐标系统的 (x, y) 位置处
*/
g.drawString(String.valueOf((char) (Math.random() * 26 + 65)),
100, 150);
drawLine(g);//随机画十条线
g.dispose();//释放此图形的上下文并释放它所使用的所有系统资源
/*
* write(RenderedImage im,String formatName,OutputStream output)
* formatName - 包含格式的非正式名称的 String
*/
ImageIO.write(image, "JPEG", sos);
sos.close();
}
private void drawLine(Graphics g) {
int x1, x2, y1, y2;
for (int i = 0; i < 10; i++) {
x1 = getRandomNum(270) + 10;
y1 = getRandomNum(170) + 10;
x2 = getRandomNum(270) + 10;
y2 = getRandomNum(170) + 10;
g.setColor(getColor());
g.drawLine(x1, y1, x2, y2);
}
}
private Color getColor() {
return new Color(getRandomNum(255), getRandomNum(255),
getRandomNum(255));
}
private int getRandomNum(int rang) {
return (int) (Math.random() * rang);
}
RequestDispatcher.forward:请求转发
HttpServletResponse.sendRedirect:请求重定向
forward(ServletRequest request, ServletResponse response)
include(ServletRequest request, ServletResponse response)
ServletContext接口中定义了两个用于获取RequestDispatcher对象的方法:
1) getRequestDispatcher(java.lang.String path):返回包装了某个路径所指定的资源的RequestDispatcher对象,传递给该方法的路径字符串必须以“/”开头(因为ServletContext与某个具体请求没有关系,所以是相对的是某个Web应用目录,所以只能以“/”开头,否则与某个请求路径相关了),这个“/”代表的是当前Web应用程序的根目录(“/”),即这个路径是相对于当前应用的应用目录的。WEB-INF目录中的内容对RequestDispatcher对象是可见的,所以,传递给getRequestDispatcher方法的资源可以是WEB-INF目录中不能被外界访问的文件。
注,调用ServletContext对象的getContext()方法可以获取另一个Web应用程序的上下文对象,利用该上下文对象调用getRequestDispatcher()方法得到的RequestDispatcher对象,可以将请求转发到另一个Web应用程序中的资源。但要注意的是,要跨Web应用程序访问资源,需要在当前Web应用程序的<context>元素的设置中,指定crossContext属性的值为true。
2) getNamedDispatcher(java.lang.String name):返回包装了某个Servlet或JSP文件的RequestDispatcher对象,传递给该方法的参数是在web.xml文件中为Servlet或Jsp文件指定的名称,如<servlet-name>元素的值。比如现在有如下的Servlet配置,不过它是以Jsp文件:
<servlet>
<servlet-name>jspservlet</servlet-name>
<jsp-file>/test.jsp</jsp-file>
</servlet>
<servlet-mapping>
<servlet-name>jspservlet</servlet-name>
<url-pattern>/jspservlet</url-pattern>
</servlet-mapping>
此时可以通过servletContext.getNamedDispatcher("jspservlet")来得到将要转发的名为jspservlet 的Servlet,并可以跳转到此资源所对应的Jsp页面。
3) 在ServletRequest接口中也定义了一个getRequestDispatcher方法来获取RequestDispatcher对象,它与ServletContext.getRequestDispatcher方法的区别在于:传递给这个方法的参数除了可以采用以“/”开头的路径字符串,还可以采用非“/”开头的相对路径。
通过ServletRequest获取的RequestDispatcher对象只能包装当前Web应用程序中的资源,所以,通过这个对应的forward和include方法只能在同一个Web应用程序内的资源之间转发请求与实现资源包含,因为这个RequestDispatcher对象只能是相对于当前Web应用目录(以“/”开头,如request.getRequestDispatcher("/test.jsp"),则要转发到的资源路径为/myapp/test.jsp)或当前请求路径(如request.getRequestDispatcher("test.jsp"),而是从“/myapp/servlet/ForwardServletTest”请求跳转过来的,则此时要转发到的资源路径为“/myapp/servlet/test.jsp”)。
注,ServletContext.getRequestDispatcher(java.lang.String path)中的path一定要是以“/”开头,且不能包含应用目录,否则运行出错,请看:
l 路径不以“/”开头:RequestDispatcher rd = getServletContext().getRequestDispatcher("IncludedServlet?p1=中国");抛异常java.lang.IllegalArgumentException: Path IncludedServlet?p1=中国 does not start with a "/" character
l 包含了应用目录:RequestDispatcher rd = getServletContext().getRequestDispatcher("/myapp/IncludedServlet?p1=中国");在include的地方会以这样的错误提示信息代替The requested resource (/myapp/myapp/IncludedServlet) is not availableafter including
而request.getRequestDispatcher(java.lang.String path)的中path可以以“/”开头,也可以不以“/”开头,以“/”开头时与ServletContext.getRequestDispatcher一样不能包含应用目录;如果不以“/”开头时,则path相对于当前请求的路径,比如path为“IncludedServlet”,请求的地址为http://localhost:8080/myapp/IncludingServlet,则最终的path等效为“http://localhost:8080/myapp/IncludedServlet”。以下两种都可以:
l RequestDispatcher rd = request.getRequestDispatcher("/IncludedServlet?p1=中国");
l RequestDispatcher rd = request.getRequestDispatcher("IncludedServlet?p1=中国");
RequestDispatcher.include方法用于将RequestDispatcher对象封装的资源内容作为当前响应内容的一部分包含进来。被包含的Servlet程序不能改变响应消息的状态码和响应头,如果它里面存在这样的语句,这些语句的执行结果将被忽略。在调用RequestDispatcher.include方法时,Servlet容器不会去调整HttpServletRequest对象中的信息,HttpServletRequest对象仍然保持其初始的URL路径和参数信息,也就是说,在被调用者程序中检索当前访问路径时,得到的结果是调用者应序的URL路径,而不是被调用者程序自己的URL路径
示例:
public class IncludingServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//response.setContentType("text/html;charset=GB2312");
PrintWriter out = response.getWriter();
RequestDispatcher rd = getServletContext().getRequestDispatcher(
"/IncludedServlet?p1=中国");
out.println("before including<br>");
rd.include(request, response);
out.println("after including<br>");
}
}
public class IncludedServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=GB2312");
response.setCharacterEncoding("gb2312");
PrintWriter out = response.getWriter();
out.println("中国<br>");
out.println("URI:" + request.getRequestURI() + "<br>");
out.println("URL:" + request.getRequestURL() + "<br>");
out.println("QueryString:" + request.getQueryString() + "<br>");
out.println("param p1:" + request.getParameter("p1") + "<br>");
}
}
地址栏中输入:http://localhost:8080/myapp/IncludingServlet,页面显示
before including
??
URI:/myapp/IncludingServlet
URL:http://localhost:8080/myapp/IncludingServlet
QueryString:null
param p1:??
after including
通过telnet查看响应头
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Length: 123
Date: Tue, 13 Jul 2010 12:57:12 GMT
上面中文显示的是乱码,并且响应头中没有 Content-Type(但为什么页面解析了<br>标签?如果在包含的页面里设置response.setContentType("application/octet-stream");,则浏览器又不会解析<br>标签,这说明浏览器在没有Content-Type头或设置了错误的MIME类型时,默认认为是 text/html 类型),说明IncludedServlet中的如下两名没有起效:
response.setContentType("text/html;charset=GB2312");
response.setCharacterEncoding("gb2312");
这是因为被包含的Servlet程序不能改变响应消息的状态码和响应头,如果它里面存在这样的语句,这些语句的执行结构将被忽略。在IncludedServlet中request.getQueryString()显示为null,因为浏览器在访问IncludedServlet时没有传递参数,所以在IncludedServlet程序中获得的整个查询参数为null。但是可以从request中获取参数,因为在include时,request与response都传递进IncludedServlet了;现在如果将include改为forward,则结果如下:
??
URI:/myapp/IncludedServlet
URL:http://localhost:8080/myapp/IncludedServlet
QueryString:p1=??
param p1:??
上面是include一个Servlet,如果是一个HTML静态页面结果如何,实事证明include一个静态页面与include一个Servlet效果是一样的。凡是在web.xml文件中找不到匹配的<servlet-mapping>元素的URL访问请求,也就是所有其他Servlet都不处理的访问请求,都将交给Tomcat中设置的一个缺省Servlet处理,客户端对静态HTML文件和图片的访问其实都是由缺省Servlet来完成响应的。所以在一个Servlet中include一个静态HTML文件实际上就是在调用缺省Servlet,所以调用静态HTML文件与调用另外一个Servlet程序在本质上没有什么区别。在这里,因为作为被调用者的缺省Servlet程序不能改变响应消息的状态码和响应头,它里面的response.setContentType语句将不再起作用。
要注意的是,Tomcat的缺省的Servlet中是使用response.getOutputStream输出静态内容,还是使用response.getWriter()输出静态内容,请看org.apache.catalina.servlets.DefaultServlet的doGet方法代码片断:
// 如果抛出了异常,说明前面已经调用getWriter方法,则在异常处理代码中继续使用getWriter
try {
ostream = response.getOutputStream();
} catch (IllegalStateException e) {
// 只有文本内容者可以用PrintWriter对象进行转换输出
if ( (contentType == null)
|| (contentType.startsWith("text"))
|| (contentType.endsWith("xml")) ) {
writer = response.getWriter();
} else {//如果请求的不是文本文件,而又在请求静态资源前调用了response.getWriter(比如从一个Servlet中跳转到一个静态资源时),则会抛出异常。比如,在输出图片时,如果在输出前调用了response.getWriter,则会抛异常,请看后面的实验。
throw e;
}
}
从上面代码可以看出,如果调用者在include之前已使用response.getWriter(),则在DefaultServlet继续使用这个response.getWriter(),否则使用response.getOutputStream(),当然如果在是include后调用者调用response.getWriter()则会抛异常;另外,forward一个静态页面也是这样一个过程。
Servlet程序在调用这个方法进行转发之前可以对请求进行一些前期的预处理。在MVC中,它是一个核心方法,控制器就是使用该方法来跳转到相应的视图组件上的。使用forward方法注意点:
1) 如果在调用forward方法之前,在Servlet程序中写入的部分内容已经被真正地传送到了客户端(比如强制通过response.flushBuffer()将数据写到客户端时),forward方法将抛出IllegalStateException异常。
// Reset any output that has been buffered, but keep headers/cookies
if (response.isCommitted()) {
throw new IllegalStateException(sm
.getString("applicationDispatcher.forward.ise"));
}
try {
response.resetBuffer();//清除缓冲区,但不清除头
} catch (IllegalStateException e) {
throw e;
}
2) 如果在调用forward方法之前向Servlet引擎的缓冲区中写入了内容,只要写入到缓冲区中的内容还没有被真正输出到客户端,forward方法就可以被正常执行,原来写入到输出缓冲区中的内容将被清空(但设置的头信息不会被清空)。在调用forward方法之后,如果调用者程序继续向Servlet引攀的缓冲区中执行写入操作,这些写入操作的执行结果将被忽略。
3) 在调用者程序中设置的响应状态码和响应头不会彼忽略,在被调用者程序中设置的响应状态码和响应头也不会被忽略(但被调用者设置的响应头会覆盖掉调用者设置的同样的头)。注意:对于RequestDispatcher.include方法调用,只有调用者程序设置的响应状态码和响应头才会有效,在被调用者程序中设置的响应状态码和响应头将被忽略。
4) 不管是include还是forward,如果不是以“/”开头,则它们都是相对于被调用者,且“被调用者里(即在服务器上运行的Servlet或jsp里)”的使用的相对路径还是相对于被调用者路径;页面里的超链接则是相对于地址栏中的地址。
================= ForwardTestServlet.java====================
public class ForwardTestServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//response.setContentType("text/html;charset=gb2312");
//禁止浏览器缓存,以免影响实验效果
response.setDateHeader("Expires", 0);
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
//PrintWriter out = response.getWriter();
//out.println("before forward");
//用于将缓冲区中的内容强制刷新到客户端
//response.flushBuffer();
RequestDispatcher rd = getServletContext().getRequestDispatcher(
"/test.html");
rd.forward(request, response);
//out.println("after forward");
System.out.println("after forwardd");//forward调用后会往下执行
}
}
=================test.html====================
/myapp/test.html文件中含有中文,且是GBK编码保存。
1) 使用telnet测试:
linux-7qez /home/fpcsmp> telnet 192.168.1.94 8080
Trying 192.168.1.94...
Connected to 192.168.1.94.
Escape character is '^]'.
GET /myapp/ForwardTestServlet HTTP/1.1
Host:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Cache-Control: no-cache
Accept-Ranges: bytes
ETag: W/"4-1279075506000"
Last-Modified: Wed, 14 Jul 2010 02:45:06 GMT
Content-Type: text/html
Content-Length: 4
Date: Wed, 14 Jul 2010 02:45:18 GMT
中国—end
从上面响应消息看出,即使ForwardTestServlet程序中没有设置Content-Type,当然test.html里更不会设置,但头里已经有 Content-Type,且MIME类型为text/html。当forward方法将请求主转发给test.html页面时,实际上在调用Tomcat的缺省Servlet,上面消息包中的Content-Type头就是由Tomcat的缺省Servlet根据web.xml所配置的MIME(文件扩展名对应MIME类型)来自动添加。
2) 去掉程序中所有注释,除response.flushBuffer();外。从输出的结果可以看出只有test.html文件的内容被输出,在forward之前与之后的内容没有被输出。
3) 去掉程序中PrintWriter out = response.getWriter(); 注释,保留对response.setContentType("text/html;charset=gb2312");的注释,这里在浏览器注会看到乱码(一些问号,这些问号是由于在将字节流以ISO8859-1编码方式转换成字符流所造成的)。由于Tomcat的缺省Servlet首先检查当前HttpServletResponse对象是否已经调用过getWriter方法返回了PrintWriter对象,如果已经调用,则使用getWriter方法返回的PrintWriter对象来输出静态HTML文件中的内容,由于没有为PrintWriter对象指定字符编码,所以该PrintWriter对象将静态HTML文件中的内容按默认的ISO8859-1编码进行转换后输出到客户端。
4) 去掉程序中PrintWriter out = response.getWriter(); 与response.setContentType("text/html;charset=gb2312");的注注释,浏览器也能正常显示中文。道理与上面是一样,只是PrintWriter现在的编码方式已经设置成了GBP312了,此时Tomcat缺省Servlet将使用该PrintWriter对象把静态HTML文件中的内容按GB2312编码格式转换后再输出到客户端。现如果将setContentType中的GB2312修改成UTF-8还能正常显示吗?还是也能正常显示,为什么呢?test.html文件的保存格式不是GB2312吗?怎么设置成UTF-8也不会有问题?这主要的是原因是setContentType中设置的编码方式只会影响到PrintWriter将JVM中的Unicode字符串编码后输出到客户端,但并不意味着JVM将源文件读到到内存时就是采用的setContentType方法中设置的编码方式,实际上缺省Servlet在将源文件读取到内存并转换成Unicode字符时采用的是操作系统中默认的字符编码方式,在中文windows下采用的是GBK方式。
5) 取消掉PrintWriter out = response.getWriter();与response.flushBuffer()的注释,可以看到浏览器中显示了forward语句前面写入的内容,而没有显示出test.html文件中的内容。查看logs/localhost.xxx.log日志文件,我们会发现抛出了如下异常:
java.lang.IllegalStateException: Cannot forward after response has been committed
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:323)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:311)
at ForwardTestServlet.doGet(ForwardTestServlet.java:27)
...
注,在Jsp页面里调用out.flush()方法也会出此问题。
6) 去掉程序中response.setContentType("text/html;charset=GB2312");与PrintWriter out = response.getWriter();的注释,并将test.html以UTF-8的方式存储,这时浏览器显示为乱码。Tomcat的缺省Servlet可以将静态HTML文件中的内容按照某种字符集编码进行转换后输出到客户端,这是因为PrintWriter对象的输出方法接受的参数类型都是Java字符串,这些字符串在内存中都是以Unicode编码的形式存在的,所以PrintWriter将内存中的这些Unicode字符串输出时先要将内存中的Unicode字符串以程序中设置的GB2312方式编码后再输出到客户端。现在出现的问题在于:这些要输出的Java字符串又是从静态HTML文件中读取到的,这个读取过程只有知道静态HTML文件中的内容的字符集编码,才能将它们读取到内存中并转换成正确的Unicode编码字符串,而从HTML文件本身的内容上无法准确知道其所采用的字符集编码,所以Tomcat的缺省Servlet只能去假定和猜侧静态HTML文件的字符集编码。由于人们一般都按操作系统本地字符集编码来编写自己的文件,所以,Tomcat的缺省Servlet假定所有静态HTML文件的字符集编码为操作系统本地字符集编码,对中文系统来说,即GB2312。在通常情况下,进行这种假定都不会出什么问题,但是,一旦静态HTML文件的字符集编码与本地操作系统的不一致时,就会出现问题。这里的test.html文件本身的字符集编码为UTF-8,但Tomcat的缺省Servlet以GB2312字符集编码进行读取,所以就出现了乱码问题。
注,在conf/web.xml文件中的缺省Servlet的注释说明中,我们可以看到一个名为fileEncoding初始化参数用于指定缺省Servlet所计入的静态HTML文件的字符集编码,如果设置了这个参数,Tomcat的缺省Servlet对所有的文件都按照该指定的字符集编码来处理。
下面我们看看Tomcat的缺省Servlet在读取静态内容时所采取方式,是字节流的形式读取静态文件内容,还是以字符流的形式读取静态文件内容?
1) 以response.getOutputStream对象向客户输出静态内容时,DefaultServlet将静态资源输出到客户端的代码片断如下:
...
InputStream resourceInputStream = cacheEntry.resource.streamContent();
InputStream istream = new BufferedInputStream(resourceInputStream, input);
...
// Printing content 将静态资源以二进制形式原样输出到客户端,服务器端不会涉及到编码过程,所以只要客户端的编码方式与静态资源的编码方式相同,则就不会出现乱码
exception = copyRange(istream, ostream, currentRange.start,currentRange.end);
2) 以response.getWriter对象向客户输出静态内容时,DefaultServlet将静态资源输出到客户端的代码片断如下:
...
Reader reader;
if (fileEncoding == null) {
//如果没有配置fileEncoding参数,则以操作系统的编码方式来将字节流转换成字符流
reader = new InputStreamReader(resourceInputStream);
} else {//如果web.xml中给default Servlet配置了fileEncoding
//以指定的编码方式将字节流转换成字符流
reader = new InputStreamReader(resourceInputStream,fileEncoding);
}
...
// Printing content 将静态内容输出到客户端
exception = copyRange(reader, writer, currentRange.start, currentRange.end);
从上面两个代码片断可以看出,不管是以字节还是以字符流形式向客户端输出静态内容时,DefaultServlet都是以字节流的方式将静态内容读取到内存中,此时还未涉及到编码方式,只有在要求以字符流形式向客户端输出时,才将先前读取的字节流包装成字符流,即从字节流向字符流转换,此过程是涉及到编码方式的。所以这就解释了下面第7个实例中,为什么缺省Servlet在没有设置fileEncoding参数时,读取UTF-8编码方式的静态页面,并以字节流输出到客户端时不会出现乱码现象。
7) 注释掉程序中的PrintWriter out = response.getWriter();并设置charset=UTF-8,test.html存储格式为UTF-8。这时浏览器能正常显示出采用UTF-8编码的test.html文件内容。这是因为Tomcat的缺省Servlet检查到还没有调用response.getWriter方法,它将调用response.getOutputStream方法返回的字节流对象将静态的HTML文件中的内容接字节形式原样地输出到客户端,而这里设置浏览器的编码方式又为UTF-8,且与test.html存储编码方式一致,所以不会出现乱码。如果将charset设置成GB2312,test.html也以GB2312编码方式存储,则也会正常显示,总之,这时服务器端根本没有涉及到编码方式的转换过程。
8) 去掉PrintWriter out = response.getWriter();的注释,并修改成跳转到一个图片资源:RequestDispatcher rd = getServletContext().getRequestDispatcher("/favicon.ico");。运行时抛出异常:java.lang.IllegalStateException: getWriter() has already been called for this response。对于非文本格式的资源文件,Tomcat总是调用response.getOutputStream返回的字节流来将它们原样地输出到客户端,但由于response.getWriter已经在ForwardTestServlet中调用过了,所以就抛出异常了。如果将PrintWriter out = response.getWriter();注释掉,则会正常显示图片。注意,这个问题requestDispatcher.inclue也存在。
HttpServletResponse:sendRedirect(String location) throws IOException
sendRedirect方法用于302响应码和Location响应头,从而通知客户端去重新访问Location响应头中指定的URL。location参数可以使用相对的URL,Servlet引擎会自动将相对URL翻译绝对URL后,再生成Location头字段。使用下面两条语句也能完成response.sendRedirect(url)语句所完成的功能:
response.setStatus(response.SC_MOVED_TEMPORARILY)
response.setHeader("Location" ,url)
但是,response.setHeader("Location" ,url)语句中的URL参数必须是一个绝对的URL。另外,sendRedirect方法还会自动创建包含重定向URL的超链接的文本正文,该文本正文显示在不支持自动重定向的旧浏览器中,以便用户可以手工进入该URL所指向的页面。
sendRedirect方法不仅可以重定向到当前应用程序中的其他资源,它还可以重定向到同一个站点(即同一主机,也可以是同一虚拟主机)上的其他应用程序中的资源,甚至是使用绝对URL(以“/”开头或“http://”开头)重定向到其他站点的资源(即不同的主机或不同的虚拟主机),而RequestDispatcher.forward方法只能在同一个Web应用程序内的资源之间转发请求(不对,通过ServletContext获得的RequestDispatcher就可以转发到同一站点或同一虚拟主机上的其他不同的Web应用,比如从同一主机中的“/myapp”应用跳转到“/myapp2”应用。比如现在 /myapp Web应用中有个test.jsp文件,现要通过RequestDispatcher跳转到 /myapp2 Web应用中的othersite.jsp,下面两种方式都可以实现:
<%
RequestDispatcher rd = pageContext.getServletContext().getContext("/myapp2").getRequestDispatcher("/othersite.jsp");
//response.sendRedirect("/myapp2/othersite.jsp");//这种也可以实现
rd.forward(request,response);
%>
)。如果传递给sendRedirect方法的相对URL不是以“/”开头,则表示是相对于当前请求的URL,如果该相对URL是以“/”开头,则是相对于整个Web站点的根目录而不是相对于当前Web应用程序的根目录(也就是说,只要你以 / 开头,则Tomcat服务器不再为你在路径前面加上“/<应用目录>”),例如,下面的语句表示重定向到当前站点中的examples应用程序根目录下的index.html页面:
response.sendRedirect("/examples/index.html");
即使在调用sendRedirect方法之前向Servlet引擎的缓冲区中写入了内容,只要写入到输出缓冲区中的内容还没有被真正传送到客户端,sendRedirect方法就可以被正常执行,并且将输出缓冲区中原来写入的内容清空。
为了在客户端不支持或关闭了Cookie的情况下,当sendRedirect方法将请求重定向到当前Web应用程序中另外的一个资源时仍然能进行会话跟踪,需要调用HttperServletResponse.encodeRedirectURL方法传递给sendRedirect方法的url进行URL重写。
GET /myapp/test.jsp HTTP/1.1
Host:
HTTP/1.1 302 Moved Temporarily
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=EFF594E7C4DB9E182FCC40B6D4A10F07; Path=/myapp
Location: http:///myapp/othersite.jsp
Content-Type: text/html
Content-Length: 0
Date: Thu, 15 Jul 2010 02:02:56 GMT
----end
RequestDispatcher.forward只能在同一Web应用(通过request.getRequestDispatcher获取的RequestDispatcher)或同一主机(或同一虚拟主机)中不同的Web应用(通过ServletContext.getRequestDispatcher获取的RequestDispatcher)之间转发;而HttpServletResponse.sendRedirect方法不仅可以在同一主机中跳转,还能重定向到不同站点(或不同虚拟主机)上的资源。如果传递给sendRedirect的url以“/”开头,它是相对于整个Web站点(主机)的根目录,如果创建RequestDispatcher对象时指定的URL以“/”开头,它是相对于当前Web应用的根目录。
sendRedirect重定向结束后,地址栏会发生改变,而forward不会。
sendRedirect不带上下文跳转(这种方式要传值出去的话,只能在url中带parameter或者放在session中,无法使用request.setAttribute来传递),而forward则会带上下文跳转。
无论是sendRedirect还是forward谅地,在调用它之前,都不能有内容已经被真正输出到了客户端,如果缓冲区中已经有了一些内容,这些内容将被从缓冲区中清除。
forward重定向将原始的HTTP请求对象(request)从一个servlet实例传递到另一个实例,而采用sendRedirect方式两者不是同一个application。
response.sendRedirect是向客户浏览器发送页面重定向指令,它是在客户端执行的。RequestDispatcher.forward则是直接在服务器中进行处理,forward是直接在服务器中进行处理,所以forward的页面只能是本服务器的。
注意:
(1).使用forward、sendRedirect时,前面不能有HTML输出。
这并不是绝对的,不能有HTML输出其实是指不能有HTML被送到了浏览器。事实上现在的server都有cache机制,一般在8K(我是说JSP SERVER),这就意味着,除非你关闭了cache,或者你使用了out.flush()强制刷新,那么在使用sendRedirect之前,有少量的HTML输出也是允许的。
(2).response.sendRedirect之后,应该紧跟一句return;
我们已经知道response.sendRedirect是通过浏览器来做转向的,所以只有在页面处理完成后,才会有实际的动作。既然你已经要做转向了,那么后的输出还有什么意义呢?而且有可能会因为后面的输出导致转向失败。
<jsp:forward page="" />:
它的底层部分是由RequestDispatcher.forward()方法实现的。
如果在<jsp:forward>之前有很多输出,前面的输出已使缓冲区满,将自动输出到客户端,那么该语句将不起作用,这一点应该特别注意。