JSP 早期大量 Scriptlet (<% ... %>) 導致頁面邏輯與展示耦合,難以維護。自定義標籤 (Custom Tag) 將 Java 邏輯封裝為類 XML 標籤,實現:
| 術語 | 說明 |
|---|---|
| Tag (標籤) | XML 風格元素,如 <my:pagination total="100" />,大小寫敏感 |
| Tag Library (標籤庫) | 功能相關標籤的集合,打包為 JAR 發布 |
| TLD (Tag Library Descriptor) | XML 配置文件,定義標籤名、處理類、屬性、Body 類型等元數據 |
| Tag Handler (標籤處理類) | 實現標籤邏輯的 Java 類,繼承 TagSupport 或實現 SimpleTag |
| PageContext | JSP 隱含對象,提供對 request/session/application/out 等作用域訪問 |
<%@ taglib prefix="my" uri="/WEB-INF/tlds/mytags.tld" %>
<!-- 或使用 urn 風格 uri(需 web.xml 映射) -->
<%@ taglib prefix="my" uri="https://mycompany.com/tags" %>
<!-- 空標籤 -->
<my:copyright year="2024" />
<!-- 帶 Body 標籤 -->
<my:panel title="用戶列表">
<table>...</table>
</my:panel>
<%@ taglib ...%>,記錄 prefix → uri 映射<my:panel>,根據 prefix 找到 uriweb.xml 中查找 <taglib> 元素匹配 uri,獲取 TLD 文件路徑panel 找到 <tag-class>setPageContext()、setParent() 注入上下文doStartTag() → 處理 Body → 調用 doEndTag()package com.example.tags;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.TagSupport;
import java.io.IOException;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
public class CurrentTimeTag extends TagSupport {
private String pattern = "yyyy-MM-dd HH:mm:ss"; // 默認格式
// 屬性 Setter(容器自動調用)
public void setPattern(String pattern) {
this.pattern = pattern;
}
@Override
public int doStartTag() throws JspException {
try {
JspWriter out = pageContext.getOut();
String formatted = LocalDateTime.now().format(DateTimeFormatter.ofPattern(pattern));
out.print(formatted);
} catch (IOException e) {
throw new JspException("寫入響應失敗", e);
}
return SKIP_BODY; // 無 Body 內容
}
@Override
public int doEndTag() throws JspException {
return EVAL_PAGE; // 繼續執行頁面其餘部分
}
}
/WEB-INF/tlds/mytags.tld)<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="https://java.sun.com/xml/ns/javaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
version="2.1">
<tlib-version>1.0</tlib-version>
<short-name>my</short-name>
<uri>https://mycompany.com/tags</uri>
<tag>
<name>currentTime</name>
<tag-class>com.example.tags.CurrentTimeTag</tag-class>
<body-content>empty</body-content>
<attribute>
<name>pattern</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue> <!-- 支援 <%= ... %> 動態表達式 -->
<type>java.lang.String</type>
<description>日期時間格式模式</description>
</attribute>
</tag>
</taglib>
<taglib>
<taglib-uri>https://mycompany.com/tags</taglib-uri>
<taglib-location>/WEB-INF/tlds/mytags.tld</taglib-location>
</taglib>
| Body Content 類型 | TLD 聲明 | 適用場景 | 處理方式 |
|---|---|---|---|
| empty | <body-content>empty</body-content> | 無體標籤 <tag/> | doStartTag() 返回 SKIP_BODY |
| scriptless (JSP 2.0+) | <body-content>scriptless</body-content> | Body 含 EL/標籤/模板文本,無 Scriptlet | 推薦使用 SimpleTagSupport,getJspBody().invoke(out) |
| tagdependent | <body-content>tagdependent</body-content> | Body 為純文本/模板,由標籤自行解析 (如 SQL、代碼生成) | doStartTag() 返回 EVAL_BODY_INCLUDE,在 doEndTag() 讀取 bodyContent.getString() |
| JSP (舊規範) | <body-content>JSP</body-content> | Body 可含 Scriptlet(不推薦) | 同 scriptless |
更簡潔、無 doStartTag/doEndTag 狀態機、自然支援 Body 處理、支援標籤屬性依賴注入風格。
public class PanelTag extends SimpleTagSupport {
private String title;
private String cssClass = "panel panel-default";
public void setTitle(String title) { this.title = title; }
public void setCssClass(String cssClass) { this.cssClass = cssClass; }
@Override
public void doTag() throws JspException, IOException {
JspWriter out = getJspContext().getOut();
// 渲染面板頭部
out.write(String.format("<div class=\"%s\"><div class=\"panel-heading\">%s</div><div class=\"panel-body\">",
cssClass, title));
// 渲染 Body 內容(支援 EL、其他標籤)
getJspBody().invoke(out);
// 渲染面板尾部
out.write("</div></div>");
}
}
TLD 中聲明:<body-content>scriptless</body-content>
rtexprvalue="true":屬性值可為運行時表達式 (<%= user.name %>、${user.name})type:指定屬性 Java 類型,容器自動類型轉換required="true":強制屬性,缺失編譯報錯deferredValue / deferredMethod:支援 EL 延遲求值 (JSP 2.1+)將常用片段封裝為 .tag 文件,放在 /WEB-INF/tags/ 目錄:
<!-- /WEB-INF/tags/copyright.tag -->
<%@ tag description="版權標籤" pageEncoding="UTF-8" %>
<%@ attribute name="company" required="true" type="java.lang.String" %>
<%@ attribute name="year" required="false" type="java.lang.Integer" %>
<div class="copyright">
© ${year != null ? year : (new java.util.Date()).year + 1900} ${company}. All rights reserved.
</div>
使用:<%@ taglib prefix="t" tagdir="/WEB-INF/tags" %> <t:copyright company="MyCorp" />
在 TagHandler 中通過 pageContext.getRequest().getLocale() 獲取用戶 Locale,配合 ResourceBundle 輸出多語言:
public class I18nMessageTag extends SimpleTagSupport {
private String key;
public void setKey(String key) { this.key = key; }
@Override
public void doTag() throws JspException, IOException {
Locale locale = getJspContext().getRequest().getLocale();
ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
String msg = bundle.getString(key);
getJspContext().getOut().write(msg);
}
}
| 場景 | 建議 |
|---|---|
| 大量 HTML 輸出 | 使用 Tag File 或模板引擎,避免在 Java 中拼接字符串 |
| 需要訪問請求參數 | 優先用 pageContext.getRequest().getParameter(),而非標籤屬性傳遞 |
| 標籤巢狀 | 用 findAncestorWithClass(this, ParentTag.class) 獲取父標籤上下文 |
| 性能敏感 | 標籤類做成無狀態、線程安全;避免在 doTag 中創建重對象 |
| 異常處理 | 拋出 JspException 讓容器統一處理,不要吞噬異常 |
自定義標籤是 JSP 實現「模板化、組件化、可復用」的核心機制。掌握 TagSupport (經典) 與 SimpleTagSupport (現代) 兩種開發模式、TLD 元數據定義、Body 內容類型 選擇、Tag File 快速開發,即可構建企業級標籤庫。現代項目中,配合前端模板引擎或 React/Vue 組件化,JSP 標籤多用於服務端片段復用與舊系統改造。