本篇將介紹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對象,那么本篇博客就該結束了,等等,最后那個下載的圖片到底是什么啊??哈哈,當然是我們可愛的銀魂一家子: