1. 程式人生 > 其它 >JSP第六篇【自定義標籤之傳統標籤】

JSP第六篇【自定義標籤之傳統標籤】

為什麼要使用自定義標籤?

JSTL標籤庫只提供了簡單的輸出等功能,沒有實現任何的HTML程式碼封裝,並且某些複雜型別轉換,或者邏輯處理的時候,JSTL標籤庫完成不了,需要自定義標籤!

編寫自定義標籤的步驟:

  1. 編寫一個實現Tag介面的Java類【標籤處理器類】
  2. 在WEB-INF目錄下建立tld(Tag Library Descriptor)檔案,在tld檔案中對標籤處理類(實現Tag介面的Java類)進行描述

快速入門

  • 目標:使用標籤輸出客戶機的IP地址
  • 按照步驟來:首先編寫一個實現Tag介面的Java類
    public class showIp implements Tag {
    
        @Override
        public void setPageContext(PageContext pageContext) {
    
        }
    
        @Override
        public void setParent(Tag tag) {
    
        }
    
        @Override
        public Tag getParent() {
            return null;
        }
    
        @Override
        public int doStartTag() throws JspException {
            return 0;
        }
    
        @Override
        public int doEndTag() throws JspException {
            return 0;
        }
    
        @Override
        public void release() {
    
        }
    }
  • 既然要獲取到客戶機的IP地址,那麼request物件是必不可少的。現在問題來了,在Tag重寫的方法好像不能直接獲取到request物件啊
  • 經過我一番仔細的觀察,發現了下面這個方法:
        @Override
        public void setPageContext(PageContext pageContext) {
    
        }
  • 既然能獲取到pageContext物件,那麼其他8大內建物件還不是隨隨便便?於是乎,我就定義一個成員變數pageContext,在setPageContext()方法中傳遞過來的pageContext賦值給我定義的成員變數即可
        private PageContext pageContext = null;

        @Override
        public void setPageContext(PageContext pageContext) {
            this.pageContext = pageContext;
        }
  • 好的,看回我們的需求:使用標籤輸出客戶機的IP地址。在上面剩餘5個方法中,最有可能就是在doStartTag()方法中編寫程式碼
    @Override
    public int doStartTag() throws JspException {

        //獲取到request物件
        HttpServletRequest httpServletRequest = (HttpServletRequest) pageContext.getRequest();

        //獲取到客戶機的ip地址
        String ip = httpServletRequest.getRemoteAddr();
        
        //獲取輸出到瀏覽器的物件
        JspWriter jspWriter = pageContext.getOut();
        
        //下面的異常只能捕獲,因為子類的異常不能比父類多
        try {
            jspWriter.write(ip);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return 0;
    }
  • 接著,編寫tld檔案,描述實現Tag介面的Java類【標籤處理類】
    <?xml version="1.0" encoding="ISO-8859-1"?>
    
    <taglib xmlns="http://java.sun.com/xml/ns/javaee"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
            version="2.1">
    
        <tlib-version>1.0</tlib-version>
        <short-name>zhongfucheng</short-name>
        <uri>/zhongfucheng</uri>
    
        <!-- Invoke 'Generate' action to add tags or functions -->
        <tag>
            <name>viewIp</name>
            <tag-class>tag.showIp</tag-class>
            <body-content>empty</body-content>
        </tag>
    
    
    </taglib>
  • 下面我們來測試一下看能不能用

標籤處理類詳細說明

看完上面的程式,大部分人都是懵逼的。因為還不知道它具體是怎麼用的,呼叫順序是什麼

  • 首先我們來看一下Tag介面的原始碼
    public interface Tag extends JspTag {
        int SKIP_BODY = 0;
        int EVAL_BODY_INCLUDE = 1;
        int SKIP_PAGE = 5;
        int EVAL_PAGE = 6;
    
        void setPageContext(PageContext var1);
    
        void setParent(Tag var1);
    
        Tag getParent();
    
        int doStartTag() throws JspException;
    
        int doEndTag() throws JspException;
    
        void release();
    }
  • 上面程式的執行流程:
    • JSP引擎遇到自定義標籤,首先建立標籤處理器類的例項物件
    • JSP引擎例項化完標籤處理器類後,呼叫setPageContext()方法,將pageContext物件傳遞給標籤處理器類,使得標籤處理器類可以通過pageContext物件與JSP頁面進行通訊!
    • setPageContext()方法執行完後,呼叫setParent()方法,將當前標籤的父標籤傳遞給當前處理器類,如果當前標籤沒有父標籤,則傳入null
    • 當WEB容器執行到自定義標籤的開始標記時,呼叫doStartTag()方法。
    • 當WEB容器執行到自定義標籤的結束標記時,呼叫doEndTag()方法。
    • 一般來說,當WEB容器執行完自定義標籤後,標籤處理器類會駐留在記憶體中,直至停止WEB應用時,WEB容器才會呼叫release()方法

  • 我們現在已經清楚了方法的執行順序了,可Tag介面的原始碼還有4個變數阿,它們是用來做什麼的呢?我們在編寫JSP頁面時,經常需要在頁面中引入一些邏輯,例如:
    • 控制JSP頁面某一部分(標籤體)是否執行
    • 控制整個JSP頁面是否執行
    • 控制JSP頁面內容重複執行
    • 修改JSP頁面內容輸出
  • 再看回4個變數的名字,我們可以發現,這4個變數就是用來做邏輯判斷的
  • 我們來測試一下吧,在doEndTag()方法中,返回的是SKIP_PAGE變數,看下會怎麼樣
    @Override
    public int doEndTag() throws JspException {
        return SKIP_PAGE;
    }
  • 我們再來看一看效果:
  • 好像是沒什麼區別!我們再檢視一下原始碼,發現執行完標籤後,後面的程式碼全都沒有執行!
  • doStartTag()方法使用的是SKIP_BODY和EVAL_BODY_INCLUDE這兩個變數,判斷是否執行標籤體的內容。
  • doEndTag()方法使用的是SKIP_PAGE和EVAL_PAGE這兩個變數,判斷是否執行剩下頁面的內容
  • 控制JSP頁面內容重複執行和修改JSP頁面內容輸出後面會有!

tld檔案詳細說明

  • 首先我們來看一下tld檔案當前用到的內容吧
    <tlib-version>1.0</tlib-version>
    <short-name>myshortname</short-name>
    <uri>http://mycompany.com</uri>
    
    <tag>
        <name></name>
        <tag-class></tag-class>
        <body-content></body-content>
    </tag>
  • 我們一個一個來看:
    • shortname推薦使用prefix
    • uri就是引入這個標籤庫使用的uri
    • name為標籤名
    • tagclass為實現類
    • bodycontent為標籤體的限制,它有4個值: EMPTY【不允許有標籤體】,JSP【允許有JSP程式碼】 ,scriptless【不允許有指令碼程式碼(也就是<%%>),允許有EL表示式,文字,JSP行為】 , tagdepentend【標籤體內的JSP程式碼不會被解析,直接輸出文字】

TagSupport類

大部分時候我們都不需要實現Tag介面來編寫自定義標籤,TagSupport是Tag的一個模板類,實現了pageContext,parent的getter、setter方法以及一些其他的功能。我們要做的就是重寫doStartTag()和doEndTag()方法

  • 下面我們就來簡單使用一下吧:
  • 繼承TagSupport類,重寫doStartTag()方法,比直接實現Tag介面簡潔很多!
    public class Demo1 extends TagSupport {
    
        @Override
        public int doStartTag() throws JspException {
    
            //獲取到request物件
            HttpServletRequest httpServletRequest = (HttpServletRequest) pageContext.getRequest();
    
            String method = httpServletRequest.getMethod();
    
            JspWriter jspWriter = pageContext.getOut();
    
            try {
                jspWriter.write(method);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return 0;
        }
    }
  • 在tld檔案中描述一把
    <tag>
        <name>showMethod</name>
        <tag-class>tag.Demo1</tag-class>
        <body-content>empty</body-content>
    </tag>
  • 效果:

帶屬性的標籤

上面我們編寫的自定義標籤都沒有附帶屬性的,我們在使用core標籤庫的時候,標籤一般都帶有屬性

其實JSTL標籤庫的原理就是自定義標籤,把自定義標籤搞明白了,對JSTL標籤庫的使用就有更好的理解了

  • 想要自定義標籤帶有屬性也非常簡單,只要在標籤處理器類上加一個成員變數和setter、getter(),再在tld檔案中描述下該屬性即可!它的原理是這樣的:當標籤使用到屬性的時候,引擎就會呼叫它的setter()方法
  • 下面我想要完成的功能是:使用標籤的人,傳入一個字串格式就可以顯示想要的格式日期
  • 編寫標籤處理器類,增加一個成員變數以及對應的setter、getter方法
    public class Demo1 extends TagSupport {
    
    
        //建立成員物件,對應的setter、getter方法
        private String format = null;
    
    
        @Override
        public int doStartTag() throws JspException {
    
            //建立日期格式化物件
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
    
            //格式化日期並向瀏覽器輸出
            try {
                pageContext.getOut().write(simpleDateFormat.format(new Date()));
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return 0;
        }
    
        public String getFormat() {
            return format;
        }
    
        public void setFormat(String format) {
            this.format = format;
        }
    }
  • 在tld檔案中描述標籤和屬性,name代表的是屬性的名字,required代表的是是否為必須,rtexprvalue代表能否使用EL表示式
    <tag>
        <name>formatDate</name>
        <tag-class>tag.Demo1</tag-class>
        <body-content>empty</body-content>
        <attribute>
            <name>format</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
    </tag>
  • 我們來看一下效果:

標籤的繼承關係

  • 在深入講解之前,我們先來看一下各種Tag介面、類之間的關係,這樣學習下去才不會暈

IterationTag說明

  • 我們已經使用過了Tag介面和TagSupport類了。接下來我們看一下IterationTag是什麼玩意。
    public interface IterationTag extends Tag {
        int EVAL_BODY_AGAIN = 2;
    
        int doAfterBody() throws JspException;
    }
  • 從關係圖我們也可以看出,IterationTag介面實現了Tag介面,InterationTag介面和Tag介面最主要的區別就是多了個doAfterBody()方法和EVAL_BODY_AGAIN變數
  • 理解起來也很簡單:當doAfterBody()返回的是EVAL_BODY_AGAIN變數,那麼標籤體的內容就一直迴圈!當然了,TagSupport也實現了Iteration介面,也就是說TagSupport類也能完成Iteration介面的事情
  • 我們來使用一下吧:
    public class Demo1 extends TagSupport {
    
        @Override
        public int doStartTag() throws JspException {
    
            try {
                pageContext.getOut().write("hello");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            //執行標籤體
            return EVAL_BODY_INCLUDE;
        }
    
        @Override
        public int doAfterBody() throws JspException {
    
            //標籤體不斷迴圈,直到doAfterBody()返回的是SKIP_BODY
            return EVAL_BODY_AGAIN;
        
        }
    }
  • tld檔案中描述,既然標籤體有內容,就不能用empty了
    <tag>
        <name>foreverEval</name>
        <tag-class>tag.Demo1</tag-class>
        <body-content>tagdependent</body-content>
    </tag>
  • 注意看橫向的滑輪,已經死迴圈輸出了:
  • doAfterBody()中只要返回的是SKPI_BODY就退出迴圈,執行doEndTag()方法
        //定義一個變數,規定標籤體迴圈的次數
        int x = 0;
        
        @Override
        public int doStartTag() throws JspException {
    
            try {
                pageContext.getOut().write("hello");
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            //執行標籤體
            return EVAL_BODY_INCLUDE;
        }
    
        @Override
        public int doAfterBody() throws JspException {
            
            x++;
            if (x >= 10) {
                return SKIP_BODY;
            }
    
            //標籤體不斷迴圈,直到doAfterBody()返回的是SKIP_BODY
            return EVAL_BODY_AGAIN;
    
        }
  • 現在我們已經能控制迴圈的次數了

BodyTag說明

前面我們已經使用到了帶標籤體的自定義標籤了,前面的都是隻能直接輸出而得不到標籤體的內容,既然得不到標籤體的內容,就更別說修改標籤體了

  • 此時,我們就需要BodyTag介面的支援了!它專門用來處理帶標籤體的標籤,下面我們來看一下BodyTag的原始碼
    public interface BodyTag extends IterationTag {
        /** @deprecated */
        int EVAL_BODY_TAG = 2;

        int EVAL_BODY_BUFFERED = 2;
    
        void setBodyContent(BodyContent var1);
    
        void doInitBody() throws JspException;
    }
  • BodyTag多了EVAL_BODY_BUFFERED變數【一個已經標識過時了】,多了setBodyContent和doInitBody()兩個方法
  • 其實使用BodyTag十分簡單
    • 如果doStartTag()方法返回的是EVAL_BODY_BUFFERED,把標籤體的內容快取起來
    • 接著呼叫setBodyContent()方法和doInitBody()方法,封裝標籤體的內容到BodyContent物件中
    • 接著呼叫doEndTag()方法
    • 對於標籤體的內容,我們可以通過getBodyContenet()來獲取!
  • 再看回上面的關係圖,BodyTag實現了IterationTag和Tag介面,如果直接實現BodyTag介面做開發,要實現的方法就太多了。一般我們使用繼承BodyTag的BodyTagSupport來做開發

BodyTagSupport說明

  • 首先來看一下原始碼吧:
    public class BodyTagSupport extends TagSupport implements BodyTag {
        protected BodyContent bodyContent;
    
        public BodyTagSupport() {
        }
    
        public int doStartTag() throws JspException {
            return 2;
        }
    
        public int doEndTag() throws JspException {
            return super.doEndTag();
        }
    
        public void setBodyContent(BodyContent b) {
            this.bodyContent = b;
        }
    
        public void doInitBody() throws JspException {
        }
    
        public int doAfterBody() throws JspException {
            return 0;
        }
    
        public void release() {
            this.bodyContent = null;
            super.release();
        }
    
        public BodyContent getBodyContent() {
            return this.bodyContent;
        }
    
        public JspWriter getPreviousOut() {
            return this.bodyContent.getEnclosingWriter();
        }
    }
  • 可以發現:BodyTagSupport主要擴充了以下的內容:
    • 把BodyContent直接定義為成員變數,在獲取標籤體內容的時候就不需要通過getBodyContent()獲取了
    • 提供獲取JspWriter的方法,不需要從pageConext中獲取了
    • 以上的兩個擴充都簡化了我們的程式碼書寫
        protected BodyContent bodyContent;

        public JspWriter getPreviousOut() {
            return this.bodyContent.getEnclosingWriter();
        }
  • 從BodyTag介面中,我就說到了:標籤體的內容封裝到了BodyContent類中,那麼BodyContent類究竟是什麼?我們來看一下原始碼
    public abstract class BodyContent extends JspWriter {
        private JspWriter enclosingWriter;
    
        protected BodyContent(JspWriter e) {
            super(-2, false);
            this.enclosingWriter = e;
        }
    
        public void flush() throws IOException {
            throw new IOException("Illegal to flush within a custom tag");
        }
    
        public void clearBody() {
            try {
                this.clear();
            } catch (IOException var2) {
                throw new Error("internal error!;");
            }
        }
    
        public abstract Reader getReader();
    
        public abstract String getString();
    
        public abstract void writeOut(Writer var1) throws IOException;
    
        public JspWriter getEnclosingWriter() {
            return this.enclosingWriter;
        }
    }
  • 原來BodyContent繼承著JspWriter,它與JspWriter最大的區別是:BodyContent類的任何寫入的內容並不自動地向頁面輸出!
  • 我們一般使用BodyContent都使用兩個方法:
    //將資料轉變成Reader物件
    public abstract Reader getReader();

    //將資料轉變成String物件
    public abstract String getString();
  • 再從關係圖我們可以看初,BodyTagSupport繼承了TagSupport類實現了BodyTag介面,可以說:BodyTagSupport有著前面講的介面和類的所有功能!
  • 下面我們來使用下BodyTagSupport將標籤體的內容轉成是小寫的
  • 標籤處理器類
    public class Demo1 extends BodyTagSupport {
    
        @Override
        public int doStartTag() throws JspException {
    
    
            //想要獲取到標籤體的內容,就要返回EVAL_BODY_BUFFERED變數
    
            return EVAL_BODY_BUFFERED;
    
        }
    
        @Override
        public int doEndTag() throws JspException {
    
            //獲取到標籤體的內容
            String value = bodyContent.getString();
    
            //將標籤體的內容轉成小寫並輸出
            try {
                this.getPreviousOut().write(value.toLowerCase());
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return super.doEndTag();
        }
    
    }
  • tld檔案:
    <tag>
        <name>BodyContentToLowerCase</name>
        <tag-class>tag.Demo1</tag-class>
        <body-content>tagdependent</body-content>
    </tag>
  • 效果:

如果文章有錯的地方歡迎指正,大家互相交流。