tld文件是对标签库的说明。一个tld文件就是一个以xml格式书写的文本文件。
因此,熟悉tld文件就是熟悉tld文件中的标签。例如:
<?xml version="1.0" encoding="ISO-8859-1">
<!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>test</short-name>
<uri>http://www.manning.com/sampleLib</uri>
<tag>
<name>greet</name>
<tag-class>sampleLib.GreetTag</tag-class>
<body-content>empty</body-content>
<description>Prints Hello and the user name</description>
<attribute>
<name>user</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
</tag>
</taglib>
上例清晰的向使用者表达了三件事。
1.象所有XML文件一样,第一行标明了XML的版本号及字符编码;
2.接下来的DOCTYPE声明了文档类型。对于JSP1.2版本,必须按上例书写;
3.最后,由<taglib>所包围的才是内容的主体。我们所要熟悉的也就是这一部分。
下面来看看其中的几个重要元素:
<taglib>元素
是tld文件的顶层元素,根元素。其下有很多二级元素,如下表:
元素 | 描述 | 出现次数 |
tlib-version | 标签库版本 | 只允许一次 |
jsp-version | 指定所遵循的jsp规范的版本 | 只允许一次 |
short-name | 作用同前缀 | 只允许一次 |
uri | 标识本库。作用同taglib-uri和taglib-location | 多数情况就一次 |
display-name | 显示名称 | 多数情况就一次 |
samll-icon | 作用同字面意义 | 多数情况就一次 |
large-icon | 作用同字面意义 | 多数情况就一次 |
description | 对本库的文字描述 | 多数情况就一次 |
validator | 多数情况就一次 | |
listener | 指定事件侦听器类 | 任何数量 |
tag | 一个具体的标签对应一个tag,具体内容由tag子元素指定 | 至少出现一次 |
注意其中的uri子元素是一种隐含标识标签库。不过,如果web.xml文件中显式的做了taglib-uri和taglib-location声明,则以web.xml为准。
我的理解是,tld文件中的uri属于开发时描述,web.xml是属于部署时描述。tld的主要职责是描述标签库,而具体标签库被放在哪里当然应该由web.xml文件描述。所以tld中的uri只能属于参考。
<tag>元素
tag用于具体描述每个标签。它和标签是一对一关系,有多少标签就会有多少tag。其下有很多二级元素来详细描述一个标签。见下表:
元素 | 描述 | 出现次数 |
name | 标签名 | 只允许一次 |
tag-class | 该标签实际对应的类 | 只允许一次 |
tei-class | 一个可选的附加类 | 多数情况就一次 |
body-content | 标签体类型.empty,jsp,or tagdependt.缺省jsp | 多数情况就一次 |
display-name | 标签显示名 | 多数情况就一次 |
samll-icon | 作用同字面意义 | 多数情况就一次 |
large-icon | 作用同字面意义 | 多数情况就一次 |
description | 对本标签的文字描述 | 多数情况就一次 |
variable | 指定变量信息 | 任何数量 |
attribute | 描述标签可接受的参数 | 任何数量 |
example | 标签使用示例 | 多数情况就一次 |
对于同一个类定义不同的标签名是允许的。但是不允许有相同的标签名。
<attribute>元素
是<tag>的子元素。如果该标签可以接收外部参数,则需要用<attribute>来指定。一个参数一个<attribute>。
每个<attribute>又另外有5个子元素,用以描述可接收的外部参数,见下表:
元素 | 描述 |
name | 参数名(外部参数名) |
required | 指定是否为必须。缺省是false。取值范围是true,false,yes,or no. |
rtexprvalue | 是否可以是请求时表达式。缺省是false。取值范围是true,false,yes,or no. |
type | 只有当<rtexprvalue>为true时才有效。指示请求时表达式的返回值类型。缺省是字符串。 |
descrption | 参数描述 |
以上元素只有name,是必须的,其它为可选。
<body-content>元素
该元素也是<tag>的子元素。这个元素只有三个值。empty,jsp,tagdependent。
.empty-指定该标签不可以有其它内容。
.jsp-指出该标签可以带任何可以正常转换成jsp代码的内容。如:文本,HTML标记,脚本元素,标准动作,甚至是另外的标签。地线是只要能正常转换成JAVA代码。
.tagdependent-指出其内容不是jsp代码。这样jsp引擎就不会去执行代码,转而交给其标签所对的class类去处理。通常出现这种值的情况是,所写代码不是java语言。例如:
<test:dbQuery>
SELECT count(*) FROM USERS
</test:dbQuery>
显然其中的内容是SQL语句。
由于tld文件本身是以XML格式书写的,所以要遵循以下规则。
1.元素间的层次和顺序关系要注意。
2.大小写敏感。
3.对于jsp 1.2版本,必须带连字符-。象<tagclass>在jsp 1.1中是合法的,但在1.2版必须写成<tag-class>。
标签扩展API
标签扩展API是一组接口和类。我们写servlet需要用到servlet API,为了写定制标签我们就需要标签扩展API(Tag Extension API)。
这个API只有一个包javax.servlet.jsp.tagext。其中有4个接口和13个类。主要的接口和类见下表:
接口 | 描述 |
Tag | 所有的标签处理器都直接或间接的继承该接口。是该包的基接口。 |
IterationTag | 继承了Tag接口,增加一个方法doAfterBody()。支持某部分的重复执行。 |
BodyTag | 继承IterationTag接口,增加两个方法doInitBody()和setBodyContent()。 支持对标签体的缓冲。 |
类 | 描述 |
TagSupport | 实现了IterationTag接口,并对所有方法提供缺省实现。 |
BodyTagSupport | 实现了BodyTag接口,并对所有方法提供缺省实现。 |
BodyContent | 继承JspWriter类,用于临时为标签体的计算提供缓冲区。 这个对象只可以和BodyTag接口及BodyTagSupport类连用。 |
此外标签处理器还使用另外两个在javax.servlet.jsp中定义的异常。
异常类 | 描述 |
JspException | 来源于java.lang.Exception异常。一些重要的方法如doStartTag(), doInitBody(), doAfterBody(), and doEndTag()都会抛出JspException异常。 不捕捉这个异常将会触发error-page。 |
JspTagException | 扩展于JspException。标签处理器用它来指示不可恢复的错误。 |
主要接口和类的继承关系如图:椭圆标识接口,矩形标识类。
在继续详述标签接口及类之前,先回忆一下定制标签的总体的调用机制。见图:(其中蓝色为声明过程,红色为调用过程)
实现Tag接口
所有的标签处理器都需要间接或直接的实现这个接口。
下面列出Tag接口定义的方法和常量:
方法名 | 描述 |
int doStartTag() | 当碰到标签起始标记时执行。(<XXX:XXX>) |
int doEndTag() | 当碰到标签结束标记时执行。(</XXX:XXX>) |
Tag getParent() | 获得一个父标签对象 |
void release() | 从内存中释放该标签对象 |
void setPageContext(PageContext) | 设置当前页上下文(page context) |
void setParent(Tag) | 对父标签对象进行设置 |
常量 | 描述 |
EVAL_BODY_INCLUDE | doStartTag()的返回值。指出jsp引擎计算标记体并输出 |
SKIP_BODY | doStartTag()的返回值。指出jsp引擎跳过标记体且不输出 |
EVAL_PAGE | doEndTag()的返回值。指出jsp引擎计算页面剩余部分并输出 |
SKIP_PAGE | doEndTag()的返回值。指出jsp引擎跳过页面剩余部分且不输出 |
setPageContext()方法
setPageContext()方法是一个定制标签声明周期内第一个要被调用的方法。完整写法是:
public void setPageContext(PageContext);
jsp引擎会将jsp页转换时隐含创建的pageContext对象,作为参数,调用setPageContext方法。
通常的做法会将这个参数保存为本标记处理器的参数。
setParent()和getParent()方法
当标签嵌套使用时,外层的标签被成为父标签,内部的被称为子标签。完整的写法是:
public void setParent(Tag);
public Tag getParent();
其中setParent方法对父标签对象进行设置。而getParent方法用以获得父标签对象。
setter方法
当定制标签中包含属性时,在标签处理器中有和javaBean相同的设置机制。即每一个属性XXX,都有对应的setXXX()方法。
这些方法的调用是在setPageContext()和setParent()之后,doStartTag()之前。(就是说所有的属性-即实例变量-必须完全赋值,因为随后的方法可能会调用到它们)
doStartTag()方法
该方法标志着真正的标签处理开始。当前三阶段的方法执行完后,jsp引擎就会调用doStartTag()方法了。完整写法是:
public int doStartTag() throws JspException;
该方法给了标签处理器一个初始化计算和校验属性值合法性的机会。如果初始化失败或属性值不合法,则抛出JspException异常,或其子类,如JspTagException。
初始化计算过后,该方法将决定是否继续对标签体进行计算。作为结果,返回一个整数常量(EVAL_BODY_INCLUDE或SKIP_BODY),用以指示是否跳过标签体的执行。除此之外不返回任何其它值。
doEndTag()方法
执行完doStartTag()方法后,就该执行doEndTag()了。完整写法是:
public int doEndTag() throws JspException;
该方法标志着真正的标签处理结束。同样该方法将决定是否继续执行jsp文件中该标签之后的内容。它也会返回一个整数常量(EVAL_PAGE或SKIP_PAGE),用以指示页面后续内容是否跳过。注意此处的范围是页,并不是转换单位。所以计算不计算全都是针对该页而言。
release()方法
最后,jsp引擎将调用release方法,当标签处理结束,标签处理器对象不再使用时。完整写法是:
public void release();
注意。一个页面内同时有多个相同标签时,jsp引擎会只会为该标签创建一个对象。只有等该页内相同标签全部处理完了。jsp引擎才会调用release()方法,将该标签对象从内存清理掉。
各方法的调用流程如下图:
实现IterationTag接口
IterationTag接口继承了Tag接口,允许对标签体做多次循环计算,就像编程语言中的循环。
在Tag的基础上,InterationTag增加了一个方法和一个常量。doAfterBody()和EVAL_BODY_AGAIN。
doAfterBody方法
在该方法中,doStartTag被调用之后会返回一个常量(EVAL_BODY_INCLUDE或SKIP_BODY)指出标签体是否被包括。
jsp引擎在对标签体进行了第一次计算后,会自动调用doAfterBody方法(第一次);在doAfterBody方法中会决定是否需要再次计算标签体。
当不需要再次计算时,该方法返回SKIP_BODY常量,并结束该方法的执行。
反之,该方法返回EVAL_BODY_AGAIN常量。jsp引擎在得到这个常量后,会对标签体做再次计算。接着再调用doAfterBody方法(第二次),以询问是否还要计算标签体。
周而复始,直至返回SKIP_BODY。
显然这时一个先执行后循环的循环语句,类似于java的do...while()语句。
jsp引擎自动调用doAfterTag的根据是来自于标签的实现是否是一个IterationTag实例,而不是根据tld文件的描述。
其流程图如下:
实现BodyTag接口
BodyTag接口继承了IterationTag接口。它提供了这样一种功能,将标签体的处理结果放到一个缓冲区,在缓冲区内对结果在进行处理,而不是象Tag或IterationTag接口将标签体的处理结果直接发送到输出流。
另外,由于继承了IterationTag接口,对标签体结果的处理也可以循环进行多次。
在该接口中定义了两个方法(setBodyContent()和doInitBody())和一个整数常量(EVAL_BODY_BUFFERED)。
setBodyContent()方法
完整写法是:
public int setBodyContent(BodyContent);
该方法的典型实现是将BodyContent赋值给本地局部变量以供后面使用。
doInitBody()方法
jsp引擎在调用完setBodyContent方法后会接着调用doInitBody方法。完整写法是:
public void doInitBody() throws JspException;
该接口的运行机制象前两种接口一样,先依次调用setPagerContext(),setParent(),实例变量的setXXX方法,doStartTag()。
这是doStartTag方法返回一个常量(EVAL_BODY_INCLUDE, SKIP_BODY或EVAL_BODY_BUFFERED)。
如果返回的是EVAL_BODY_BUFFERED,jsp引擎首先创建一个BodyContent类对象。BodyContent类是JspWriter的子类,而且所有的write,print方法都被重写。不再直接输出到客户端,而是输出到该对象(缓冲对象)内部。
jsp引擎接着将该对象作为参数带入到setBodyContent方法中,在执行完setBodyContent方法后,接着再调用doInitBody方法,此时并没有对缓冲对象进行计算,只是给出一个机会,对缓冲对象初始化。
接着,jsp引擎处理缓冲对象,并将结果再存在缓冲对象中。
再后来,doAfterBody方法被调用,它将返回EVAL_BODY_AGAIN或EVAL_BODY_BUFFERED用以指示缓冲对象需要循环计算,而返回SKIP_BODY则标明就此结束。
最后,呼叫doEndTag方法,结束标签处理。
其流程图如下:
继承TagSupport和BodyTagSupport类
前面我们依次介绍了Tag,IterationTag,BodyTag接口。实际应用当中,我们并不需要直接实现他们。jsp规范中已经为我们提供了两个类TagSupport和BodyTagSupport,用以对IterationTag和BodyTag的缺省实现。
所以,我们只需要根据需要去继承这两个类,然后重写我们需要定制的方法就可以了。
TagSupport类
该类是IterationTag的缺省实现。除了实现原有方法外,本身还增加了一些有用的其他方法和成员变量。下表列出其中重要的几个:
名称 | 描述 |
重写的方法和他们的返回值 | |
void doStartTag() | 继承自Tag。返回值SKIP_BODY。 |
void doAfterBody() | 继承自IterationTag。返回值SKIP_BODY。 |
void doEndTag() | 继承自Tag。返回值EVAL_PAGE。 |
处理嵌套标签的有用的方法 | |
void setParent(Tag) | 接受和维护一个父标签的引用。 |
Tag getParent() | 返回父标签的引用。 |
Tag findAncestorWithClass(Tag,Class) | 静态方法。寻找最近的指定类的标签。 |
标签属性操作 | |
void setValue(String,Object) | 设置属性。属性名是字符串,值可以是任何对象。 |
Object getValue(String) | 返回给定属性名的属性的值。 |
Enumeration getValues() | 返回一个所有值的枚举。 |
void removeValue(String) | 删除给定的属性。 |
受保护的实例变量 | |
PageContext pageContext | 一个保存着PageContext对象的变量。 |
BodyTagSupport类
该类同时继承了TagSupport类,并实现BodyTag接口。除了前表所示方法,该类还提供了一些其它方法便于使用。
名称 | 描述 |
重写的方法和他们的返回值 | |
void doStartTag() | 继承自Tag。返回值EVAL_BODY_BUFFERED。 |
void doAfterBody() | 继承自IterationTag。返回值SKIP_BODY。 |
void doEndTag() | 继承自Tag。返回值EVAL_PAGE。 |
用以处理缓冲的方法 | |
void setBodyContent(BodyContent) | 接受和维护一个BodyContent对象(缓冲对象)的引用。 |
BodyContent getBodyContent() | 返回BodyContent对象的引用。 |
JspWriter getPreviousOut() | 返回JspWriter对象,BodyContent就是对他的简单包裹。 |
定制标签和JavaBeans之间的简单区分
虽然两者都是为了提高组件可重用性而设计的。但是一个经常被问起的问题是:如何决定在何时使用其中的一种技术,而不是另一种。
这里有个简单的比较,来帮助你如何决策。
.JavaBeans是jsp页的数据处理器并增加数据管理的逻辑。主要用以存储。定制标签,倾向于对特定请求的处理。
.标签是线程安全的,而bean则不是。Bean的线程安全是由开发人员手工实现的。
.标签了解他所运行的外部环境(pageContext),而bean则不。
.标签属于转换单位的一部分,而bean独立于转换单位之外。
.标签可以访问隐含对象,bean则不能。(这是对上两条的细节描述)
.标签只有页范围(page scope)。他们在一个请求内和一个页面内被创建。虽然他们可以访问所有的范围。beans则可以被创建于各种不同的范围。标签可以创建和维护bean,反之不然。
.标签的API是围绕jsp页的概念开发的,所以标签只能用在jsp页中。而bean却可以被更广泛的组件使用。
.标签不是持久化对象。而bean可以被持久化(序列化),以被后续使用。
相比之下标签较靠近用户,且只能用于jsp页面;而bean较靠近商业逻辑,更适合处理数据处理和维护类工作,且可以被任何组件重用。因此,首选的使用方法是用户的请求由标签负责处理,而需要应用商业逻辑和数据处理的时候,由标签再调用bean来处理。