1. 程式人生 > >Java EE - JSP 小結

Java EE - JSP 小結

lan empty xxx 邏輯操作 一起 tld 關於 前置 The

Table of Contents

  1. 前言
  2. JSP 與 Servlet
    1. JSP 初始化參數
  3. 腳本元素
    1. page 指令
    2. 禁用腳本元素
  4. EL 表達式
    1. EL 函數
    2. taglib 指令
  5. 標記
    1. TLD 文件的位置
    2. 標準動作
    3. 第三方標記庫
    4. 定制標記
      1. 標記文件
      2. 簡單標記處理器
      3. 標記的屬性與體
  6. include 指令
  7. 結語

前言

在接觸 Servlet 和 JSP 之前我一直覺得兩者中應該是 Servlet 比較難,接觸了之後才發現,JSP 的東西怎麽那麽多啊?

但是,進行了簡單的梳理後,發現 JSP 的東西雖然多,但是理清了以後其實也不是很難。

這篇博客的內容便是對 JSP 相關內容的簡單總結。

註意:

  1. 這篇博客中按照自己的理解將 JSP 的相關內容分為了四大元素1:腳本元素、指令、EL 表達式、標記
  2. 這是篇偏向總結的博客,關於 JSP 各個元素的使用的內容並不多

JSP 與 Servlet

JSP 和 Servlet 的關系十分緊密,可以說 Servlet 是學習 JSP 的前置基礎,這可能也是諸多教程將 Servlet 放在 JSP 前面的原因之一。

默認情況下,在初次訪問某個 JSP 的時候,這個 JSP 將會被容器編譯為 Servlet2,然後和一般的 Servlet 一樣,容器將會創建這個從 JSP 編譯過來的 Servlet 的實例。

然後將 請求對象響應對象

交給這個實例的 Service 方法進行處理。

也就是說,你編寫的 JSP 在最後將會被編譯為 Java 代碼,其中的 HTML 將會被當做字符串直接寫入響應,其他元素也會進行相應的處理。

這便意味著,JSP 可以和一般的 Servlet 一樣訪問 ServletContext,也可以擁有自己的 ServletConfig!

進一步的,只要進行簡單的處理,我們就可以直接在 JSP 中訪問請求、響應、上下文等對象。

JSP 初始化參數

JSP 初始化參數的配置和 Servlet 初始化參數的配置基本相同,只需要一點簡單的修改:

<servlet>
  <servlet-name>name</servlet-name>
  <servlet-class>...</servlet-class>
  <init-param>
    <param-name>...</param-name>
    <param-value>...</param-value>
  </init-param>
</servlet>

上面這是配置 Servlet 初始化參數的方式,JSP 只需要將 servlet-class 修改為 jsp-file 就可以了:

<servlet>
  <servlet-name>name</servlet-name>
  <jsp-file>/example.jsp</jsp-file>
  <init-param>
    <param-name>...</param-name>
    <param-value>...</param-value>
  </init-param>
</servlet>

腳本元素

腳本元素應該是很多人初學 JSP 接觸的第一個 JSP 元素,其組成為:scriptlets(<% %>)、scriptlet expressions(<%= %>) 和 scriptlet declarations(<%! %>)。

腳本元素 scriptlets 的功能為:嵌入 Java 代碼,當 JSP 被編譯為 Servlet 時,相應的 Java 代碼會被直接嵌入到 Service 方法體中。

腳本元素 scriptlet expressions 的功能為:將 Java 表達式的結果轉化為字符串寫入響應。

腳本元素 scriptlet declarations 的功能為:聲明這個 JSP 對應的 Servlet 的字段與方法。

這裏我們可以來看一個簡單的例子,對於如下的 JSP 文件來說:

<html>
  <head>
    <title>Example</title>
    <meta http-equiv="content-type" content="text/html" charset="utf-8" />
  </head>
  <body>
    <!-- scriptlets -->
    <%
    for (int i = 0; i < 10; ++i) {
      out.println(i);
    }
    %>

    <!-- scriptlet expressions -->
    <%= Math.random() %>

    <!-- scriptlet declarations -->
    <%!
    int count = 0;

    public int getCount() {
      return count;
    }
    %>
  </body>
</html>

將編譯後的結果簡化可以得到如下代碼:

public final class hello_jsp {
  int count = 0;

  public int getCount() {
    return count;
  }

  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {

    out.write("<html>\r\n");
    out.write("  <head>\r\n");
    out.write("    <title>Example</title>\r\n");
    out.write("    <meta http-equiv=\"content-type\" content=\"text/html\" charset=\"utf-8\" />\r\n");
    out.write("  </head>\r\n");
    out.write("\t<body>\r\n");
    out.write("    <!-- scriptlets -->\r\n");
    out.write("    ");

    // scriptlets
    for (int i = 0; i < 10; ++i) {
      out.println(i);
    }

    out.write("\r\n");
    out.write("\r\n");
    out.write("    <!-- scriptlet expressions -->\r\n");
    out.write("    ");
    // scriptlet expressions
    out.print( Math.random() );
    out.write("\r\n");
    out.write("\r\n");
    out.write("    <!-- scriptlet declarations -->\r\n");
    out.write("    ");
    out.write("\r\n");
    out.write("\t</body>\r\n");
    out.write("</html>\r\n");
  }
}

可以很清楚的看到腳本元素編譯成的 Java 代碼!

然後是腳本元素可以使用的隱式對象,這些對象就聲明在 Service 方法體中:

// request, response, pageContext, session, application, config, out, page
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;

  pageContext = _jspxFactory.getPageContext(this, request, response,
                                            null, true, 8192, true);
  application = pageContext.getServletContext();
  config = pageContext.getServletConfig();
  session = pageContext.getSession();
  out = pageContext.getOut();
}

如果是 錯誤頁面 的話,還會有一個 exception 隱式對象,但常用的隱式對象都在上面了。

可以看到,腳本元素的原理還是很簡單的,就是將 Java 代碼簡單處理後直接放到代碼中,算是四大元素中最沒有逼格的一個 @[email protected]

page 指令

由於指令和另外三大元素都有關系,而且有些元素對指令的依賴還很大,因此,指令的內容將會向這樣拆分開來講解。

使用指令時,我們是通過指令的 屬性 來影響這個 JSP 頁面的編譯與使用,而使用腳本元素意味著我們編寫的就是 Java 代碼,因此可以通過 page 指令的 import 屬性告訴容器這個 JSP 需要那些而外的依賴,容器將會把定義的 import 語句增加到生成的 Servlet 類代碼中。

比如這樣的一個 page 指令:

<%@ page import="java.util.List, java.util.Map" %>

生成的 Servlet 類代碼中將會包含:

import java.util.List;
import java.util.Map;

page 指令還用其他一些屬性,比如屬性 pageEncoding 可以設置當前頁面的編碼,避免中文亂碼。

禁用腳本元素

當我們在 DD3 添加如下配置的時候就會使得腳本元素無法使用(用了就會出錯):

<jsp-config>
  <jsp-property-group>
    <url-pattern>*.jsp</url-pattern>
    <scripting-invalid>true</scripting-invalid>
  </jsp-property-group>
</jsp-config>

EL 表達式

EL 表達式很簡單,尤其是對於使用者來說,只需要記住一些簡單的語法便可以直接上手使用,不需要像腳本元素那樣需要會 Java。

當然了,EL 表達式也僅僅只是表達式,當容器遇到 EL 表達式時,會計算這個表達式的結果並將其寫入響應。

比如說 EL 表達式 ${1 + 3}, Tomcat 容器的處理方式是:

out.write((java.lang.String) org.apache.jasper.runtime.PageContextImpl.proprietaryEvaluate("${1 + 3}", java.lang.String.class, (javax.servlet.jsp.PageContext)_jspx_page_context, null));

因此,EL 表達式難以完成復雜的邏輯操作,這時,我們便可以使用 EL 函數或標記。

EL 函數

使用 EL 函數是很簡單的,只需要使用 taglib 指令告訴容器你使用的 EL 函數來自什麽地方:

<%@ taglib prefix="mine" uri="xxx" %>

${mine:random()}

困難的地方在於 EL 函數的創建:

  1. 你需要編寫一個有 公共靜態 方法的 Java 類,比如:

    public class Example {
      public static double method() {
        return Math.random();
      }
    }
  2. 然後,你需要編寫一個 TLD4 文件建立 EL 函數和靜態方法的映射:

    <taglib>
      <uri>xxx</uri>
    
      <function>
        <name>random</name>
        <function-class>Example</function-class>
        <function-signature>
          double method()
        </function-signature>
      </function>
    
    </taglib>

關鍵在於這個 TLD 文件中的內容,TLD 文件中的 uri 就是 taglib 指令使用的 uri, 而 function 部分告訴容器可以在什麽地方找到這個函數。

也就是說是通過 TLD 文件的 function 標簽建立 EL 函數名和實際的函數之間的映射。

某種程度上,EL 函數也不復雜,但主要問題在於 EL 函數的映射是借助 TLD 文件建立的,在 JSP 中使用也需要使用 taglib 指令,這和 標記 混雜在了一起。

taglib 指令

taglib 指令的使用更多是在使用標記的時候,但是 EL 函數卻需要使用 taglib 指令來使用……

這個指令的常見形式如下:

<%@ taglib prefix="your-prefix" uri="..." %>

指令的 prefix 屬性可以自己隨便定義,而 uri 也只是一個標識,不一定需要是具體的路徑,只要和 TLD 文件中定義的 uri 相同就可以了。

標記

標記應該是 JSP 中最復雜的一部分,在我的理解中,標準動作、第三方標記庫、定制標記都屬於標記。

這就意味著標記這一節需要掌握的東西很多,而且需要分清楚不同的內容之間的區別。

TLD 文件的位置

在進一步了解標記之前需要先來看看 TLD 文件可以放在那些地方:

  1. 直接放在 WEB-INF 目錄下
  2. 直接放在 WEB-INF 目錄的一個子目錄下,比如說 WEB-INF/tlds
  3. WEB-INF/lib 下的一個 JAR 文件中的 META-INF 目錄中
  4. WEB-INF/lib 下的一個 JAR 文件中的 META-INF 目錄的子目錄中

只要你將 TLD 文件放在這些目錄中,容器就可以找到你自己定義的標記與 EL 函數。

標準動作

標記的語法比 EL 表達式還要簡單,使用上的問題主要集中在標記的作用、屬性與標記體上,因此這裏將會略過快速標準動作的相關內容。

標準動作中存在一個比較特殊的動作:,這個動作可以用來設置其父標記的屬性值:

<prefix:name>
  <jsp:attribute name="attributeName">value</jsp:attribute>
</prefix:name>

這個動作的特殊之處在於:即使父標記要求體為空,也任然可以通過 來設置父標記的屬性值。

其他一些常用的標準動作:

<jsp:include>、<jsp:param>、<jsp:forward>、<jsp:useBean>、<jsp:setProperty>、<jsp:getProperty>

第三方標記庫

這裏的第三方標記庫包括 JSTL,雖然說 JSTL 被叫做標準標記庫,但它不是和標準動作不一樣,不是內置的標記。

使用時和其他第三方標記庫一樣,需要將包含標記庫的 jar 放到 WEB-INF/lib 目錄。

如果你解壓包含 JSTL 的 jar,就可以看到前面說的在 jar/META-INF 目錄下的 TLD 文件了。

JSTL 使用時通常使用如下形式的 taglib 指令:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

這個 uri 可以在 jstl.jar/META-INF/c.tld 文件中發現:

<taglib>
  <uri>http://java.sun.com/jsp/jstl/core</uri>
</taglib>

定制標記

定制標記有三種方式:標記文件、簡單標記處理器和傳統標記處理器,這篇博客將只涉及標記文件和簡單標記處理器。

標記文件

標記文件更像是可以通過標記語法進行包含的 JSP 文件,使用它的 tablib 指令也和一般的指令存在一定區別:

<%@ taglib prefix="prefix" tagdir="xxx" %>

假如你的標記文件是直接放在 WEB-INF/tags 目錄或其子目錄中,那麽就可以通過 tagdir 屬性指定標記文件的位置,使用時就可以通過 <prefix:tagFileName> 的方式使用。

如果你的標記文件在 jar 中,那麽你就需要一個 TLD 文件來描述你的標記文件的位置:

<tagfile>
  <name>tagName</name>
  <path>/META-INF/tags/...</path>
</tagfile>

然後通過 uri 指定引用的標記文件,使用時通過 <prefix:tagName> 的方式使用。

在標記文件中,我們可以通過 attribute 指令聲明屬性,通過 tag 指令聲明標記文件的體的限制(這兩個指令只能在標記文件中使用):

<%@ attribute name="name" required="true" %>
<%@ tag body-content="tagdependent" %>

<p>Hello ${name}, <jsp:doBoby /></p>

上面這個標記文件:

  1. 通過 attribute 指令聲明了 name 屬性,這個屬性的值必須給出
  2. 通過 tag 指令說明了這個標記的題將會按原樣取出放入 <jsp:doBody> 的位置

比如說,假如這個標記為 tag:example,那麽如下內容:

<tag:example name="tony">${hello}</tag:example>

將會被翻譯為:

<p>Hello tony, ${hello}</p>

簡單標記處理器

簡單標記處理器的實現還是比較簡單的,只需要擴展 SimpleTagSupport 類就可以了:

public class MyTag extends SimpleTagSupport {
  public void doTag() throws JspException, IOException {
    ...
  }
}

標記處理器可以訪問標記體、標記屬性,也可以訪問 PageContext 從而得到作用域屬性和請求及響應。

一個簡單的簡單標記處理器需要:

  1. 實現 doTag 方法
  2. 對於所有在 TLD 文件中聲明的屬性,給出對應的 set 方法

簡單標記處理器在 TLD 中的註冊形式:

<tag>
  <description>...</description>
  <name>tagName</name>
  <tag-class>package.className</tag-class>
  <body-content>empty</body-content>

  <attribute>
    <name>attrName</name>
    <requirend>true</requirend>
    <rtexprvalue>true</rtexprvalue>
  </attribute>
</tag>

到目前為止,需要在 TLD 文件中註冊的就有:EL 函數、標記文件、簡單標記處理器:

<taglib>
  <!-- EL 函數 -->
  <function>...</function>

  <!-- 標記文件 -->
  <tagfile>...</tagfile>

  <!-- 簡單標記處理器 -->
  <tag>...</tag>
</taglib>

標記的屬性與體

在自定義標記的時候我們可以限制標記的屬性與體的形式:

  • 當屬性的 rtexprvaluefalse 時,屬性值就只能是字符串字面量,為 true 時,可以有如下三個形式:

    <prefix:tag attr="${xxx}" %>
    <prefix:tag attr="<%= %>" %>
    <prefix:tag>
      <jsp:attribute name="attr">value</jsp:attribute>
    </prefix:tag>

    即:EL 表達式、腳本表達式、標準動作

  • 標記體的內容通過 body-content 進行限定,其值包括:

    含義
    empty 標記不能有體,但還是可以通過標準動作 設置屬性
    scriptless 標記體不能有腳本元素(默認)
    tagdependent 標記體將被看做純文本
    JSP 標記體支持一切 JSP 元素

對於標記文件來說,rtexprvalue 可以通過 attribute 指令的 rtexprvalue 屬性設置,而標記文件的體不能有腳本元素,因此值只能是 scriptless、tagdependent 和 empty,可以通過 tag 指令的 body-content 屬性進行設置。

include 指令

我們可以通過 include 指令將一個資源的內容增加到一個 JSP 中,作用相似的還有標準動作 <jsp:include> 和 JSTL 標記 <c:import>.

其中,include 指令告訴容器復制目標文件中的所有內容,並粘貼到使用這個指令的位置,這個過程在 JSP 編譯為 Servlet 時便完成了:

<%@ include file="xxx.jsp" %>

都是在處理請求是動態的將資源內容增加到 JSP 中:

<jsp:include page="xxx.jsp" />
<c:import url="xxx" />

結語

這篇博客目前來說達到了自己的目的:梳理 JSP 相關的內容。

當然了,這是篇偏向總結的博客,關於 JSP 各個元素的使用的內容並不多,更多的是我在學習過程的感覺比較重要的內容的記錄,以及較為困惑的內容的梳理。

……

Footnotes

1 其實算上 HTML 的話可以分為五大元素,但是 HTML 還是當做背景板好了

2 對於 Tomcat 來說,編譯得到的類文件可以在 tomcat/work/Catalina/localhost/{your-web-app} 找到

3 Deployment Descriptor - 部署描述文件,也就是常用的 web.xml 文件

4 標記庫描述文件,文件後綴為 tld

Java EE - JSP 小結