之前一直說的是Servlet,但是由於只是做一個Demo,並沒有完全顯示一個介面,所以它的兩個缺點沒有顯示出來:
所有的HTML標籤和文本都必須使用一個字串形式通過ServletResponse的執行個體寫出去; 所有的文本和樣式必須得進行寫入程式碼,也就是說如果你想稍微更改一下前端的顯示的話,必須得重新編譯Servlet執行個體; 針對這兩個問題,提出了jsp這種技術,一般來說jsp有點像HTML檔案,但是可以看到裡面還有一些其他的元素,這就是java代碼,但是相對於Servlet,它就是一個文字檔,不需要進行編譯(其實是你沒有看見而已),也就是說當你新添加一個JSP頁面時不需要重啟Servlet容器,並且該分頁檔可以使用任何的文字編輯器編輯。JSP頁面一般是在JSP容器中啟動並執行,Servlet容器一般也是JSP容器,所以說Tomcat也可以充當JSP容器。 一、 請求JSP頁面 對JSP頁面的請求在本質上和請求一個Servlet是類似的,但是我們上面提到JSP頁面的一大優點就是不需要編譯,其實嚴格來說這是不對的,它只是不需要靜態編譯,也就是在正式部署運行之前是不需要編譯的,但是在web應用部署運行之後則會動態地進行轉換為servlet類並且編譯這個類。 當第一次請求JSP頁面時,會將轉換成一個頁面實作類別,轉換的工作是由servlet容器完成的,所產生的servlet的名稱由servlet容器決定,如果轉換錯誤則會發出錯誤到用戶端;如果轉換成功則會進一步被servlet容器編譯(此處是動態編譯)成servlet類,之後容器載入和執行個體化該類,並執行相應的生命週期方法; 對該頁面的後續請求,servlet容器也不傻,每次都轉換它也累,所以它會檢查該JSP頁面自從上一次轉換後是否發生修改,如果修改過則會重新轉換、編譯並執行,如果沒有則執行記憶體中已經存在的JSP類執行個體。 從上述說明中可以看到,對某個JSP頁面的第一次請求是比較耗費時間的,有沒有解決辦法呢。當然有了。。 配置應用程式啟動時就轉換和編譯所有的JSP頁面,而不是在接收第一次請求才做這些工作; 預先編譯JSP頁面,並將它們以servlet的形式進行部署; 關於這方面是有一些工具提供使用的,還有一些web伺服器是提供這方面的配置的,這裡就不細說了,但是我是使用的tomcat,沒有找到這方面相關的配置,如果哪位大神指教一下,感激不盡啊。。 二、 JSP的本質分析 首先說一下和JSP相關的包,如下: javax.servlet.jsp:包含核心的介面和類,servlet容器使用這些介面和類將JSP頁面轉換成servlet,最重要的莫過於JspPage和HttpJspPage;
javax.servlet.jsp.el:包含支援JSP中的EL運算式的相關的類和介面
javax.servlet.jsp.tagext:包含用於開發定製標籤的類型,也就是如果你想自己寫一個標籤必須用到這裡的API;
javax.el:為Unified EL提供API; JSP轉換產生的頁面類必須實現或者間接實現javax.servlet.jsp.JspPage介面,不出你所料,這個介面繼承了javax.servlet.Servlet介面,如下:
這樣我們就可以相信了,JSP頁面確實是一個Servlet(我沒有胡謅。。)。為了直觀一些,我們直接看JSP頁面轉換產生之後的servlet類,通過對比進一步解釋JSP的本質,如下是一個JSP頁面index.jsp:
<html><body><h2>Hello World!</h2></body></html>
可以看出這就是由普通的HTML標籤組成的文本(你騙我,這不是JSP,這就是HTML),前面說過JSP頁面其實就是由HTML元素和java程式碼群組成的。 轉換成對應的servlet,index_jsp.java如下,從名字可以看出jtomcat將jsp頁面轉換成servlet時的名稱是由“名稱_jsp”組成:
/* * Generated by the Jasper component of Apache Tomcat * Version: Apache Tomcat/7.0.68 * Generated at: 2016-04-16 12:01:36 UTC * Note: The last modified time of this file was set to * the last modified time of the source file after * generation to assist with modification tracking. */package org.apache.jsp;import javax.servlet.*;import javax.servlet.http.*;import javax.servlet.jsp.*;public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent { private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory.getDefaultFactory(); private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants; private volatile javax.el.ExpressionFactory _el_expressionfactory; private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.Map<java.lang.String,java.lang.Long> getDependants() { return _jspx_dependants; } public javax.el.ExpressionFactory _jsp_getExpressionFactory() { if (_el_expressionfactory == null) { synchronized (this) { if (_el_expressionfactory == null) { _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory(); } } } return _el_expressionfactory; } public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() { if (_jsp_instancemanager == null) { synchronized (this) { if (_jsp_instancemanager == null) { _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig()); } } } return _jsp_instancemanager; } public void _jspInit() { } public void _jspDestroy() { } public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html"); 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(); _jspx_out = out; out.write("<html>\r\n"); out.write("<body>\r\n"); out.write("<h2>Hello World!</h2>\r\n"); out.write("</body>\r\n"); out.write("</html>\r\n"); } catch (java.lang.Throwable t) { if (!(t instanceof javax.servlet.jsp.SkipPageException)){ out = _jspx_out; if (out != null && out.getBufferSize() != 0) try { if (response.isCommitted()) { out.flush(); } else { out.clearBuffer(); } } catch (java.io.IOException e) {} if (_jspx_page_context != null) _jspx_page_context.handlePageException(t); else throw new ServletException(t); } } finally { _jspxFactory.releasePageContext(_jspx_page_context); } }}
從上面的原始碼來看index_jsp繼承了org.apache.jasper.runtime.HttpJspBase類,那麼這個類是什麼。從tomcat的javaDoc來看,如下: 這個類繼承了了HttpServlet並且還實現了JspPage和HttpJspPage介面。在JSP中的主體代碼被轉換成index_jsp類的_jspService()方法中的內容,並且這個方法是由Httpbase中的service()方法調用的,如下:
/** * Entry point into service. */ @Override public final void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { _jspService(request, response); } @Override public abstract void _jspService(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
當index_jsp繼承了Httpbase時,則必須實現_jspService()方法,那麼當響應來的時候就可以被httpbase執行個體調用。
三、 JSP隱式對象 其實之前每次看到這部分的時候,我都會很懵,因為不知道這些隱式對象是怎麼起作用的(人類總是對自己無知的事情或者隱藏的事情充滿了好奇和恐懼)。這次順便一起說明一下,也是對自己的督促。我們知道每次servlet初始化或者接受請求的時候servlet容器都會傳給生命週期方法一些對象,那麼問題來了,JSP頁面怎麼接受這些對象。是在JSP頁面轉換成servlet的時候隱式地將這些對象加入到產生的servlet類中的,看上面由JSP頁面產生的servlet類可以看出這一點,下面是隱式對象和類型之間的對應關係:
對象 |
類型 |
request |
javax.servlet.http.HttpServletRequest |
response |
javax.servlet.http.HttpServletResponse |
session |
javax.servlet.http.HttpSession |
application |
javax.servlet.ServletContext |
config |
javax.servlet.ServletConfig |
exception |
java.lang.Exception |
out |
javax.servlet.jsp.JspWriter |
pageContext |
javax.servlet.jsp.PageContext |
page |
javax.servlet.jsp.HttpJspPage |
對於上述對象可以當做已經存在而直接使用,這裡主要說三個對象: out:這個執行個體的類直接繼承自java.io.Writer,所以可以直接將資料寫出隨響應而返回給用戶端,就像我們在servlet中做的那樣,response.getWriter().write(); page:表示該頁面,基本沒什麼用; pageContext:這個類表示這個頁面的上下文環境,可以用來管理和儲存在這個頁面使用的屬性,另外從上面產生的原始碼來看,通過這個屬性可以獲得其他幾個對象,如下;
application = pageContext.getServletContext();config = pageContext.getServletConfig();session = pageContext.getSession();out = pageContext.getOut();
另一個重要的作用對於pageContext來說,它可以儲存各個作用範圍的屬性,由於它是直接繼承自javax.servlet.jsp.JspContext,所以可以這樣操作,有如下4個作用範圍: APPLICATION_SCOPE SESSION_SCOPE REQUEST_SCOPE PAGE_SCOPE 作用範圍是從大到小的順序列出的。對應的API如下,scope則可以使用上述4個範圍指定:
abstract public void setAttribute(String name, Object value, int scope)
另外還可以通過設定只在該頁使用的屬性,即頁面作用範圍,如下:
abstract public void setAttribute(String name, Object value)
四、 JSP頁面內容 JSP頁面既包括模板資料,也包括句法元素,之前HTML標籤和文本就是模板資料,而句法元素是由<%%>包含起來的內容,可以說除了句法元素之外全部是模板資料。關於模板資料我們這裡不詳細說明,不是我們的重點,這裡著重說句法元素。句法元素主要有以下幾種:
1、 指令 指令的作用是用來在JSP頁面轉換成servlet類的時候起到一種指導作用,說白了就是告訴JSP轉換器怎麼將一個JSP頁面轉換成servlet類,其中可以包含一些資料用來在轉換的過程中加入類中,由於指令實在太多,所以這裡我們說明兩個比較重要的,page指令和include指令。
Page指令: 就如同這個指令的名字暗示的那樣,這個指令是用來說明整個頁麵包含的資訊的,我們怎麼使用它呢。文法如下:
<%@ attribute=value attribute=value%>
它有幾個重要的屬性,如下:
<%@page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isErrorPage="false" %><%@page import="java.util.*, java.nio.file.*" session="true" buffer="8kb" autoFlush="true"%>
說明如下: contentType:這個大家就太熟悉了,就是http中的content-type,每次由ServletResponse的執行個體設定返回; pageEncoding:是該頁面的頁面編碼,如果有中文的話就要小心這個值,如果沒有可以直接預設(ISO-8859-1) isErrorPage:說明這個頁面是否是一個在發生錯誤時返回的頁面,可以用作進行異常處理時返回的頁面; import:從名字就可看出來就是對應於java程式中的import關鍵字,匯入依賴包; session:該屬性用來說明該頁面是否參與session管理,也就是說session在該頁面是否起作用; buffer:指定out對象的緩衝區大小,單位kb,也可以為none,表示不使用緩衝; autoFlush;說明緩衝區滿的時候是否自動重新整理,true時則會自動重新整理,如果是false,則必須手動調用response.getWriter().flush()進行強制重新整理,這個屬性如果看源碼的話是在PrintWriter中定義的; 關於這個指令還有如下幾個方面要注意: page指令可以放在頁面的任何位置,但是注意,對於上面
屬性中涉及編碼的必須放在最前面; page指令可以多次出現,但是如果在多個指令中出現同一屬性則它們的值必須相同,但是
import例外,它是進行累加的;
include指令: 這個指令使用比較簡單,它是用來包含另外一個檔案在當前頁面中,如果你的一個頁面比較大的話想分成多個表示不同地區的頁面則可以使用這個指令來做,另外如果部分內容在多個頁面中都被引用則可以做成一個獨立的頁面然後使用該指令包含在其他頁面中。比較熟悉的使用如下:
<html><body><%@include file="title.jsp" %><%@include file="/jsp/content.jsp" %><%@include file="foot.jsp" %></body></html>
使用這個指令有以下兩個問題要注意: 包含的檔案的路徑:如果file屬性以"/"開頭則說明,該檔案的路徑是以web應用程式在伺服器上的根路徑開始,也就是一個絕對路徑;如果不是,則該路徑被解釋成相對於該頁面的路徑,也就是說被包含的分頁檔必須和指令使用的頁面在同一個檔案夾下; 包含的檔案的類型:可以是JSP檔案,或者是jspf檔案,還可以是靜態檔案html
2、 指令碼元素 指令碼元素就是將java代碼轉成一個JSP頁面,主要有三種形式: Scriptlet 這就是一小段嵌入在HTML元素中的Java代碼,使用<%......%>包含起來,如下:
<%Enumeration<String> enums = request.getHeaderNames();while(enums.hasMoreElements()) {String headerName = enums.nextElement();out.write(headerName + ": " + request.getHeader(headerName));}%>
注意:定義在其中的變數在之後的Scriptlet是可見的,也就是說可以直接使用。 運算式 運算式其實就是Scriptlet的封裝,是通過<%= %>封裝起來的,它其實是把java的運算結果或者說傳回值直接賦值給了隱式對象out的print()方法,避免了我們在Scriptlet中使用out輸出,使用如下:
Now is <%=new Date(System.currentTimeMillis()) %>
聲明 聲明主要是用來聲明一下在JSP頁面中使用的變數和方法,不建議這麼使用,但是作為指令碼元素的一部分還是要提一下,文法是通過<%! %>包含對應的聲明部分。
<%!public String hello(){return "Hello world";}%><%=hello() %>
其實我們上邊對指令碼元素說了很多,在實際開發中並不常用,稍微懂一點web開發的都知道MVC模式,使用指令碼元素明顯違反了這種模式,鑒於之後又出現EL這種工具,所以更沒有必要使用指令碼元素了(不過自己做一些demo還是可以使用的)。 我們可以通過在部署描述符中配置來禁止使用指令碼元素,如下:
<jsp-config><jsp-property-group><url-pattern>*.jsp</url-pattern><scripting-invalid>true</scripting-invalid></jsp-property-group></jsp-config>
在jsp-config中還可以配置很多屬性,這裡就不一一介紹了,如果有興趣的可以去看部署描述符的schema檔案。
3、 動作 動作會被編譯成執行某個操作的Java代碼,有點類似於通過文本的配置然後在接受請求時卻會轉換成可執行檔代碼。動作有很多種,它們是以標籤的形式存在,除了標準的標準,還有一些定製的標籤,這裡就不說了,實在是太多了,如果想瞭解的話可以去查看JSTL相關的資訊,也可以瞭解一下怎麼編寫自訂標籤。 這裡作為舉例,說明兩個即可:
include: 該動作用於動態地包含另一個資源,這個可以是JSP頁面,servlet或者是HTML靜態頁面。文法如下:
<jsp:include page="world.jsp"><jsp:param value="name" name="lmy"/><jsp:param value="age" name="23"/></jsp:include>
注意它和上述include指令之間的區別。主要如下: include指令是
發生在JSP頁面轉換成servlet類的時候,因此include指令適合靜態頁面的包含,而
include動作則是發生在每次請求的時候,因此可以使用include動作傳遞參數,同時還因為include動作是發生在接受請求的時候,每次的被包含頁面的響應根據商務邏輯可以是最新的,但是由於include指令是在頁面轉換時作用的,所以如果被包含的頁面不變化,那麼返回的頁面內容總是相同的; 如果想要使用include動作的這種在接受請求時返回最新響應的機制,則被包含的頁面必須是以jsp結尾的; 關於include指令和include動作的實現原理其實是,include指令是包含一個頁面進去,而inclu動作其實包含進去一個URL地址,所以使用include動作其實在底層是向該被包含的頁面發送一個請求(自己理解),具體的原理看如下連結文章。
forward:
這個我們之前在 Java Web基礎知識之Filter:過濾一切你不想看到的事情中遇到過,通過它可以轉向不同的資源,包括JSP和servlet,關於這個標籤的底層實現是通過RequestDispatcher.forward()實現的,這個標籤的使用如下:
<jsp:forward page="world.jsp"><jsp:param value="name" name="lmy"/><jsp:param value="age" name="24"/></jsp:forward>
在轉寄的時候可以添加一些新的請求參數如上所示,關於轉寄和重新導向的區別見如下連結