想要在 JavaServer Pages (JSP) 應用程序中添加自定義標簽嗎?本教程將為您展示如何用這些標簽編寫類似于 JSP 技術自帶操作 —— 如 jsp:useBean
、jsp:getProperty
和 jsp:forward
—— 的自定義操作。介紹如何用特定于自已的域的表示邏輯的自定義操作來擴展 JSP 語法。
在 JSP 應用程序中添加 自定義標簽 的能力可以使您將工作重點放到以文檔為中心的開發方式上。可以使 Java 代碼不出現在 JSP 頁中,從而使這些頁面更容易維護。(我從經驗中學到,在 JSP 頁中放入過多的 Java 代碼時,代碼維護就會成為可怕的任務)。本教程將使您可以立即開發出自定義標簽。了解了 JSP 自定義標簽開發的好處后,您可能會對程序員沒有更多地使用它而感到意外。
在本教程中,我將討論使用自定義標簽的基本內容。將介紹如何用自定義標簽創建可重用的表示組件并避免在 JSP 頁加入 Java scriptlet。
在本教程中,我們將:
- 定義一個 JSP 自定義標簽體系結構。
- 解釋簡單標簽。
- 定義嵌套標簽。
- 用
BodyContent
解釋標簽。
- 在標簽中添加屬性。
- 在標簽中添加 scriptlet 變量。
- 用自定義標簽實現控制流程。
- 用 Struts 簡化標簽部署。
如果發現自己在 JSP 應用程序中加入了大量 Java scriptlet,那么本教程就是為您準備的。 閱讀本教程后,就會掌握將 Java 代碼從 JSP 頁面中清除出去所需要的信息。
本教程假定讀者熟悉 Java 平臺、JavaServer Pages (JSP) 技術、MVC 模式、Reflection API、Model 2,最好還有 Struts 框架。此外,要從本教程中得到最大的收獲,還需要很好的使用標簽庫的經驗
Rick Hightower 是一位 J2EE 開發人員和顧問,他熱衷于使用 J2EE、Ant、Hibernate、Struts、IMB 的 ETTK 和 Xdoclet。 Rick 是 Trivera Technologies 的前任 CTO,這是一家全球培訓、指導和咨詢公司,其重點是企業開發。他經常在 IBM developerWorks 上發表文章,并編寫了 10 多篇 developerWorks 教程,內容從 EJB 技術到 Web 服務到 XDoclet。 Rick 不久前與別人共同開辦了另一家名為 ArcMind 的公司,它專門研究 agile 方法,還從事 Struts/JavaServer Faces 開發、咨詢和指導。
在為 eBlox 工作時,Rick 和 eBlox 小組遠在 1.0 版本之前就已使用 Struts 為電子商務站點構建了兩個框架和一個 ASP (應用程序服務提供者)。這個框架目前正在為 2000 多個在線商店店面提供支持。
Rick 最近完成了一本名為 Professional Jakarta Struts 的書。在周游全國對 J2EE 和 Struts 項目提供咨詢,或者在大會上發表關于 J2EE 和極端編程 (extreme programing)的講演之余,Rick 喜歡在通宵咖啡店喝咖啡,寫一些有關 Struts、J2EE 和其他內容的文章,并以第三人稱描寫他自己。
在創建自定義標簽之前,需要創建一個 標簽處理程序。標簽處理程序是一個執行自定義標簽操作的 Java 對象。在使用自定義標簽時,要導入一個 標簽庫 —— 即一組標簽/標簽處理程序對。通過在 Web 部署描述符中聲明庫導入它,然后用指令 taglib
將它導入 JSP 頁。
如果 JSP 容器在轉換時遇到了自定義標簽,那么它就檢查 標簽庫描述符(tag library descriptor) (TLD) 文件以查詢相應的標簽處理程序。TLD 文件對于自定義標簽處理程序,就像 Web 部署描述符對于 servlet 一樣。
在運行時,JSP 頁生成的 servlet 得到對應于這一頁面所使用的標簽的標簽處理程序的一個實例。生成的 servlet 用傳遞給它的屬性初始化標簽處理程序。
標簽處理程序實現了 生存周期 方法。生成的 servlet 用這些方法通知標簽處理程序應當啟動、停止或者重復自定義標簽操作。生成的 servlet 調用這些生存周期方法執行標簽的功能。
可以定義兩種類型的標簽:
javax.servlet.jsp.tagext.Tag
javax.servlet.jsp.tagext.BodyTag
對 正文 進行操作 —— 即對在開始和結束標簽之間的內容進行操作的 —— 標簽必須實現 BodyTag
接口。在這個教程中,我們將稱這些標簽為 正文標簽。我們將不對其正文操作的標簽稱為 簡單標簽。簡單標簽可以實現 Tag
接口,盡管不要求它們這樣做。要記住不對其正文操作的標簽仍然 有 正文,只不過,它的標簽處理程序不能讀取這個正文。
上一頁 下一頁
Struts 框架帶有幾個自定義標簽庫(有關 Struts 的更多信息的鏈接請參閱 參考資料 )。這些庫中的一個標簽可以創建一個支持改寫 URL 的鏈接并用 jsessionid
對改寫的連接編碼。
不過有一個問題:如果希望傳遞一組請求參數(如查詢字符串),也許必須為此創建一個 Java scriptlet。真是亂!下面的清單 (search_results.jap) 展示了一個 JSP 頁,它被迫加入了這樣一個 scriptlet。
<%@ taglib uri="struts-html" prefix="html" %> <jsp:useBean class="java.util.HashMap" id="deleteParams" /> <% deleteParams.put("id", cd.getId()); deleteParams.put("method","delete"); %> <!-- Pass the map named deleteParams to html:link to generate the request parameters--> <html:link action="/deleteCD" name="deleteParams">delete </html:link> </font></td>
search_results.jsp 創建一個 hashmap 并向這個 map 傳遞兩個屬性。在下面幾小節,我們將創建一個不用 Java 代碼完成這項工作的自定義標簽。我們的標簽將定義如下的一個 hashmap:
<map:mapDefine id="deleteParams"> <map:mapEntry id="id" name="cd" property="id"/> <map:mapEntry id="method" value="delete"/> </map:mapDefine> <!-- Pass the map named deleteParams to html:link to generate the request parameters--> <html:link action="/deleteCD" name="deleteParams">delete </html:link> </font></td>
這將使我們可以容易地創建小型 map。
這個例子將展示幾個關鍵概念,包括使用嵌套標簽和定義 scriplet 變量。首先我將解釋這個標簽是如何工作的。然后在以后的幾節中建立這些概念,并介紹如何編寫這個標簽的不同形式,使它們處理其正文并控制執行流程。
讓我們創建一個定義一個 HashMap
scriptlet 變量的標簽。為此,需要實現標簽處理程序接口 (javax.servlet.jsp.tagext.Tag)。因此,我們要創建的第一個標簽將是一個簡單標簽。
這個標簽將實例化一個 map。使用這個標簽的開發人員可以指定要實例化的 map 的類型 —— HashMap
、TreeMap
、FastHashMap
或者 FastTreeMap
。FastHashMap
和 FastTreeMap
來自 Jakarta Commons Collection library (有關鏈接請參閱 參考資料)。開發人員還可以指定標簽所在的范圍 —— 頁、請求、會話還是應用程序范圍。
要構建這個簡單標簽,我們需要完成以下步驟:
- 創建實現了
Tag
接口(準確地說是 javax.servlet.jsp.tagext.Tag)的標簽處理程序類。
- 創建一個 TLD 文件。
- 在標簽處理程序 Java 類中創建屬性。
- 在 TLD 文件中定義與標簽處理程序 Java 類中定義的屬性對應的屬性。
- 在 TLD 文件中聲明 scriptlet 變量。
- 實現
doStartTag()
方法。在標簽處理程序類中,根據屬性將值設置到 scriptlet 變量中。
如果您像我一樣,可能會提前閱讀書的結尾,所以請查看 附錄 中標簽處理程序類的完整列表以了解這個過程是如何結束的。
在下面幾小節中,我們將分析 MapDefineTag
的實現,并分析如何到達這一步。
第 1 步:創建一個實現了 Tag 接口的標簽處理程序 |
第 3 頁(共9 頁) |
為了編寫標簽處理程序,必須實現 Tag
接口。如前所述,這個接口用于不操縱其標簽正文的簡單標簽處理程序。就像 J2EE API 文檔 (有關鏈接請參閱 參考資料)所說的:Tag
接口定義了標簽處理程序和 JSP 頁實現類之間的基本協議。它定義了在標簽開始和結束時調用的生存周期和方法。
標簽處理程序接口有以下方法:
方法 |
作用 |
int doStartTag() throws JspException |
處理開始標簽 |
int doEndTag() throws JspException |
處理結束標簽 |
Tag getParent() /void setParent(Tag t) |
獲得/設置標簽的父標簽 |
void setPageContext(PageContext pc) |
pageContext 屬性的 setter 方法 |
void release() |
釋放獲得的所有資源 |
TagSupport
現在,不必直接實現 Tag
接口,相反,用 map 定義的(map-defining)標簽將繼承 TagSupport 類。這個類以有意義的默認方法實現 Tag
接口,因而使開發自定義標簽更容易 (有關 TagSupport 的 API 文檔的鏈接請參閱 參考資料)。 例如,TagSupport 類定義了 get
/setParent()
和 setPageContext()
,這與所有標簽處理程序幾乎相同。 get
/setParent()
方法允許標簽嵌套。TagSupport 類還定義了一個可以被子類使用的 pageContext
實例變量 (protected PageContext pageContext
),這個變量是由 setPageContext()
方法設置的。
在默認情況下,TagSupport 實現了 doStartTag()
以使它返回 SKIP_BODY
常量,表示將不對標簽正文進行判斷。 此外,在默認情況下,doEndTag()
方法返回 EVAL_PAGE
,它表示 JSP 運行時引擎應當對頁面的其余部分進行判斷。 最后,TagSupport 實現了 release()
,它設置 pageContext
及其父元素為 null
。
TagSupport 類還實現了 IterationTag
接口和 doAfterBody()
,這樣它就返回 SKIP_BODY
。 在后面討論進行迭代的標簽時我將對此加以更詳細的解釋(請參閱 用自定義標簽控制流程)。
好了,現在讓我們通過繼承 TagSupport 來實現 Tag
接口:
... import javax.servlet.jsp.tagext.TagSupport; ... public class MapDefineTag extends TagSupport { ...
我們已經定義了標簽處理程序,現在需要增加從處理程序到 TLD 文件中的標簽的映射。我們將在下一小節中對此進行處理。然后,將完成 MapDefineTag
中剩余的代碼。
第 2 步:創建一個 TLD 文件 |
第 4 頁(共9 頁) |
TLD 文件對自定義標簽處理程序的作用就像 Web 部署描述符對 servlet 的作用。 TLD 文件列出了從標簽名到標簽處理程序的映射。 這個文件中的大多數數據都是在 JSP 頁轉換時使用的。 TLD 文件通常保存在 Web 應用程序的 WEB-INF
目錄,并在 web.xml 文件中聲明。它們一般用 .tld
擴展名結束。
TLD 文件有一個 導言(preamble),在這里標識 JSP 技術的版本和使用的標簽庫。這個導言通常看起來像這樣:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN" "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd"> <taglib> <tlib-version>1.0</tlib-version> <jsp-version>1.2</jsp-version> <short-name>map</short-name>
讓我們更詳細地分析一下這些標簽:
- TLD 文件的根元素是
taglib
。taglib
描述了一個 標簽庫 —— 即一組標簽/標簽處理程序對。
- 因為我們使用的是 JSP 版本 1.2,所以在這個例子中需要
tlib-version
和 short-name
元素。
tlib-version
元素對應于標簽庫版本。
jsp-version
對應于標簽庫所依賴的 JSP 技術的版本。
short-name
元素定義了 IDE 和其他開發工具可以使用的標簽庫的簡單名。
taglib
元素包含許多 tag
元素,標簽庫中每一個標簽有一個 tag
元素。
因為我們剛創建了自己的類,所以我們將繼續往下進行,在 TLD 文件中聲明這個類,如下所示:
<taglib>
...
<tag>
<name>mapDefine</name>
<tag-class>trivera.tags.map.MapDefineTag</tag-class>
<body-content>JSP</body-content>
...
tag
元素用于將自定義標簽映射到它們的自定義標簽處理程序。上述清單中的 tag
元素將自定義標簽 mapDefine
映射到處理程序 trivera.tags.map.MapDefineTag
。 因此,不論在 mapDefine
上運行的是什么轉換引擎,都會調用 trivera.tags.map.MapDefineTag
。
已經在 TLD 中定義了標簽,接下來要在標簽處理程序類中定義這個標簽的一些屬性了。
第 3 步:在標簽處理程序 Java 類中創建屬性 |
第 5 頁(共9 頁) |
上一頁 下一頁
我們希望為 mapDefine
標簽指定三個屬性,如下所示:
屬性說明 |
id |
新 scriptlet 變量的名字。 |
scope |
新 scriptlet 變量所在的范圍。 |
type |
新 scriptlet 變量的類型 (HashMap 、FastHashMap 、TreeMap 或者 FastTreeMap )。 如果 type 設置為 hash ,那么就會創建一個 HashMap 。如果 type 設置為 fasthash ,那么將創建 FastHashMap 。 |
在 JSP 頁中使用這個標簽時,它看起來將像下面這樣:
<map:mapDefine id="editParams" scope="session" type="hash">
...
</map:mapDefine>
這個標簽將在會話范圍內創建一個名為 editParams
的 HashMap
。
為了在標簽處理程序中創建屬性,需要定義相應的 JavaBean 屬性。 因此,每一個屬性在標簽處理程序中都有對應的 setter 方法,如下所示:
public class MapDefineTag extends TagSupport {
...
private String type = FASTTREE;
private String id;
private String scope;
public void setType(String string)
{ type = string; }
public void setId(String string)
{ id = string; }
public void setScope(String string)
{ scope = string; }
轉換引擎將用硬編碼的配置數據或者運行時表達式設置這個標簽的屬性。 我們將在 第 4 步:在 TLD 文件中定義屬性 中對此做更詳細的討論。
在 第 5 步:實現 doStartTag() 方法 中,我們將在標簽處理程序的 doStartTag()
方法中使用這些屬性。
第 4 步:在 TLD 文件中定義屬性 |
第 6 頁(共9 頁) |
就 像上一小節中所做的那樣,通過聲明 JavaBean 屬性定義自定義屬性,然后在 TLD 文件中聲明這些屬性。 每一個 JavaBean 屬性都必須與相應的自定義標簽屬性相匹配。 在 TLD 中定義的屬性必須匹配 JavaBean 屬性,不過卻可以有與標簽屬性不匹配的 JavaBean 屬性。
下面是 MapDefineTag
的屬性聲明:
<tag> <name>mapDefine</name> <tag-class>trivera.tags.map.MapDefineTag</tag-class> <body-content>JSP</body-content> ... <attribute> <name>id</name> <required>true</required> <rtexprvalue>false</rtexprvalue> <description>The id attribute</description> </attribute> <attribute> <name>scope</name> <required>false</required> <rtexprvalue>false</rtexprvalue> <description>The scope attribute</description> </attribute> <attribute> <name>type</name> <required>false</required> <rtexprvalue>false</rtexprvalue> <description> Specifies the type of map valid values are fasttree, fasthash, hash, tree </description> </attribute> </tag>
name
元素指定屬性的名字。required
元素指定屬性是否是必需的(默認值是 false
)。rtexprvalue
元素表明屬性是硬編碼了轉換時的值還是允許使用運行時 scriptlet 表達式。
記住,MapDefineTag
類必須為前面描述的每一個屬性定義一個 JavaBean 屬性,我們在 第 3 步:在標簽處理程序 Java 類中創建屬性 中完成這個任務。
第 5 步:實現 doStartTag() 方法 |
第 7 頁(共9 頁) |
標簽開始時調用 doStartTag()
方法 —— 從開發人員的角度看,這是當引擎遇到 <map:mapDefine>
時發生的。如果 doStartTag()
返回 SKIP_BODY
,那么將不處理標簽正文。 如果它返回一個 EVAL_BODY_INCLUDE
,那么將處理正文。
MapDefine
類的 doStartTag()
方法完成以下工作:
- 根據
type
屬性確定要創建的 map 的屬性。
- 根據
scope
屬性確定新的 map 對象放在什么范圍內。
- 根據
id
屬性確定新 map 對象要放入的范圍的名字。
讓我們更詳細地分析這個過程。MapDefine
類檢查 type
屬性是設置為 FASTTREE
、HASH
、TREE
還是 FASTHASH
。然后創建相應的 map,如下所示:
/* String constants for the different types of maps we support */ public static final String FASTHASH = "FASTHASH"; public static final String FASTTREE = "FASTTREE"; public static final String HASH = "HASH"; public static final String TREE = "TREE"; /** The map we are going to create */ private Map map = null; /** The member variable that holds the type attribute */ private String type = FASTTREE; ... public int doStartTag() throws JspException { /** Based on the type attribute, determines which type of Map to create */ if (type.equalsIgnoreCase(FASTTREE)) { map = new FastTreeMap(); } else if (type.equalsIgnoreCase(HASH)) { map = new HashMap(); } else if (type.equalsIgnoreCase(TREE)) { map = new TreeMap(); } else if (type.equalsIgnoreCase(FASTHASH)) { map = new FastHashMap(); }
然后,用 id
和 scope
屬性將 hashmap 以一個給定的名字設置到一個給定范圍中:
private String id; private String scope; public int doStartTag() throws JspException { ... if (scope == null){ pageContext.setAttribute(id, map); }else if("page".equalsIgnoreCase(scope)){ pageContext.setAttribute(id, map); }else if("request".equalsIgnoreCase(scope)){ pageContext.getRequest().setAttribute(id, map); }else if("session".equalsIgnoreCase(scope)){ pageContext.getSession().setAttribute(id, map); }else if("application".equalsIgnoreCase(scope)){ pageContext.getServletContext().setAttribute(id, map); } return EVAL_BODY_INCLUDE; }
如果范圍屬性是 null
,那么 map 將放入頁范圍。否則,參數將放入通過 scope
屬性傳遞的范圍名所對應的范圍中。
到目前為止,我們已經有一個非常簡單的標簽,它有三個屬性:id
、scope
和 type
。 我們將用給定的名字將 map 放到一個范圍中。但是,我們還有一件事沒做,就是聲明 scriptlet 變量。 為了做到這一點,需要向 TLD 再添加一項,這就是我們在下一小節中所要做的事。