第 9 章 封装taglib组件

注意

这里讲介绍自定义标签库(taglib),将原本需要写在jsp中的java代码封装起来,成为可复用的组件。
taglib本意是为了弥补jsp的先天不足,但它的笨重与复杂也颇为经典,可惜有的地方又不得不用,如果对其没有耐心尽可跳过。
如果你不满足以下任一条件,请继续阅读,否则请跳过此后的部分,进入下一章: 第 10 章 综合电子留言板
  1. 了解taglib的使用和制作。
  2. 根本不想消除jsp中的java代码,也不打算写一些可以复用的组件。

9.1. 用taglib实现循环

回到联系簿的例子 第 5.2 节 “Read(读取)”,不觉得这个list.jsp中的java代码太碍眼了吗?
<%
    List list = contactDao.getAll();
    for (int i = 0; i < list.size(); i++) {
        pageContext.setAttribute("contact", list.get(i));
        pageContext.setAttribute("row", i % 2 != 0 ? "odd" : "even");
%>
                <tr class="${row}" onmouseover="this.className='highlight';" onmouseout="this.className='${row}';">
                    <td>${contact.username}</td>
                    <td>${contact.sex}</td>
                    <td>${contact.email}</td>
                    <td>${contact.qq}</td>
                    <td>${contact.descn}</td>
                    <td><a href="edit.jsp?id=${contact.id}">修改</a> | <a href="remove.jsp?id=${contact.id}">删除</a></td>
                </tr>
<%
    }
%>
        
如果能像使用jsp动作(action)一样,使用<jsp:xxx>的形式进行循环该多好啊?可惜jsp动作(action)的功能太少了,它没办法进行循环,我们只好自己实现taglib。
比较一下使用taglib前后jsp中的样子。
<lingirl:for var="contact" items="${list}">
    <tr class="${contact_row}" onmouseover="this.className='highlight';" onmouseout="this.className='${contact_row}';">
        <td>${contact.username}</td>
        <td>${contact.sex}</td>
        <td>${contact.email}</td>
        <td>${contact.qq}</td>
        <td>${contact.descn}</td>
        <td><a href="contact.do?method=edit&id=${contact.id}">修改</a> | <a href="contact.do?method=remove&id=${contact.id}">删除</a></td>
    </tr>
</lingirl:for>
        
taglib的写法和jsp动作(action)很相似,是由taglib前缀,冒号,标签名三者的组合体。其中taglib前缀是用jsp指令(direction)定义的。
<%@ taglib uri="WEB-INF/tld/lingirl.tld" prefix="lingirl" %>
        
这里的jsp指令(direction)是专门用来定义标签库的,uri指定tld定义文件的位置,prefix指定对应的taglib前缀。通过这里的定义才能在下面使用taglib。
看看taglib带给了我们什么?
  1. items="${list}"表示将对list变量进行循环操作。
  2. var="contact"表示循环得到的每个元素对应的变量名。
    taglib中循环list,每获得一个数据就通过pageContext.setAttribute("contact", contact);放到pageContext中,接着处理标签中包含的内容,这样标签中间的内容就可以通过${context.username}的形式获得每一行的数据。
了解过如何使用我们的taglib,现在可以看具体实现了,首先我们要编写一个ForTag.java。
  1. 第一步,让ForTag继承BodyTagSupport。
    BodyTagSupport专门用来制作带内容的taglib,它为我们提供了几个好用的方法来处理数据。
  2. 第二步,为ForTag设置两个自定义参数:var和items。
    对应标签中的<lingirl:for var="contact" items="${list}">,我们需要在ForTag中写两个与其名称对应的setter方法。
    public void setVar(String var) {
        this.var = var;
    }
    public void setItems(Collection items) {
        this.iterator = items.iterator();
    }
                    
    这两个方法会在标签使用的时候,自动获得参数的值,供以后使用。
  3. 第三步,让ForTag处理标签内容。
    public int doStartTag() throws JspException {
        this.index = 0;
        if (this.process()) {
            return EVAL_BODY_INCLUDE;
        } else {
            return EVAL_PAGE;
        }
    }
    public int doAfterBody() {
        if (this.process()) {
            return EVAL_BODY_AGAIN;
        } else {
            return EVAL_PAGE;
        }
    }
                    
    为了实现循环,我们需要监听两个事件。
    doStartTag()方法在标签开始时执行,要记住每次都要对类进行初始化,避免上一次的遗留数据对操作造成影响。然后判断是否有数据需要处理,如果有,则返回EVAL_BODY_INCLUDE开始处理标签里的内容,如果没有,返回 EVAL_PAGE跳过标签内容执行标签下面的内容。
    doAfterBody()方法在每次处理完标签内部内容后执行,判断循环是否已经结束,如果可以继续循环,返回EVAL_BODY_AGAIN用循环得到新的数据再次处理标签内部内容,如果循环结束就返回EVAL_PAGE结束标签。
  4. 第四步,进行循环时的处理。
    private boolean process() {
        if (this.iterator.hasNext()) {
            String row = this.index % 2 != 0 ? "odd" : "even";
            pageContext.setAttribute(var + "_index", this.index);
            pageContext.setAttribute(var + "_row", row);
            Object item = this.iterator.next();
            pageContext.setAttribute(var, item);
            this.index++;
            return true;
        } else {
            return false;
        }
    }
                    
    process()方法在doStartTag()和doAfterBody()中都会用到,它的用途是判断循环是否结束,如果还可以继续循环就返回true,否则返回false。
    如果还可以继续循环,则从iterator中循环获得下一个数据,根据var的值放到pageContext中,同时放到pageContext里的还有index索引值和row索引值的奇偶,odd代表奇数行,even代表偶数行。var="contact"的情况下,${contact}表示循环数据,${contact_index}表示索引值,${context_row}表示奇偶性,这些都可以在标签内部的jsp中直接使用。
经过如此一番周折,ForTag可以从标签获得参数,并对数据进行循环处理了。最后一步还要为它编写tld(taglib definition)标签库定义文件,提供给jsp指令(direction)引用。
<?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>lingirl</short-name>
   <uri>http://www.family168.com/lingirl</uri>
   <tag>
      <name>for</name>
      <tag-class>anni.ForTag</tag-class>
      <attribute>
         <name>var</name>
         <required>true</required>
         <rtexprvalue>true</rtexprvalue>
         <type>java.lang.String</type>
      </attribute>
      <attribute>
         <name>items</name>
         <required>true</required>
         <rtexprvalue>true</rtexprvalue>
         <type>java.util.Collection</type>
      </attribute>
   </tag>
</taglib>
        
前面一大堆复杂难懂的标签指定我们使用taglib规范的版本,进入tag部分才开始定义名字为for的标签,使用tag-class指定对应的类,再定义两个参数:var和items。required说明参数不能省略必须手工设置。rtexprvalue表示参数部分可以使用el,否则就只能用字符串。type对应的是类中使用的真实类型,taglib会根据它做类型转换。
全部的例子在09-01目录下,注意编译taglib需要将jsp-api.jar加入classpath,参考WEB-INF/src/compile.bat。
结果,为了替换4,5行java代码,我们需要编写一个ForTag.java,一个对应tld文件,在jsp中引用tld,最后才能使用ForTag对list进行循环。不得不说一句:“太麻烦啦。”

9.2. 关于jstl

taglib太笨重,也太复杂了。编写一个taglib花费的力气太大,又不容易修改或扩展。一般情况下,taglib都是由别人写好,我们再直接调用。sun就为标签库定义了一套标准,叫做jstl(java standard taglib)java标准标签库,可以去 http://jakarta.apache.org/taglibs/index.html下载apache实现的jstl。
想在项目里使用jstl,首先要把jstl.jar和standard.jar两个文件放到/WEB-INF/lib/目录下。
jsp-ch-09-02-jstl-01.png
然后在list.jsp中加入jsp指令(direction)引用jstl中定义的标签库。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
        
这里的uri是固定写法,只要写成这个就可以使用jstl了,jstl中包含多个标签库,这里我们只用到core。
经过上述配置,现在可以使用jstl了,代码如下:
<c:forEach var="contact" items="${list}" varStatus="status">
  <c:set var="row" value="${status.index % 2 != 0 ? 'odd' : 'even'}"/>
    <tr class="${row}" onmouseover="this.className='highlight';" onmouseout="this.className='${row}';">
        <td>${contact.username}</td>
        <td>${contact.sex}</td>
        <td>${contact.email}</td>
        <td>${contact.qq}</td>
        <td>${contact.descn}</td>
        <td><a href="contact.do?method=edit&id=${contact.id}">修改</a> | <a href="contact.do?method=remove&id=${contact.id}">删除</a></td>
    </tr>
</c:forEach>
        
这里使用的是c:forEach,它也是一个执行循环的标签,var和items参数的意义与上边谈到的lingirl:for标签已知,分别代表循环变量和循环数据。唯一不同的是多了一个varStatus参数,这个参数表示当前行的状态,其中status.index表示当前行的序号,我们就通过序号计算奇偶行。
在c:forEach标签中,我们还看到一个c:set标签,它的作用是可以将指定的变量保存到作用域中,默认作用域是page,这里我们使用status.index计算出行的奇偶性,然后保存到row中,后面就可以直接使用${row}调用了。
jstl中的c:forEach不但可以处理Collection,还可以处理数组和Map,使用jstl我们更容易写出结构一致的代码,以初学jsp来说,自定义taglib还是太复杂了,所以还是先学习一些常用的jstl为好。
例子在lingo-sample/09-02下,其中只有list.jsp中使用了jstl。