了解生命週期與架構


看起來 Simple Tag 的開發似乎不會太難,主要就是繼承 SimpleTagSupport 類別、重新定義 doTag() 方法、定義 TLD 檔案以及使用 taglib 指示元素。不過實際上還有很多東西需要解釋。

SimpleTagSupport 實際上實作了 javax.servlet.jsp.tagext.SimpleTag 介面,而 SimpleTag 介面繼承了 javax.servlet.jsp.tagext.JspTag 介面。

了解生命週期與架構

所有的 JSP 自訂 Tag 都實作了 JspTag 介面,JspTag 介面只是個標示介面,本身沒有定義任何的方法。 SimpleTag 介面繼承了 JspTag,定義了 Simple Tag 開發時所需的基本行為。

開發 Simple Tag 標籤處理器時必須實作 SimpleTag 介面,不過通常繼承 SimpleTagSupport 類別,因為該類別實作了 SimpleTag 介面,並對所有方法作了基本實作,所以只需要在繼承 SimpleTagSupport 之後,重新定義感興趣的方法即可,通常就是重新定義 doTag() 方法。

當 JSP 網頁中包括 Simple Tag 自訂標籤,若使用者請求該網頁,在遇到自訂標籤時,會按照以下的網頁來進行處理:

  1. 建立自訂標籤處理器實例。
  2. 呼叫標籤處理器的 setJspContext() 方法設定 PageContext 實例。
  3. 如果是巢狀標籤中的內層標籤,則還會呼叫標籤處理器的 setParent() 方法,並傳入外層標籤處理器的實例。
  4. 設定標籤處理器屬性(例如這邊是呼叫 IfTagsetTest() 方法來設定)。
  5. 呼叫標籤處理器的 setJspBody() 方法設定 JspFragment 實例。
  6. 呼叫標籤處理器的 doTag() 方法。
  7. 銷毀標籤處理器實例。

每一次的請求都會建立新的標籤處理器實例,而在執行 doTag() 過後就銷毀實例,所以 Simple Tag的 實作中,建議不要有一些耗資源的動作,像是龐大的物件、連線的取得等,正如 Simple Tag 名稱所表示的,這並不僅代表它實作上比較簡單(相較於 Tag 的實作方式),也代表著它最好用來作一些簡單的事務。

同樣的道理,由於 Tag File 轉譯後會成為繼承 SimpleTagSupport 的類別,所以在 Tag File 中,也建議不要有一些耗資源的動作。

由於標籤處理器中被設定了 PageContext,所以可以用它來取得JSP頁面的所有物件,進行所有在 JSP 頁面 Scriptlet 中可以執行的動作,所以之後就可以用自訂標籤來取代 JSP 頁面上的 Scriptlet。

JspFragment 就如其名稱所示,是個 JSP 頁面中的片段內容。在 JSP 中使用自訂標籤時若包括本體,將會轉譯為一個 JspFragment 實作類別,而本體內容將會在 invoke() 方法進行處理。以 Tomcat 為例,<f:if> 本體內容將轉譯為以下的 JspFragment 實作類別(一個內部類別):

private class Helper
    extends org.apache.jasper.runtime.JspFragmentHelper {
    // 略...
    public boolean invoke0( JspWriter out ) 
      throws Throwable {
      out.write("\n");
      out.write("            你的秘密資料在此!\n");
      out.write("        ");
      return false;
    }
    public void invoke( java.io.Writer writer )
        throws JspException {
        JspWriter out = null;
        if( writer != null ) {
            out = this.jspContext.pushBody(writer);
        } else {
            out = this.jspContext.getOut();
        }
        try {
            // 略...
              invoke0( out );
            // 略...
        }
        catch( Throwable e ) {
            if (e instanceof SkipPageException)
                throw (SkipPageException) e;
            throw new JspException( e );
        }
        finally {
            if( writer != null ) {
                this.jspContext.popBody();
            }
        }
    }
}

所以在 doTag() 方法中使用 getJspBody() 取得 JspFragment 實例,且呼叫其 invoke() 方法時傳入 null,這表示將使用 PageContext 取得預設的 JspWriter 物件來作輸出回應(而並非不作回應)。接著進行本體內容的輸出,如果本體內容中包括 EL 或內層標籤,則會先作處理(在 <body-content> 設定為 scriptless 的情況下)。在上面的簡單範例中,只是將 <f:if> 本體的 JSP 片段直接輸出(也就是 invoke0() 的執行內容)。

如果呼叫 JspFragmentinvoke() 時傳入了一個 Writer 實例,則表示要將本體內容的執行結果,以所設定的 Writer 實例作輸出,這個之後會再進行討論。

如果執行 doTag() 的過程在某些條件下,必須中斷接下來頁面的處理或輸出,則可以丟出 javax.servlet.jsp.SkipPageException,這個例外物件會在 JSP 轉譯後的 _jspService() 中如下處理:

...
try {
    // 丟出 SkipPageException 例外的地方
    // 其他 JSP 頁面片段
    // 略...
} catch (Throwable t) {
    if (!(t instanceof SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
            try { out.clearBuffer(); } catch (java.io.IOException e) {}
            if (_jspx_page_context != null)
                _jspx_page_context.handlePageException(t);
        }
    }
}
...

簡單地說,在 catch 中捕捉到例外時,若是 SkipPageException 實例,什麼事都不作!在 doTag() 中若只是想中斷接下來的頁面處理,則可以丟出 SkipPageException

若是丟出其他類型的例外,則在 PageContexthandlePageException() 中會看看有無設置錯誤處理相關機制,並嘗試進行頁面轉發或包含的動作,否則就包裝為 ServletException 並丟給容器作預設處理,這時就會看到 HTTP Status 500 的網頁出現了。