Java Web基礎 --- Jsp 綜述(下)
摘要:
JSP指令碼中包含九個內建物件,它們都是Servlet-API介面的例項,並且JSP規範對它們進行了預設初始化。本文首先通過一個JSP例項來認識JSP內建物件的實質,緊接著以基於請求/響應架構應用的執行機制為背景,引出JSP/Servlet的通訊方式與內建物件的作用域,並對每個內建物件的常見用法進行深入介紹和總結。
一. JSP 九大內建物件概述及相關概念說明
JSP指令碼中包含九個內建物件,這九個內建物件都是 Servlet API 介面的例項,並且JSP規範對它們進行了預設初始化(由 JSP 頁面對應 Servlet 的 _jspService() 方法來建立並初始化這些例項)。
- 由JSP規範提供,不用使用者例項化;
- 通過Web容器實現和管理;
- 所有JSP頁面均可使用;
- 只有在指令碼元素的表示式或程式碼段中才可使用(<%=使用內建物件%>或<%使用內建物件%>)而不能在JSP宣告中使用,因為內建物件都是_jspService()方法的區域性變數。
表1. JSP 九大內建物件
內建物件 | 說明 | 型別 | 作用域 |
---|---|---|---|
pageContext | 頁面上下文物件 | javax.servlet.jsp.PageContext | Page |
request | 請求物件 | javax.servlet.ServletRequest | Request |
session | 會話物件 | javax.servlet.http.HttpSession | Session |
application | 應用程式物件 | javax.servlet.ServletContext | Application |
response | 響應物件 | javax.servlet.SrvletResponse | Page |
out | 頁面輸出物件 | javax.servlet.jsp.JspWriter | Page |
config | 配置物件 | javax.servlet.ServletConfig | Page |
exception | 異常物件 | javax.lang.Throwable | Page |
page | 頁面物件 | java.lang.Object | Page |
1、JSP內建物件的實質
我們通過一個JSP例項來認識JSP內建物件的實質,其JSP程式碼片段和轉譯後的Java程式碼片段分別如下:
JSP程式碼片段(isErrorPage=”true”表明這是一個異常處理頁面,因為只有異常處理頁面才有exception物件)
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isErrorPage="true"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
...
</html>
對應轉譯後的Java程式碼片段:
public final class exception_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent {
...
public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, ServletException {
PageContext pageContext = null;
HttpSession session = null;
Throwable exception = org.apache.jasper.runtime.JspRuntimeLibrary.getThrowable(request);
if (exception != null) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
ServletContext application = null;
ServletConfig config = null;
JspWriter out = null;
Object page = this;
try {
_jspxFactory = JspFactory.getDefaultFactory();
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
......
}
}
}
我們可以從上述JSP程式碼片段轉譯後的Java程式碼片段看出,request、response、session、application、out、pagecontext、config、page、exception 這九個內建物件都是_jspService()方法的 區域性變數(request 和 response 兩個內建物件是該方法的形參,實質上也是它的區域性變數) ,根據JSP轉譯規則 ,我們可以直接在JSP指令碼(只有在指令碼元素的表示式或程式碼段中才可使用,不能在JSP宣告中使用)中使用這些物件,而無須建立它們。
特別地,只有設定isErrorPage=”true”的頁面才是異常處理頁面,而且只有異常處理頁面對應的Servlet才會初始化 exception 物件。
2、請求/響應架構
一般地,我們稱基於 Web 的應用為 B/S 應用,這些應用一般都是 請求/響應 架構的,即總是先由客戶端傳送請求,伺服器接收到請求並返回響應資料。現在,我們概述一下基於 請求/響應 架構的應用的執行機制,以及在其中扮演重要角色的 瀏覽器 和 Web伺服器 各自的作用,其示意圖如下:
1). 瀏覽器的作用
(1). 向遠端伺服器傳送請求;
(2). 讀取遠端伺服器返回的字串資料;
(3). 負責根據返回的字串資料渲染出一個豐富多彩的頁面。
2). Web伺服器的作用
Web伺服器負責接收客戶端請求,每當接收到客戶端連線請求之後,Web伺服器就會單獨開啟一個執行緒為該客戶端提供服務。對於每次客戶端的請求而言,Web 伺服器大致需要完成如下幾個步驟:
(1). 為客戶端請求啟動單獨的執行緒;
(2). 使用 I/O 流讀取使用者請求的二進位制流資料;
(3). 從請求資料中解析請求引數;
(4). 處理請求;
(5). 生成響應資料;
(6). 使用 I/O 流向客戶端傳送請求資料;
上面六個步驟中,第1、2和6步是通用的,由 Web伺服器來完成,但第3、4和5步則存在差異。因為不同請求的請求引數不同,處理請求的方式也不同,所生成的響應自然也不同,那麼Web伺服器如何執行這些步驟呢?
實際上,Web伺服器會呼叫Servlet的 _jspService() 方法來處理這些事情。我們知道,在轉譯時期,JSP中的靜態頁面和java指令碼都會轉換為_jspService()方法的執行程式碼,這些執行程式碼就負責完成解析請求引數、處理請求、生成響應等功能,而Web伺服器則負責完成多執行緒、網路通訊等底層實現。
Web 伺服器執行到第三步得到請求引數後,會根據這些請求引數來建立 HttpServletRequest、HttpServletResponse 等物件,作為呼叫 _jspService()方法的引數。由此可見,一個Web伺服器必須實現Servlet API中絕大部分介面提供實現類(也就是說,Web伺服器要實現J2EE中的Servlet、JSP等標準)。
3、JSP/Servlet的通訊與內建物件的作用域
我們知道,Web應用中的 JSP、Servlet 等程式都是由Web伺服器來呼叫的,JSP與Servlet之間通常不會相互呼叫,那麼在它們之間 如何共享、交換資料以便相互通訊就成為了一個極為關鍵的問題。
為了解決這個問題,幾乎所有的Web伺服器都會提供四個類似Map的結構,即page、request、session 和 application,並允許 JSP/Servlet 在這四個 類似Map的結構 中存取資料,而這 四個Map結構 的區別僅在於作用域不同。特別地,JSP 中 pageContext、request、session 和 application 四個內建物件分別用於操作 page、request、session 和 application 範圍內的資料。
作用域是指變數的有效範圍。我們通過一個例子進行簡單說明,假設這樣一個場景:
當我們訪問 index.jsp 的時候,分別對 pageContext, request, session 和 application 四個作用域中的整型變數進行累加。然後,從 index.jsp forward 到 test.jsp,並在 test.jsp 裡再進行一次累加,然後顯示出這四個整數來。從顯示的結果來看,我們可以直觀的看到:
- page 範圍裡的變數無法從index.jsp傳遞到test.jsp,只要頁面跳轉了,它們就不見了;
- request 範圍裡的變數可以跨越forward的前後的兩頁,但是隻要重新整理頁面,它們就重新計算了;
- session 範圍裡的變數一直在累加,開始還看不出區別,但只要關閉瀏覽器,再次重啟瀏覽器訪問這個頁面,它們就重新計算了;
- application 範圍裡的變數一直在累加,除非你重啟tomcat,否則它們會一直變大。
實際上,
(1) 如果我們把變數放到pageContext裡,就說明這個變數的作用域是page,它的有效範圍只在當前jsp頁面裡。也就是說,從把變數放到pageContext開始,到jsp頁面結束,你都可以使用這個變數。
(2) 如果把變數放到request裡,就說明這個變數的作用域是request,它的有效範圍是當前 請求週期。所謂請求週期,就是指從http請求發起,到伺服器處理結束並返回響應的整個過程。在這個過程中可能使用forward的方式跳轉了多個jsp頁面,但由於仍然是同一個請求,因此在這些頁面裡,我們都可以使用這個變數。
(3) 如果把變數放到session裡,就說明這個變數的作用域是session,它的有效範圍是 當前會話。所謂當前會話,就是指從使用者開啟瀏覽器開始,到使用者關閉瀏覽器這中間的過程,這個過程可能包含多個請求和響應。也就是說,只要使用者不關瀏覽器,伺服器就有辦法知道這些請求是一個人發起的,整個過程被稱為一個會話(session),而放到會話中的變數,就可以在當前會話範圍內使用。
(4) 如果把變數放到application裡,就說明這個變數的作用域是application,它的有效範圍是 整個應用。所謂整個應用是指從應用啟動,到應用結束。特別地,我們沒有說“從伺服器啟動,到伺服器關閉”,這是因為一個伺服器可以部署多個應用,只要你結束了當前應用,這個變數就失效了。
與上述三個作用域不同的是,application 作用域裡的變數的存活時間是最長的,如果不進行手工刪除,它們就一直可以使用。此外,application裡的變數可以被所有使用者共用,也就是說,如果使用者甲的操作修改了application中的變數,使用者乙訪問時得到的是修改後的值。這在其他作用域中是不會發生的,page、 request 和 session 都是完全隔離的,無論如何修改都不會影響其他使用者。
二. application 物件
application 物件代表 Web 應用本身,因此我們常常使用 application 來操作 Web 應用層面的相關資料。我們其有如下兩個作用:
1) 通過以下兩個方法在整個Web應用的多個JSP、Servlet間共享Web應用狀態層面的資料:
- setAttribute(String attrName, Object value)
- getAttribute(String attrName)
2) 訪問獲取 Web 應用的配置引數:
使用 getInitParameter(String attrName) 方法獲取 Web 應用的配置引數,這些引數應該在 web.xml 檔案中使用 <context-param></context-param> 元素進行配置,每個元素配置一個引數,例如:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- Web 應用配置引數 -->
<context-param>
<param-name>admin</param-name>
<param-value>Rico</param-value>
</context-param>
<context-param>
<param-name>campus</param-name>
<param-value>NEU & TJU</param-value>
</context-param>
</web-app>
通過這種方式可以將一些配置資訊放在web.xml檔案中配置,從而避免使用硬編碼的方式寫在程式碼中,從而更好地提高程式程式碼的可移植性。
三. pageContext 物件
pageContext 物件代表 JSP 頁面上下文,其主要用於訪問JSP間的共享資料,其可以訪問 page、request、session 和 application 範圍內的變數。其有如下兩個作用:
1) 通過以下兩個方法訪問共享資料:
(1) get/setAttribute(String name):取得取得/設定 page 範圍內的 name 屬性;
(2) get/setAttribute(String name, int scope):取得/設定指定範圍內的 name 屬性,其中 scope 可以是如下四個值;
- public static final int PAGE_SCOPE = 1;
- public static final int REQUEST_SCOPE = 2;
- public static final int SESSION_SCOPE = 3;
- public static final int APPLICATION_SCOPE = 4;
2) 訪問獲取其它內建物件
例如:
- HttpSession getSession();
- Object getPage();
- ServletRequest getRequest();
- ServletResponse getResponse();
- Exception getException();
- ServletConfig getServletConfig();
ServletContext getServletContext();
因此,我們一旦獲得了 pageContext 物件,就可以通過它獲取其他的內建物件。
四. request 物件
request 物件是JSP中的重要物件,每個 request 物件封裝一個使用者請求,並且所有的請求引數都被封裝到request 物件中,因此request物件是獲取請求引數的重要途徑。此外,request 可代表本次請求範圍,用於操作 request範圍的屬性。總的來說,該物件有如下兩個作用:
1) 獲取請求引數/請求頭
Web 應用是請求/響應架構的應用,瀏覽器傳送請求時總會附帶一些請求頭,還可能包含一些請求引數傳送給伺服器。實際上,請求頭和請求引數都是使用者傳送到伺服器的資料,只不過前者通常由瀏覽器自動新增,而後者需要開發人員控制新增。伺服器端負責解析請求頭/請求引數的就是 JSP/Servlet,而 JSP/Servlet 獲取請求引數的途徑就是 request 物件,其提供如下幾個方法來獲取請求引數:
- getParameter(String name)
- getParameterMap()
- getParameterNames()
- getParameterValues(String name)
此外,客戶端傳送請求的方式一般有兩種:
GET 方式的請求:直接在瀏覽器位址列輸入訪問地址所傳送的請求或提交表單的預設請求引數傳送方式;
POST 方式的請求:以 post 方式提交表單,傳送請求引數。該種方式一般將請求引數放在HTML HEADER 中傳輸,因此使用者不能在位址列看到請求引數值,安全性較高。
因此,一般建議以POST的方式傳送請求。特別地,對於通過提交提交表單傳送請求引數而言,需要注意兩點:
- 只有具有name屬性的表單域才生成請求引數;
若多個表單域有相同的name屬性,則這些表單域只生成一個請求引數,只是該引數具有多個值。
關於JSP中文亂碼更多的介紹,包括 頁面亂碼、引數亂碼、表單亂碼、原始檔亂碼 等知識,見我的另一篇部落格《JSP中文亂碼問題終極解決方案》。
2) 操作 request範圍的屬性
使用 request物件的如下兩個方法可以設定/獲取request範圍的屬性(特別注意的是,forward前後仍是同一個請求):
- setAttribute(String name, Object o)
- getAttribute(String name)
3) 執行 forward/include 操作
HttpServletRequest 提供了 getRequestDispatcher(String path) 方法去執行 forward/include 操作,也就是代替JSP所提供的 forward/include 動作指令,其中引數path就是希望 forward/include 的路徑。具體方式如下:
request.getRequestDispatcher("目標路徑").forward/include(request, response);
上述程式碼的語義就是將請求forward到了目標頁面或將目標頁面包含到了當前頁面。需要注意的是,以該種方式進行 forward/include 時,path引數必須以 “/” 開頭。
五. session 物件
session 物件代表一次使用者會話,具體指從使用者開啟瀏覽器開始,到使用者關閉瀏覽器,這個可能包含多個請求和響應的過程。因此,我們常常使用 session 來跟蹤使用者的會話資訊,如判斷使用者是否登入,或者在包含購物車的應用中用於追蹤使用者購買的商品等。我們可以通過以下兩個方法進行存取session範圍內的屬性:
- setAttribute(String attrName, Object value)
getAttribute(String attrName)
特別地,通常只應該把與使用者會話狀態相關的資訊放入session範圍內,而不應該僅僅為了兩個頁面間交換資訊,就將該資訊放入session範圍內。
Session 的屬性值可以是任何可序列化的 Java 物件。
六. response 物件
response 物件代表伺服器對客戶端的響應。但大部分情況下,程式無須使用 response 物件來響應客戶端請求,因為有個更為簡單的響應物件 —— out,它代表頁面輸出流,直接使用 out 生成響應更簡單。不過,out 是 JSPWriter 的例項,是字元流輸出物件,無法輸出非字元內容。因此,若需要在 JSP 頁面動態生成一副點陣圖或者輸出一個PDF文件,則必須使用 response 作為響應輸出,此不贅述。此外,response 物件還有兩個重要作用,即重定向和操作Cookie。
1) 重定向
請求重定向與 forward 不同,重定向會丟失所有的請求引數 和 request範圍內的屬性。因為重定向將生成一個全新的請求,與前一次請求不在同一個 request 範圍內,所以會丟失原有請求的所有請求引數和request範圍內的屬性。我們通過以下方式進行重定向:
response.sendRedirect("request/MyJsp.jsp");
特別需要注意的是,請求重定向方法 sendRedirect() 的 path引數不必以 “/” 開頭(若以 “/” 開頭,其代表的是 “http://localhost:8080/“)。
2) 操作 Cookie
Cookie 通常用於網站記錄客戶的某些資訊,例如客戶的使用者名稱等。一旦使用者下次登入,網站就可以獲取到客戶的相關資訊,根據這些客戶資訊,網站可以對客戶提供更為友好的服務。Cookie 與 Session的最大不同在於:session會隨著瀏覽器的關閉而失效,但Cookie會一直存放在客戶端的機器上,除非超出Cookie的生命週期(Cookie的預設生命週期就是整個會話)。通常,如下面的例子所示,在客戶端增加一個 Cookie 分為如下三個步驟:
- 建立Cookie例項,其構造器為 Cookie(String name, String value);
- 設定Cookie的生命週期;
- 向客戶端增加Cookie。
// 獲取請求引數
String name = request.getParameter("name");
// 以獲取到的請求引數為值,建立一個Cookie物件
Cookie c = new Cookie("username" , name);
// 設定Cookie物件的生存期限
c.setMaxAge(24 * 3600);
// 向客戶端增加Cookie物件
response.addCookie(c);
out.print(name);
訪問客戶端的Cookie時,需要使用request物件。request物件提供了getCookies()方法,該方法將返回客戶端機器上所有Cookie組成的陣列,遍歷該陣列的每個元素,找到希望訪問的Cookie即可,例如:
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies){
out.print(cookie.getName() + " : " + cookie.getValue() + "</br>");
}
特別地,Cookie值不允許出現中文字元,如果需要值為中文內容的Cookie時,可以藉助於 java.net.URLEncoder/URLDecoder 進行轉換。更多關於 java.net.URLEncoder/URLDecoder的介紹見我的另一篇博文《使用URLDecoder和URLEncoder對中文進行處理》。
七. config 物件
config 物件代表當前 JSP 配置資訊,但 JSP 頁面通常無需配置,因此也就不存在配置資訊,所以在 JSP 頁面較少使用這個物件。但是,其在 Servlet 中經常使用(javax.servlet.ServletConfig 的例項)。因為Servlet需要在web.xml中進行配置,可以指定配置引數。因此,我們常常使用 getInitParameter(String paramName) 來獲取Servlet的配置引數,例如:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- Servlet 配置 -->
<servlet>
<servlet-name>config</servlet-name>
<servlet-class>com.edu.tju.rico.servlet.configServlet</servlet-class>
<init-param>
<param-name>name</param-name>
<param-value>Rico</param-value>
</init-param>
<init-param>
<param-name>age</param-name>
<param-value>24</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>config</servlet-name>
<url-pattern>/servlet/config</url-pattern>
</servlet-mapping>
八. exception 物件
exception 物件是Throwable的例項,代表JSP指令碼中產生的異常和錯誤 ,是JSP頁面異常機制的一部分,並且只有當isErrorPage屬性設為true時才可以訪問exception內建物件。我們知道,在JSP指令碼中通常無需處理異常,即使是checked異常。事實上,JSP指令碼所有可能出現的異常都可以交給錯誤處理頁面處理。看下面非異常處理頁面轉譯成Servlet後的例子(片段):
public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, ServletException {
// 內建物件的宣告
try {
// 內建物件的初始化
out.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\r\n");
out.write("<html>\r\n");
//... ...
out.write(" </body>\r\n");
out.write("</html>\r\n");
} catch (Throwable t) {
if (!(t instanceof SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
out.clearBuffer();
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
}
} finally {
if (_jspxFactory != null)
_jspxFactory.releasePageContext(_jspx_page_context);
}
我們可以看到,之所以在JSP指令碼中通常無需處理異常,包括checked異常,是因為當JSP轉譯成Servlet後,其靜態HTML和動態java指令碼都將置於 try 語句塊中。特別地, exception 物件只有在異常處理頁面中才有效。看下面異常處理頁面轉譯成Servlet後的例子(片段):
public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, ServletException {
// 內建物件的宣告
PageContext pageContext = null;
//...
PageContext _jspx_page_context = null;
Throwable exception = org.apache.jasper.runtime.JspRuntimeLibrary.getThrowable(request);
if (exception != null) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
try {
// 內建物件的初始化
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
//...
//使用內建物件pageContext給_jspx_page_context賦值
_jspx_page_context = pageContext;
out.write("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\r\n");
out.write("<html>\r\n");
... ...
out.write("</html>\r\n");
} catch (Throwable t) {
if (!(t instanceof SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
out.clearBuffer();
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
}
} finally {
if (_jspxFactory != null) _jspxFactory.releasePageContext(_jspx_page_context);
}
}
如上面程式碼片段所示,一旦 try 語句塊 捕獲到JSP指令碼的異常,並且 _jspx_page_context 不為 null,就會由該物件來處理異常。實際上,該物件的處理邏輯也很簡單:如果該頁面的page指令指定了errorPage屬性,則將請求forward到errorPage屬性所指定的頁面,否則使用系統頁面來輸出異常資訊即可。特別地,JSP宣告部分仍然需要處理checked異常。
九. out 物件
out 物件代表JSP頁面輸出流 ,通常用於在頁面上輸出變數值及常量。實際上,輸出表達式(<%= %>)的本質就是out.write(…),因此二者作用等價,在使用上可以互換。
此外,page 物件代表頁面本身,也就是Servlet類中的this,也就是說,能用page的地方就能用 this。
關於JSP的原理、執行過程、指令碼元素、編譯指令和動作指令的介紹見我的上一篇部落格《 Java Web基礎 — Jsp 綜述(上)》。
引用