Java 服務端頁面 JSP

JSP ( java server page ) java 服務端頁面,從名字就可以看出它是運行在服務端的 java 代碼,由 Tomcat 來執行的。早期是沒有 JSP 這種東西的,動態網頁技術是通過在服務端輸出字符串的方式拼接出 HTML 代碼,然后將其發送給瀏覽器,由瀏覽器去渲染出可視化界面。但是這樣難度太大了,稍不留神就會出錯,簡直讓人崩潰。后來就出現了 JSP 技術,它能夠在 HTML 頁面中嵌入 java 代碼,然后由 Tomcat 去執行,轉成正式的 HTML 頁面,發送到客戶端瀏覽器去運行。

JSP 的執行流程

首先客戶端瀏覽器向服務端發送一個請求的頁面地址,服務端在收到用戶請求后找到對應的 xxx.jsp 文件,然后將其轉成 xxx_jsp.java 文件,再編譯成 xxx_jsp.class 字節碼文件,然后在堆區生成對應的 JSP 對象。以后客戶端每次訪問 xxx.jsp 都是相當于直接訪問堆區的這個 JSP 對象。所以才會有第一次訪問 JSP 頁面的時候,執行速度會很慢,之后速度就會變快,就是因為這中間存在一個轉換過程。

嚴格來說,這里講到的 JSP 對象應該稱為 Servlet 對象。以 welcome.jsp 為例,Tomcat 會把 welcome.jsp 轉成 welcome_jsp.java 文件。查看轉成 .java 文件的源碼后發現,這是一個 Servlet 類,此類繼承了 org.apache.jasper.runtime.HttpJspBase 類,這是一個繼承自 HttpServlet 類抽象類。繼承自HttpServlet,繼承自HttpServlet,繼承自HttpServlet,重要的事情說三遍啊。這還不能證明 JSP 就是 Servlet 嗎? 以后提起 JSP 只需要記住一句話,它間接繼承了 HttpServlet 類。

這足以證明 JSP 本質上還是一個 Servlet ,只是因為程序員直接用 Servlet 做 HTML 元素的字符串拼接太麻煩了,然后就提供了一種更為簡單的方式供程序員使用編寫代碼,然后將具體 HTML 字符串拼接的工作交給 JSP 轉換器自動去完成。本質并沒有改變,只是程序員用得更爽了。

每次訪問 JSP 頁面前,Servlet 容器都會去檢查 JSP 頁面是否改動了,如果改動就重新轉成 .java 文件,再編譯成 .class 文件,如果沒有改動過,就直接使用已經存在的這個對象。這也就是說,改動 JSP 頁面,不需要重啟 Servlet 容器,就可以看到新的改變。

學習 JavaSE 的時候,主角是 JVM ,但是到了學習 JavaWeb 的階段,JVM 就只是作為一個配角了,Tomcat 才是時刻存在、需要關注的主角。

JSP 注釋

JSP 頁面中有兩種注釋,一種是 JSP 注釋,一種是 HTML 注釋。

  • `` JSP 頁面中用來給 HTML 代碼做注釋的,Tomcat 并不會處理這種注釋,會把它發給瀏覽器去處理。但是因為是注釋,所以注釋中的內容不會被瀏覽器渲染出來,但是會存在 HTML 源代碼中。 web.xml 文件中也是用的這種方式做注釋,但是當然是 Tomcat 來處理的啊。通過配置信息來告訴框架程序該怎么執行,這就是 java 中應用的極為廣泛的聲明式編程。
  • <%--這是JSP的注釋--%> JSP 中的注釋,Tomcat 在將其轉成 .java 文件時就已經把注釋部分的內容給去掉了,更不可能被發送到客戶端。而且在<% %>這種標簽中的 java 代碼中還可以用 //單行注釋/*多行注釋*/

JSP 中的腳本元素

JSP 的腳本元素有三種類型,Scriptlet 、聲明及表達式。

  • Scriptlet 用<% %>表示,在此標簽中可以直接添加 java 代碼,但是通常不建議這樣做,會導致 JSP 頁面的可讀性簡直差到了極點。而且同一個 JSP 頁面中的多個 Scriptlet 中的 java 代碼最后都會被 Tomcat 放在轉成 .java 文件的void _jspService(HttpServletRequest request,HttpServletResponse response)方法中,所以這種 Scriptlet 中的變量本質上屬于局部變量,每調用一次就在對應線程棧中開辟一個方法棧幀,方法結束后,就釋放內存空間。在寫代碼的時候,想象下正在編寫的 JSP 頁面轉成 .java 文件的時候,代碼是如何組織的,就能夠很好的理解這種復雜的邏輯了。JSP 中的 HTML 元素在轉成對應 .java 文件時,最終還是通過 out.print() 方法進行字符串輸出的,所以本質沒有任何改變,只是提供了一個簡易的方式給程序員使用而已。
  • 聲明用 <%! %>表示,在這里面定義的變量屬于全局變量,在 JSP 頁面對應的位置屬于全局變量,也就是在內存中是在堆區對象的內存空間中。學習啟示:手動思考下 JavaSE 部分學習的內存劃分知識,不要學了 web 就忘了之前學的了,把不同抽象層次的東西建立聯系,才能夠真正做到融會貫通。
  • 表達式用 <%= %>表示,它后面跟的是一個變量或者常量,不需要加;。它對應到 .java 文件中,就是使用了out.print()方法進行輸出而已,方法內的參數變量肯定不能加上;啊。

JSP 中的指令

JSP 中的指令(Directive) 作用是,指示 JSP 轉換器應該如何將 JSP 頁面轉換成 Servlet 的命令。最常用的就是 page 和 include 了!

page 指令

page 指令是用來定義頁面的相關屬性的,常用的屬性有 contentType 、errorPage 及 import 等。

  • contentType 屬性,一般用法如下<%@page contentType = "text/html;charset=utf-8"%>,這種利用 charset 指定編碼的方式,指的是服務端發送給客戶端內容的編碼,所以發送到客戶端仍然極有可能會出現亂碼問題。真正要解決亂碼問題,還是應該用 page 指令的 pageEncoding 屬性,它指的是整個 JSP 頁面的編碼方式。
  • pageEncoding屬性,一般用法如下:<%@page contentType = "text/html" pageEncoding = "utf-8"%>它指的是整個 JSP 頁面的編碼方式。所以,以后 JSP 中的頁面用這種方式來指定編碼,并配合 HTML 屬性 <meta charset = "utf-8">指定瀏覽器使用這種方式讀取 HTML 頁面即可。

include 指令

通常一個 JSP 頁面有多個模塊組成,比如頂部的菜單欄,任何界面都需要此菜單,所以把它做成一個單獨的頁面,然后其他頁面去引用這個文件即可。這時候就需要用到 include 指令了!

使用方式如下:<%@include file = "menu.html"%>,file 屬性對應的 url 如果加上/,那么在服務端就會被解讀為一條絕對路徑,如果不加上/就會被解讀為當前 JSP 頁面的相對路徑。無論包含進來的是哪種文件,這個 include 指令的作用就是直接把對應文件的內容原封不動的拷貝過來,然后再轉成 .java 文件,進行編譯,所以可能會存在沖突問題。

JSP 標簽庫

在 JSP 中使用原生 java 代碼會造成頁面可讀性很差,難以維護。所以就想出了使用標簽來解決這個問題。標簽的思想就是,使用的一種看起來更簡潔、用起來方便的一種符號來替代大量的 java 代碼,使得編寫 JSP 頁面變的更簡單。通過查看源碼即可證明,事實如此。

對于 JSP 標簽,只需要理解它的思想即可,因為實際編程中最多只會用到 JSTL 標簽或者 web MVC 框架的標簽。而且 JSP 這種東西早就不用了,基于它之上的標簽就更不用說了。

這幾個標簽之前也花費了好多時間去學習,但是現在發現,學這東西根本就沒卵用啊。老師真的是有毒,明明知道用不上還教,搞得自己還傻不拉幾的花了這么多時間在這上面

所謂的 JSP 標簽其實也就是 JSP 設計者提供給程序員而已。使用標簽能夠少寫好多行代碼啊,用著超級爽的,而且在框架中還有特殊的用途,對于標簽要做到掌握 JSTL 和相應的 web 框架的標簽就可以了。JSP 引擎還是會把這些標簽轉換成原生 Java 代碼的,查看源碼即可證明。

JSP 中還有三個專門用來操作 JavaBean 的標簽,點此查看對應筆記

include 標簽

include 動作具體使用方式如下:<jsp:include page = "info.jsp">,利用這種 include 動作包含頁面和 include 指令包含不同點在于:include 指令無論包含的是靜態頁面還是動態頁面,都是直接將被包含頁面的內容原封不動的復制過來,然后 JSP 轉換器再將其轉成 .java 文件,最后編譯成 .class 文件。而 include 指令則是先對被包含頁面進行動態處理,將輸出信息包含進來,如果是靜態頁面當然也是直接包含進來。

所以,以后在需要進行包含頁面時,利用 include 動作而不是 include 指令,這樣更好。

還可以利用 include 動作給被包含頁面傳遞參數,因為本質上這是一次 http 請求,所以頁面之間共享的是同一個 request 對象,使用方式如下:

<jsp:include page = "menu.jsp">
    <jsp:param name="info" value = "www.tencent.com"/>
</jsp:include>

上述代碼的執行過程如下:先給 request 對象添加屬性 name 和 value 這樣的鍵值對元素,然后去執行被包含頁面(被包含頁面可以通過 request 對象得到傳遞過來的屬性),被包含頁面執行完后的結果被包含進來。

forward 標簽

forward 動作屬于服務端跳轉,也就是說瀏覽器并沒有第二次發送請求,從頭到尾都只是發送了一次請求,當前頁面和后續的跳轉頁面都是共享 request 等對象的(要理解這一點,前提是要理解 http 協議的執行過程)。所以在客戶端瀏覽器的地址欄中 url 并沒有改變,還是原來的請求 url 。

具體使用方式如下,同 include 動作一樣,也可以通過嵌套 param 動作來傳遞數據到后續頁面。

<jsp:forward page = "success.jsp">
    <jsp:param name = "userName" value = "password"/>
</jsp:forward>

JSP 腳本元素中的 9 個內置對象

JSP 中雖然號稱有 9 大內置對象,但是其實只要死死的理解了以下 6 個就可以了,另外 3 個就當它不存在吧。

  • request
  • response
  • session
  • application
  • config
  • page 此對象值在 JSP 中有用

在編寫 JSP 頁面時 ,它有 9 個不同功能的內置對象,不需要 JSP 程序員顯示進行初始化,就能夠直接使用。這給程序員提供了很大的便利,但無論是書本上還是老師上課都沒有講過這些內置對象是哪來的,上來就講內置對象有什么用、怎么用?給人感覺很虛,對內置對象的理解停留在非常表面的層次,知識始終是掌握的不牢靠的。

其實所謂的內置對象本質上就是 Tomcat 負責創建和管理的封裝了一系列參數信息的堆區的很普通的對象而已,JSP 中能夠使用它是因為 jsp_Service() 方法中定義了幾個按照規定命名的局部變量,然后拿到了這些對象在堆區的地址而已。而且這些內置對象不僅僅是只能在 JSP 中用到,在 Servlet 類中是一樣使用的,這有什么奇怪的嗎?沒有啊,同樣都是在操作系統管理的一個 JVM 進程中的堆區內存中運行的對象,為什么不能夠使用呢?不要因為內置對象是在學習 JSP 內容的時候了解到的,就以為內置對象只有 JSP 才能夠使用。

如果再說的深入一點,JSP 本質就是 Servlet 啊,所以內置對象本質上其實是給 Servlet 準備的。只是在 JSP 中直接拿到了這幾個對象并且按照約定俗成的名字命名好了給程序員使用(這部分代碼不許要客戶端程序員自己使用而已)而在 Servlet 中并沒有預先指定命名,需要自己去拿到這些內置對象,自己命名變量指向這些堆區的對象,僅此而已。

JSP 的 9 個內置對象到底是哪來的?

前面已經知道了 xxx.jsp 文件會被轉成對應的 xxx_jsp.java 文件,腳本元素<% %><%= %>中的代碼都會被放在對應 Servlet 類的 _jspService() 方法中。也只有在這兩種腳本元素內才能夠使用 JSP 的內置對象,它背后本質的原因是,_jspService() 方法中有 8 個局部變量,并且進行了相應的初始化,查看源碼即可發現,就是 JSP 中的除開 exception 對象的另外 8 個內置對象

這說明什么呢?因為這兩種腳本元素中的代碼最后是被放在同一個 _jspService() 方法中的,而 JSP 頁面中使用的 9 個內置對象的引用變量也是在這里進行初始化的,指向了堆區的內存對象。所以現在轉換成了什么問題呢?一個方法內的局部變量的作用范圍肯定只在這個方法內有效啊,所以只有在這兩個腳本元素中才能使用這 9 個內置對象,在<%! %>這個腳本元素中則不可以使用內置對象,因為這里面的變量和方法會被當成全局變量看待,已經離開了內置對象的作用范圍了。

其實已經很清晰明了了,想要有更直觀的感受,就看看_jspServeice()方法的源碼吧,那一目了然了。此處限于篇幅,刪去了部分不重要代碼

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;
  }

現在回過頭來看,上面的講述過程好混亂。直接看最后 _jspService() 方法的源碼就可以知道,8 個內置對象的引用就是在這里進行初始化的,兩種腳本元素中的 java 代碼最后也是被包含在這個方法中。所以,他們是在同一個方法,使用本方法的局部變量,這不是一件再正常不過的事情了嗎?每一次請求結束,此方法就會退棧,局部變量都會消失,但不代表堆區的對象也會消失。至于堆區的內存對象到底是什么時候創建的,什么時候消失的,取決于具體的內置對象,并不相同(內置對象的創建和管理是由 Tomcat 負責的)。比如 application 對象就會在 Tomcat 服務器啟動時創建(無論是哪次請求拿到的都是堆區的同一個內存對象),request 和 response 就會在每次有新的請求時創建,請求結束就立馬被銷毀(每次請求都是兩個新的對象)。這就是涉及到內置對象的生命周期的問題了,點此查看具體內置對象的筆記

學習啟示:在學習 JSP 的過程中,千萬不要停留在 JSP 頁面本身來考慮問題,而是要去想它轉成對應的 java 類后是怎么組織源碼的,然后這不就是你在學習 JavaSE 部分的知識點了嗎?這一塊基礎還算扎實吧,所以說,Java 基礎學得有多好,決定了未來能走多遠,其他看似很牛逼的東西,本質上都是對 JavaSE 部分的上層應用而已。

application 對象

點此查看 application 對象相關筆記

config 對象

config 對象是 javax.servlet.ServletConfig 接口的實例對象,代表當前 JSP 頁面的配置信息,但是通常 JSP 頁面不需要顯示的定義配置的信息,那是因為關于 JSP 的配置信息在 conf/web.xml 中已經配置好了,通常 JSP 頁面中不會用到它。

但是在自定義的 Servlet 中用的非常多,因為每一個 Servlet 類都需要顯示的在 WEB-INF/web.xml 中進行訪問 url 的配置,所以在對應的<servlet></servlet>元素中還可以進行參數的配置。Tomcat 會為每一個 Servlet 對象都創建一個單獨的 config 對象,也就是說每個 JSP 頁面也都會有一個唯一的 config 對象,它在 Tomcat 啟動時被創建,關閉時被銷毀。

并且自動的把這里面配置的參數名和參數值放到 config 對象中去。通過 ServletConfig 接口的getInitParam(String name)方法就可以得到配置文件中的屬性值了。

<servlet>
    <servlet-name>Simple</servlet-name>
    <servlet-class>lee.SimpleServlet</servlet-class>
    <init-param>
        <param-name>port</param-name>
        <param-value>3366</param-value>
    </init-param>
</servlet>

out 對象

JSP 頁面中可以使用 out 對象來進行輸出,但是這樣會導致頁面可讀性變差,通常是用<%= %>來替代。盡管在轉成 .java 文件后,也是采用的 out 對象進行輸出,但是這個代碼又不需要程序員來看,所以不用理睬這個問題, JSP 頁面才需要程序員閱讀。

page

JSP 四個內置對象域中有一個就是 page ,一般在設定屬性的生存范圍時會用到。它在 JSP 轉成 .java 源碼中是這么寫的Object page = this,就是說這個 page 引用指向的就是當前的這個 JSP 對象。JSP 中用的還比較多,Servlet 中基本不會用到它。

pageContext 對象

pageContext 是 javax.servlet.jsp.PageContext 的實例對象, 它在 Servlet 中基本不會用,但是在 JSP 中是超級有用。查看 JSP 轉成 .java 后的源碼就可知道,如下:

application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();

JSP 中的 9 個內置對象,除了 exception 比較特殊外,其他 8 個都是在這里面進行聲明的。其中 request 和 response 是通過依賴注入的方式由 Tomcat 傳遞過來的,上述的 4 個內置對象都是通過 pageContext 對象進行依賴查找的到的對象地址。由此可見在 JSP 中 pageContext 有多重要,但是在 Servlet 中通常是通過 request 對象得到這些內置對象實例地址的。

request 對象

點擊這里查看 request 對象的筆記

response 對象

點擊這里查看 response 對象的筆記

session 對象

點次查看 session 對象的筆記

JSP 內置對象的 4 種作用域

這其實就是內置對象的生命周期的問題了,不僅僅是這個對象,而且還包括依賴于這個對象而存在的屬性,比如將表單 Bean 對象作為這幾個內置對象的屬性存在,那么這些 Bean 對象的生存周期就是依賴于這些 JSP 內置對象。詳情參考 JavaBean 相關筆記

這四個內置對象分別是 page 、request 、session 和 application 。

  • page,只在當前 JSP 頁面內有效
  • request,在客戶端發送過來的一次請求周期內有效,服務端跳轉屬于一次請求
  • session,在和服務端連接的一次會話周期內有效,也就是說系統Cookie 在瀏覽器的運行內存中存在時有效,因為這就是一次會話。
  • application,在服務器啟動到關閉周期內,對于本 web 應用內有效。
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容