JSP的学习(6)——九大隐式对象及其out对象
本篇将介绍JSP中的九大隐式对象,并重点介绍其中的out对象。
我们在之前的博客《JSP的学习(1)——基础知识与底层原理》一文中已经知道,JSP最终要被翻译和转换成Servlet,在转换后的Servlet中,由_jspService(…)方法代替我们以前直接使用Servlet中的service()方法,其实两个功能都是一样的,都是作为浏览器来访问,然后由服务器调用的方法。
但JSP在转换成Servlet后,在这个Servlet中的_jspService(…)方法会自动根据我们在JSP中的一些指令,相应的生成一些对象,可供我们在JSP上直接使用这些对象,而不需要再重新使用类来创建,总共在JSP中共有直接可调用的九大隐式对象:
- request
- response
- page
- application
- session
- config
- exception
- out
- pageContext
在《JSP的学习(1)——基础知识与底层原理》一文中也已经提到,这几个对象我们可以从Tomcat的【work】目录下由JSP转换后对应的.java文件可以看到在_jspService(…)方法对其中一些对象的定义:
我们对其中几个对象进行快速说明:
request和response 分别代表了浏览器发来的请求对象和服务器回送浏览器的响应对象,这两个对象在之前学习Servlet时都开专门的几篇博客讲过,这里的也是一样的用法,因此不多再做叙述。
从截图来看:
page对象代表了JSP转换后的该Servlet(因为可以看截图中定义”this”)。
application是ServletContext的对象,因此ServletContext能怎么使用那么application在JSP中也就能怎么使用,具体请看《Servlet的学习之ServletContext(1)》。
session对象由JSP转换后的Servlet在_jspService(…)方法中定义和创建,注意如果在JSP页面中使用page指令将”session”指令置为”false”,那么Servlet将不会对其进行创建。当然如果创建了session,那么功能还是同之前学习Servlet时的Session对象一样,具体请看《Servlet的学习之Session(1)》系列。
config对象也由JSP转换后的Servlet在_jspService(…)方法中定义和创建,也可以按之前学习ServletConfig对象的方法作用于config对象,具体请看《Servlet的学习之ServletContext(1)》 。
以上三个对象(application、session、config)不仅仅声明,而是都会自动创建:
exception对象我们说过,这个对象如果要想能被Servlet自动定义和创建,那么这个页面就必须有“isErrorPage”属性置为”true”的这个page指令(<%@ page isErrorPage="true" %>),通常来说我们对于这个JSP都是用作错误处理页面的用处。下图为设置了“isErrorPage”属性后Servlet自动创建exception对象:
切记,一般平常没设置isErrorPage属性的JSP是不能直接使用exception对象的,而这对象的不容易出现,也造成很多人会忘记JSP还有这个隐式对象。
接下来还有out对象和pageContext对象,这两个对象因为和之前Servlet的学习中有很大的不一样或者以前没有这个定义,所以是我们学习JSP隐式对象中的重点。本篇先来学习JSP中的out对象,而pageContext对象将在下一篇中进行学习。
===============================重点对象分割线==============================
out对象用于JSP向客户端浏览器发送文本数据,我们之前在查看由JSP转换后的.java文件中可以看到,Servlet正是通过out对象,将JSP中所有的模板元素(HTML标签之类)和内容显示都以out对象的方法将其写入response对象中,并响应回浏览器:
out对象是用过pageContext对象(后一篇会介绍到)的getOut()方法返回的,同时记住out对象是字符流对象,其作用和用法与ServletResponse.getWriter()方法返回的PrintWriter对象十分类似:
out对象是JspWriter类的一个实例,至于JspWriter类,其实在幕后还是PrintWriter类,只是JspWriter具有缓存,只有JspWriter中的缓存进行了刷新后才创建PrintWriter类(是JspWriter幕后创建PrintWriter),并将JspWriter对象中的内容写入到PrintWriter类对象中。
out对象(JspWriter的实例)是具有缓存的,这个缓存是由page指令中的“buffer”属性设置,我们在《JSP的学习(3)——语法知识二之page指令》已经学习了,“buffer”属性默认具有8kb字节的缓存,可以设置为“none”关闭JspWriter的缓存,也可以设置“XXXkb”来重新设置JspWriter的缓存。
使用out对象写入了内容,只有满足以下任一条件时,out对象才去调用ServletResponse.getWriter()方法,通过PrintWriter对象将out对象缓冲区中的内容写到Response响应对象中:
① 设置page指令将“buffer”属性为“none”
② out对象的buffer已满
③ 整个JSP页面执行完毕
关于out对象两个示例的结论需要知道:
1,如果在JSP页面上同时使用out对象和response.getWriter()对象进行输出,不管哪个对象在前面调用write()方法,都是response.getWriter()对象会先输出数据。说到这里不妨回顾下上一篇博客《JSP的学习(5)—语法知识三之include指令》中最后一个动态包含的例子,看看访问JSP页面后中的源码里面的内容。
我们这里继续再做一个小例子:创建一个web工程【JSPLearning】, 在index.jsp中的<body>模板元素中将内容修改如下:
1 <body> 2 <% 3 out.write("这是JspWriter对象输出的内容"); 4 response.getWriter().write("这是PrintWriter对象输出的内容"); 5 %> 6 </body>
那么在访问JSP页面时会发生什么情况呢?我们开启服务器,部署该web工程,对index.jsp页面进行访问:
从访问结果来看,并不是我们所期望的显示顺序,我们再来看看源码:
从源码可以看到,结果正如我们开始结论所说的,使用ServletResponse.getWriter()方法获得的PrintWriter对象输出都会比out对象获得的JspWriter对象输出要早,无论两者顺序如何。
这是由于通过PrintWriter对象输出的数据直接到Response响应对象的缓冲区中,而JspWriter通过out对象输出由于不满足某些条件(例如未使buffer满或者未到JSP页面结束),都无法幕后创建一个PrintWriter对象再将out对象缓冲区中的数据送到Response响应对象的缓冲区中。只有当JSP页面执行结束或者使out对象的buffer满时,才将数据再通过调用response.getWriter()获得的PrintWriter对象将数据送到Response响应对象的缓冲区中,但此时,Response响应对象中早已经有了通过直接调用Response.getWriter().write()方法所写入的内容。
请看如下动图,展示了这个例子的流程:
因此我们在JSP的编写代码中,应该尽量采用out对象这个隐式对象来输出数据。
2,如果我们要使用JSP来作为文件下载的方式,那么在JSP页面中我们不能使用out对象,因为out对象是字符流,而是应该使用response.getOutputStream()方法来获得字符流对象,并且在JSP页面中不能出现任何由out对象输出的数据(例如一些HTML标签和空格之类,这些可以在转换后的Servlet中查看是否还有)。
我们来做一个使用JSP下载的示例,在MyEclipse中创建一个web工程【JSPLearning】,并且在【WebRoot】目录下新建一个【download】目录,添加进一张“1.jpg”图片:
然后直接在“index.jsp”中操作好了,我们在<body>标签中的代码如下:
1 <body> 2 <% 3 String filePath = application.getRealPath("/download/1.jpg"); 4 String fileName = filePath.substring(filePath.lastIndexOf("\\")+1); 5 6 response.setHeader("content-disposition", "attachment;filename="+fileName); 7 8 FileInputStream fis = new FileInputStream(filePath); 9 byte[] buffer = new byte[1024]; 10 int len = 0; 11 while((len = fis.read(buffer))>0) { 12 response.getOutputStream().write(buffer, 0, len); 13 } 14 %> 15 </body>
如下图所示:
这样就能下载了吗,当然不行,因为这些里面的<html>、<head>、<title>等等都是由out这个字符流对象输出的,如果这么操作,那么服务器肯定会有getOutputStream() has already been called for this response这个异常(《Servlet的学习之Response响应对象(3)》)。
那么既然我们不能在JSP页面中同时用到字符流和字节流的话,那么应该怎么做呢?
因为图片不是字符,所以只能用字节流输出,那么我们只能将JSP中所有会被out对象用write()方法写出的数据全部删除,比如<html>、<head>、<title>等等这些,因此我们的JSP页面应该是成为这样:
貌似就已经把该JSP页面中会通过out对象输出的内容都清理完,这样就完了吗?嗯……还差一点,我们先来访问下这个JSP,虽然可以下载文件,但发现还是会有getOutputStream() has already been called for this response这个异常:
我们不妨来看看这个“index.jsp”对应的Servlet文件:
是的,还有换行,这个比较难发现,所以一般在JSP中使用字节流一定要看看对应的Servlet中到底还有没有通过out对象输出的数据,尤其是这些换行和空格之类的,我们再来看看修改后的样子:
不留任何换行,这一次重新来访问该JSP,浏览器弹出下载窗口,服务器也没有抛出异常,因此这样就解决了getOutputStream() has already been called for this response这个异常。
最后我们再来看看该“index.jsp”对应的Servlet文件源码,我们可以看到这时候已经没有任何通过out对象输出的数据,包括换行。但是out对象是依然有的,通过pageContext对象的getOut方法建立。也就是说,在JSP中,虽然该Servlet通过getOut方法获得了JspWriter这个字符流对象,但是只要没有使用,依然可以由JSP通过响应对象再获得字节流:
于是,这篇介绍JSP中几个隐式对象和重点学习了下out对象,那么本篇博客就该结束了,等等,最后那个下载的图片到底是什么啊??哈哈,当然是我们可爱的银魂一家子: