重複執行標籤本體


如果想繼承 TagSupport實作〈處理標籤屬性與本體〉的 <f:forEach> 標籤,可以根據所給定的 Collection 物件個數來決定重複執行標籤本體的次數,那麼你該在哪個方法中實作? doStartTag()?根據〈了解生命週期與架構〉中的流程圖,doStartTag() 只會執行一次!doEndTag()?這時本體內容處理已經結束了!

根據〈了解生命週期與架構〉,在 doAfterBody() 方法執行過後,如果傳回 EVAL_BODY_AGAIN, 則會再重複執行一次本體內容,而後再度呼叫 doAfterBody() 方法,除非在 doAfterBody() 中傳回 SKIP_BODY 才會呼叫 doEndTag()。顯然地,doAfterBody() 是可以實作 <f:forEach> 標籤重複處理特性的地方。

不過這邊有點小陷阱!當 doStartTag() 傳回 EVAL_BODY_INCLUDE 後,會先執行本體內容後再呼叫 doAfterBody() 方法, 也就是說,實際上本體已經執行過一遍了!所以正確的作法應該是,doStartTag()doAfterBody() 都要實作,doStartTag() 實作第一次的處理,doAfterBody() 實作後續的重複處理。例如:

package cc.openhome;

import java.util.Collection;
import java.util.Iterator;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;

public class ForEachTag extends TagSupport {
    private String var;
    private Iterator<Object> iterator;

    @Override
    public int doStartTag() throws JspException {
        if(iterator.hasNext()) {
            this.pageContext.setAttribute(var, iterator.next());
            return EVAL_BODY_INCLUDE;
        }
        return SKIP_BODY;
    }

    @Override
    public int doAfterBody() throws JspException {
        if(iterator.hasNext()) {
            this.pageContext.setAttribute(var, iterator.next());
            return EVAL_BODY_AGAIN;
        }
        this.pageContext.removeAttribute(var);
        return SKIP_BODY;
    }

    public void setVar(String var) {
        this.var = var;
    }

    public void setItems(Collection<Object> items) {
        this.iterator = items.iterator();
    }
} 

<f:forEach> 的標籤處理器實作中,必須先為第一次的本體執行作屬性設定,如此傳回 EVAL_BODY_INCLUDE 後第一次執行本體內容時,才可以有 var 所設定的 屬性名稱可以存取。接著呼叫 doAfterBody() 方法,其中再為第二次之後的本體處理作屬性設定,如果需要再執行一次本體,則傳回 EVAL_BODY_AGAIN,再度執行完本體後又會呼叫 doAfterBody() 方法,如果不想執行本體了,則傳回 SKIP_PAGE,則流程會來到 doEndTag() 的執行(在 SimpleTagdoTag() 中直接使用迴圈語法,顯然直覺多了)。

接著同樣在定義TLD檔案中定義標籤:

f.tld

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.1" xmlns="http://java.sun.com/xml/ns/j2ee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
   web-jsptaglibrary_2_1.xsd">
    <tlib-version>1.0</tlib-version>
    <short-name>f</short-name>
    <uri>https://openhome.cc/jstl/fake</uri>
    // 略...
    <tag>
        <name>forEach</name>
        <tag-class>cc.openhome.ForEachTag</tag-class>
        <body-content>JSP</body-content>
        <attribute>
            <name>var</name>
            <required>true</required>
            <type>java.lang.String</type>
        </attribute>
        <attribute>
            <name>items</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
            <type>java.util.Collection</type>
        </attribute>
    </tag>
</taglib> 

實際上在 Tomcat 中,如果觀看 JSP 轉譯後的 Servlet 原始碼會發現,只要 doAfterBody() 的傳回值不是 EVAL_BODY_AGAIN ,就不會再度執行本體內容並呼叫 doAfterBody() 方法。