6.5.1 <jsp:include>标签
<jsp:include>标签用于把另外一个Web资源引入当前JSP页面的输出内容之中。该标签的语法格式如下:
<jsp:include page="relativeURL | <%=expression%> | EL" flush="true|false"/>
其中page属性用于指定被引入的Web资源的相对路径,该属性可以用普通字符串指定相对路径,也可以使用JSP表达式或EL(表达式语言)来指定相对路径(EL将在下一章详细讲解)。flush属性表示在引入Web资源时,是否先将out对象缓冲区中的内容刷新到Servlet引擎提供的缓冲区。如果flush属性为true,表示在引入Web资源时,先刷新out对象的缓冲区。flush属性的默认值是false。
在使用<jsp:include>标签时应注意如下几点:
l 引入资源的方式:<jsp:include>标签和6.3.4节讲的include指令在引入资源时有很大的差别。它们之间最大的差别就是include指令是静态引入的,也就是在JSP引擎翻译JSP页面时就将当前JSP页面和被引入的页面合并了,最终生成的Servlet就已经是这两个JSP页面的合体的。而<jsp:include>标签是动态引入Web资源。也就是说,在JSP每次运行时都会引入page属性指定的Web资源。
2 引入资源的路径:如果单从引用文件的目录结构来看,<jsp:include>标签的page属性和include指令的file属性指定的路径是一样的。如将test.jsp文件放在“WEB-INF”目录中,通过<jsp:include>标签的page属性可设为page="/WEB-INF/test.jsp",include指令的file属性也可设为file="/WEB-INF/test.jsp",但page和file不同的是page属性可以设置在web.xml文件中配置的路径,而file属性的值只能是在目录结构中存在的文件。如将“/WEB-INF/test.jsp”映射成“/jsp/test.jsp”,这个新的路径并不存在,只是个虚拟的映射路径。在page属性中该值是有效的,而将file属性设成该值,JSP引擎会提示该路径不存在。
3 引入资源的内容:<jsp:include>标签引入的资源可以是任何内容,而include指令引入的资源必须符合JSP语法规范,即使引入的资源文件的扩展名不是.jsp,该文件的内容也必须符合JSP的语法规范。这是由于include指令在引入任何资源文件时,都会将该文件作为JSP页面进行翻译。如果有一个test.html文件,该文件的内容是<% abcd %>,很明显,该文件的内容不符合JSP语法规范(abcd并未定义,也不是表达式,在翻译成Java代码时会编译出错)。如果这个文件被<jsp:include>标签引用,会直接输出<% abcd %>,但被include指令引用,则会抛出异常。当然,如果将test.html改名为test.jsp,不管是<jsp:include>标签,还是include指令,都会抛出异常。这是由于<jsp:include>标签是根据引入文件的扩展名来决定如何处理该文件的,如果扩展名是.jsp,也会按着JSP页面来处理,所以会抛出异常。
4 <jsp:include>标签和RequestDispatcher.include方法类似,在被引入的页面中修改响应状态码和响应消息头的语句将被忽略。
5 <jsp:include>标签无论在任何情况下,都会使用PrintWriter对象来输出信息。这一点和include方法有很大的差别。对于include方法来说,系统会根据include方法前面的代码是否使用了PrintWriter或ServletOutputStream对象来决定使用哪一个对象来输出信息。而<jsp:include>标签通过某些机制使得ServletOutputStream永远不可用,因此,该标签只能使用PrintWriter对象来输出信息。从这一点可以看出,只要在JSP页面中不使用ServletOutputStream对象来输出信息,<jsp:include>标签是绝对不会由于同时调用了PrintWriter和ServletOutputStream对象来抛出异常的。所以也可以有一个推论,就是在使用<jsp:include>标签的JSP页面中使用ServletOutputStream对象,不管任何情况,都会抛出异常。关于<jsp:include>标签为什么会有这样的特性,将在本节的后面部分详细讲解。
6 效率:include指令的效率是最高的,但include指令不如<jsp:include>标签灵活。如include指令的file属性不能使用EL和JSP表达式。
7 <jsp:include>标签在引入资源文件时可以传递请求参数,但由于include指令是静态引用资源文件的,因此,include指令在引用资源文件时不能传递请求。
8 <jsp:include>标签的page属性必须是相对路径,如果以“/”开头,表示相对于当前Web应用程序的根目录(不是站点根目录),否则,相对于当前页面。
例子 : <jsp:include>标签演示
1. 编写dynamicincluding.jsp页面
该页面使用<jsp:include>指令引入一个included.jsp页面,dynamicincluding.jsp页面的代码如下:
<%@ page language="java" pageEncoding="UTF-8" %>
使用out对象输出信息<br>
<jsp:include page="included.jsp" flush="false"/>
<br>
<%
response.getWriter().println("使用PrintWriter输出信息<br>");
%>
在上面的代码中,<jsp:include>指令前面有一行静态的内容,这部分内容将通过out对象输出到客户端,在<jsp:include>指令后面通过PrintWriter对象输出了一条信息。如果<jsp:include>标签的flush指令为false,则在引入included.jsp页面时不刷新out对象的缓冲区,因此,使用PrintWriter对象输出的信息将会在最前面显示。
2. 编写included.jsp页面
该页面只是一个普通的JSP页面,代码如下:
<%@ page language="java" import = "java.util.*" pageEncoding="UTF-8"%>
included.jsp中的内容<br>
3. 测试<jsp:include>标签引入资源的效果
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/dynamicincluding.jsp
浏览器显示的信息如图6.12所示。
图6.12 使用<jsp:include>标签引入资源文件
从图6.12所示的信息可以看出,使用PrintWriter对象输出的信息显示在了页面的开始部分。如果在dynamicincluding.jsp页面的任何位置调用了response.getOutputStream方法,则一定会抛出异常。读者可以自己做这个实验。
4. 在引入资源文件时刷新out对象的缓冲区
将dynamicincluding.jsp页面中<jsp:include>标签的flush属性设为true,代码如下:
<%@ page language="java" pageEncoding="UTF-8" %>
使用out对象输出信息<br>
<jsp:include page="included.jsp" flush="true"/>
<br>
<%
response.getWriter().println("使用PrintWriter输出信息<br>");
%>
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/dynamicincluding.jsp
浏览器显示的信息如图6.13所示。
图6.13 引入资源文件时刷新out对象的缓冲区
从图6.13所示的输出信息可以看出,由于在引入included.jsp页面时已经将out对象的缓冲区刷新,所以在此之前被写入out缓冲区的内容将会首先输出的客户端,因此,<jsp:include>标签前面的静态内容会显示在最前面。
5. 引用web.xml文件中配置的资源文件
如果将dynamicincluding.jsp和included.jsp页面在web.xml中重新配置一下它们的访问路径,使用<jsp:include>标签仍然可以使用这些新的路径来引用included.jsp页面。配置代码如下:
<!-- 配置dynamicincluding.jsp -->
<servlet>
<servlet-name>dynamicincluding</servlet-name>
<jsp-file>/chapter6/dynamicincluding.jsp</jsp-file>
</servlet>
<servlet-mapping>
<servlet-name>dynamicincluding</servlet-name>
<url-pattern>/abcd/including.jsp</url-pattern>
</servlet-mapping>
<!-- 配置included.jsp -->
<servlet>
<servlet-name>included</servlet-name>
<jsp-file>/chapter6/included.jsp</jsp-file>
</servlet>
<servlet-mapping>
<servlet-name>included</servlet-name>
<url-pattern>/myjsp/included.jsp</url-pattern>
</servlet-mapping>
如果按着上面的配置代码引用included.jsp,则dynamicincluding.jsp页面的代码如下:
<%@ page language="java" pageEncoding="UTF-8" %>
使用out对象输出信息<br>
<jsp:include page="/myjsp/included.jsp" flush="true"/> <br> <% response.getWriter().println("使用PrintWriter输出信息<br>");
%>
在浏览器地址栏输入如下的URL:
http://localhost:8080/demo/abcd/including.jsp
浏览器输出的信息和图6.13所示的输出内容完全相同。
6. 为什么<jsp:include>标签一定会使用PrintWriter对象输出信息
如果读者查询由dynamicincluding.jsp页面生成的Servlet源代码,就会发现<jsp:include>标签被翻译成了下面的Java代码:
org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response,
"/test.html", out, true);
其中include方法的最后一个参数就是flush属性的值。继续查看JspRuntimeLibrary.include方法的源代码(该源代码可以在Tomcat的源代码中找到)。include方法的相关代码如下:
public static void include(ServletRequest request,
ServletResponse response,
String relativePath,
JspWriter out,
boolean flush)
throws IOException, ServletException {
... ...
RequestDispatcher rd = request.getRequestDispatcher(resourcePath);
rd.include(request,
new ServletResponseWrapperInclude(response, out));
}
从上面的代码可以看出,实际上,<jsp:include>标签最终调用的是RequestDispatcher接口的include方法。从这一点还看不出<jsp:include>标签使用的一定是PrintWriter对象。然而,“玄机”就在ServletResponseWrapperInclude类中,这个类实现了HttpServletResponse接口,因此,该类可以转换成HttpServletResponse对象。
在Tomcat源代码中找到ServletResponseWrapperInclude.java,该类的相关代码如下:
package org.apache.jasper.runtime;
... ...
public class ServletResponseWrapperInclude extends HttpServletResponseWrapper
{
private PrintWriter printWriter;
private JspWriter jspWriter;
public ServletResponseWrapperInclude(ServletResponse response,JspWriter jspWriter)
{
super((HttpServletResponse)response);
this.printWriter = new PrintWriter(jspWriter);
this.jspWriter = jspWriter;
}
public PrintWriter getWriter() throws IOException
{
return printWriter;
}
// 抛出异常,使getOutputStream方法永远不可用
public ServletOutputStream getOutputStream() throws IOException
{
throw new IllegalStateException();
}
... ...
}
从上面的代码中可以看出,在getOutputStream方法中抛出一个异常,这说明getOutputStream方法是永远不可用的。但光在getOutputStream方法中抛出异常并不足以说明<jsp:include>标签一定使用了PrintWriter对象输出信息(还有一种可能,就是最后会抛出一个异常)。
决定<jsp:include>标签使用哪个对象输出信息的最后一道“关卡”就是处理默认请求的DefaultSevlet类,该类也可以在Tomcat源代码中找到(DefaultServlet.java)。该类是通过try...catch语句来选择使用哪个对象输出信息的。下面是DefaultServlet类选择PrintWriter或ServletOutputStream对象的主要逻辑:
ServletOutputStream ostream = null;
PrintWriter writer = null;
try
{
ostream = response.getOutputStream();
}
catch(Exception(IllegalStateException e)
{
writer = response.getWriter();
}
从上面的代码可以看出,首先在try{...}块中尝试获得ServletOutputStream对象,在这时response对象实际上是ServletResponseWrapperInclude对象实例,而ServletResponseWrapperInclude类中的getOutputStream方法只有一条抛出异常的语句,而且抛出的异常正好是IllegalStateException,刚好被catch{...}捕获,因此,使用ServletResponseWrapperInclude对象实例作为include方法的第二个参数时,一定使用的是PrintWriter对象输出的信息(因为getOutputStream方法总是抛出IllegalStateException异常)。所以我建议在JSP中引用资源文件时,应尽量使用<jsp:include>标签。
下面的JSP代码将抛出一下异常:
<%@ page language="java" pageEncoding="UTF-8" %>
abcdefg
<%
request.getRequestDispatcher("/test.html").include(request, response);
%>
由于上面代码中的include方法使用了ServletOutputStream对象,因此,在访问上面的JSP页面时将抛出一个异常。根据上面的描述,可以采用ServletResponseWrapperInclude对象来包装response,如使用下面的代码将不会抛出异常:
<%@ page language="java" pageEncoding="UTF-8" %>
abcdefg
<%
// 下面的语句一定使用PrintWriter对象来输出信息
request.getRequestDispatcher("/test.html"). include(request, new
org.apache.jasper.runtime.ServletResponseWrapperInclude(response, out));
%>
要注意的是,在使用ServletResponseWrapperInclude类时,需要在demo工程中引用jasper.jar文件,该文件可以在<Tomcat安装目录>\lib目录中找到。