43.3. JSP扩展标记

43.3.1. JSP 扩展标记介绍

标准的JSP标记可以调用JavaBeans组件和执行请求(request)的分配,简化JSP页面的开发和维护。JSP技术同样支持封装其他类型的动态功能到客户定制的扩展标记中。扩展标记通常以标记库(tag library)的形式发布,包含一系列相关的标记和这些标记的实现对象。

标记库体系结构

JSP扩展标记能够完成很多任务,例如操作内置对象、处理表单、访问数据库、访问其他企业服务和实现流程控制。JSP扩展标记由精通Java语言和数据库的开发人员创建,供Web应用的设计者使用,他们只关心页面的表现形式。

扩展标记是用户定义的JSP元素。当包含扩展标记的JSP页面转化成servlet时,标记被转换成对标记处理类(tag handler)的操作。Web 容器在执行JSP页面对应的servlet时会调用标记处理类(tag handler)的方法。扩展标记有很多特性:

  • 可以通过传递属性定制

  • 访问JSP页面中的可用对象

  • 改变JSP页面的响应(response)

  • 可以互相通讯

  • 可以嵌套,实现复杂的交互

扩展标记实现原理

JSP2.0定义一种新的Tag 处理方式,直接handler.serJspContext(),然后直接执行doTag()方法。

43.3.2. 使用扩展标记

这一节介绍了如何在JSP页面中使用扩展标记和扩展标记的类型。

43.3.2.1. 声明扩展标记

在使用标记库中的标记之前,开发人员需要在JSP页面中使用taglib伪指令声明标记库:

<%@ taglib uri="myTagLib" prefix="mt" %>

uri属性指向唯一标识标记库描述符的URI。JSP引擎通过匹配uri属性与Web引用部署描述(web.xml)中定义的<taglib-uri>元素来尝试查找标记库描述符。例如,上述taglib指令中的myTaglib将引用在如下Web应用部署描述中的标记库描述符:

<taglib>
    <taglib-uri>myTagLib</taglib-uri>
    <taglib-location>/WEB-INF/myTagLib.tld</taglib-location>
</taglib>

prefix属性定义了标记库的前缀,用来和其他标记库进行区分,当使用JSP扩展标记编写页面时使用此前缀引用标记库中的标记。例如,如上例中名为myTagLib的库定义了一个名为helloTag的标记,可在JSP页面中这样引用:

<mt:helloTag>

43.3.2.2. 扩展标记的类型

JSP扩展标记使用XML语法,具有开始标记和结束标记,可能还包含标记体:

<tt:tag>
   body
</tt:tag>

没有标记体的标记可以表示为:

<tt:tag />

简单标记

简单标记不包含标记体和属性:

<tt:simple />

带属性的标记

扩展标记可能带有属性。属性被列在开始标记中,语法形式为:attr="value"。属性值用来定制扩展标记的行为,就象参数定义方法的行为一样。开发人员可以在标记库描述符中指定标记属性的类型。属性值来自字符串(String)常量和表达式,由于这两种类型都是字符串(String),那么它们转换为正确属性类型的规则遵循在第 43.1.3.6.3 节 “设置JavaBeans的属性”中的描述。

带有标记体的标记

扩展标记在开始标记和结束标记之间可以包含扩展和标准标记、脚本元素、HTML代码等标记体。

定义脚本变量的标记

扩展标记可以定义在JSP页面中使用的脚本变量。下面的代码片断示例了如何定义和使用脚本变量,脚本变量包含了从JNDI返回的对象:

<mt:lookup id="tx" type="UserTransaction"
   name="java:comp/UserTransaction" />
<% tx.begin(); %>

协作标记

扩展标记可以通过共享对象互相协作。下面的例子tag1创建了对象obj1,然后在tag2中使用。

<tt:tag1 attr1="obj1" value1="value" />
<tt:tag2 attr1="obj1" />

下一个例子,对象由一组嵌套标记中的封装标记创建,在所有内部标记中也可用。由于对象是没有命名的,可以减少潜在的命名冲突。

<tt:outerTag>
   <tt:innerTag />
</tt:outerTag>

43.3.3. 定义扩展标记

定义扩展标记需要:

  • 开发标记处理类(tag handler )和辅助类

  • 在标记库描述符( tag library descriptor )中声明标记

这一节描述了标记处理类和标记库描述符的属性,说明如何开发标记处理类和标记库描述符。

43.3.3.1. 标记库描述符

标记库描述符(tag library descriptor,TLD)是描述标记库的XML文档,包含了关于标记库的整体信息和其中每一个标记的信息。Web容器使用标记库描述符校验标记。标记库描述符必须以.tld为文件扩展名,存放在WEB-INF目录或其子目录中。

标记库描述符必须以XML文档的序言开始,定义XML和DTD的版本信息。

<?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">

也可在根元素指定(JSP2.0,jsptaglibrary 2.0起支持)

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_0.xsd"    version="2.0"

标记库描述符的根元素是taglib ,taglib的子元素为:

元素描述
tlib-version标记库的版本
jsp-versionJSP 规范的版本
short-name可选
uri唯一标识标记库的 URI
display-name可选
small-icon可选,小图标
large-icon可选,大图标
description可选,描述信息
listener应用事件监听类
tag标记

标记库描述符中的每一个tag元素描述了标记的名称、处理类、标记创建的脚本变量和标记的属性。tag元素的子元素为:

元素描述
name标记名
tag-class标记处理类
tei-class可选,javax.servlet.jsp.tagext.TagExtraInfo 子类
body-content标记体类型
display-name可选
small-icon可选,小图标
large-icon可选,大图标
description可选,描述信息
variable可选,脚本变量信息
attribute标记属性信息

例如,下面定义了helloTag标记,包含parameter属性。

<tag>
   <name>helloTag</name>
   <tag-class>com.apusic.test.MyTagHandler</tag-class>
   <body-content>JSP</body-content>
   ...
   <attribute>
      <name>parameter</name>
      <required>false</required>
      <rtexprvalue>true</rtexprvalue>
   </attribute>
   ...
</tag>

43.3.3.2. 标记处理类(Tag Handlers )

标记处理类(Tag Handlers )是在 处理包含扩展标记的JSP页面时由Web容器调用。标记处理类必须实现TagBodyTag接口,开发人员可以使用TagSupportBodyTagSupport作为基类,这些类和接口都包含在javax.servlet.jsp.tagext包中。

标记处理类的方法定义在Tag和BodyTag接口中,被JSP页面的实现servlet在不同的地方调用。当遇到开始标记时,JSP页面的实现servlet调用标记处理类适当的方法初始化,然后调用doStartTag方法。当遇到结束标记时,标记处理类的doEndTag方法被调用。如果标记处理类需要和标记体交互,那么附加的方法将在开始和结束标记之间被调用。请参考第 43.3.4 节 “标记处理类如何被调用”。为了提供标记处理类的实现,开发人员必须针对不同的情况实现接口的方法,下面总结了标记处理类的实现方法。

标记类型实现方法
简单标记doStartTag, doEndTag, release
带属性的标记doStartTag, doEndTag, set/getAttribute1...N, release
带标记体,需要执行,但不交互doStartTag, doEndTag, release
带标记体,需要反复执行doStartTag, doAfterBody, doEndTag, release
带标记体,需要交互doStartTag, doEndTag, release, doInitBody, doAfterBody, release

标记处理类能够访问与JSP页面通讯的API。访问这些API的入口点是page context对象(javax.servlet.jsp.PageContext),通过PageContext标记处理类能够访问其他JSP页面可用的内置对象:request、session和application。

如果标记是嵌套的,那么内部标记的处理类可以访问封装标记的处理类。

43.3.3.3. 简单标记

简单标记的处理类必须实现Tag接口的doStartTag和doEndTag方法。当遇到开始标记时,doStartTag方法被调用。因为简单标记没有标记体,所以doStartTag方法返回SKIP_BODY。当遇到结束标记时,doEndTag方法被调用。如果需要执行页面后面的内容,doEndTag返回 EVAL_PAGE,否则返回SKIP_PAGE。例如一个简单标记

<tt:simple />

该标记的处理类为:

public SimpleTag extends TagSupport {

   //遇到开始标记时调用
   public int doStartTag() throws JspException {
      try {
         pageContext.getOut().print("Hello.");
      } catch (Exception ex) {
         throw new JspTagException("SimpleTag: " +
            ex.getMessage());
      }
      return SKIP_BODY;
   }

   //遇到结束标记时调用
   public int doEndTag() {
      return EVAL_PAGE;
   }
}

简单标记不包含标记体,在标记库描述符中body-content必须声明为empty :

<body-content>empty</body-content>

43.3.3.4. 带属性的标记

对标记的每一个属性,开发人员必须遵循JavaBeans体系结构的设计惯例定义get和set方法。例如标记present,

<logic:present parameter="Clear">

包含属性parameter , 必须定义get和set方法,

protected String parameter = null;

public String getParameter() {
   return (this.parameter);
}

public void setParameter(String parameter) {
   this.parameter = parameter;
}
[注意]注意

如果属性命名为id,且标记处理类从TagSupport类继承,那么不需要定义get和set方法。TagSupport类中已经定义了getId和setId。

对标记的每一个属性,开发人员必须确定属性是否是必须的,属性值是否可以通过动态的表达式的值得到。通过attribute元素指定属性的类型。对于静态的属性值,属性的类型总是java.lang.String。如果rtexprvalue元素是true或yes,属性类型可以定义为期待表达式返回的类型。

<attribute>
   <name>attr1</name>
   <required>true|false|yes|no</required>
   <rtexprvalue>true|false|yes|no</rtexprvalue>
   <type>fully_qualified_type</type>
</attribute>

如果标记属性不是必须的,标记的处理类应该提供缺省值。

传递给标记的属性可以在JSP页面转换的时候被TagExtraInfo类的isValid方法验证。isValid方法的参数为TagData对象,包含了每个标记的属性-值信息。由于验证发生在JSP页面的转换阶段,需要在请求(request)阶段计算的属性值将被设置为TagData.REQUEST_TIME_VALUE。

标记 <tt:twa attr1="value1"/> 在标记库包含下面的属性声明:

<attribute>
   <name>attr1</name>
   <required>true</required>
   <rtexprvalue>true</rtexprvalue>
</attribute>

这段声明指出了属性attr1的值可以在运行时刻决定。下面的isValid()方法检查了属性attr1的值是否为Boolean类型。由于属性attr1的值可以在运行时刻决定,isValid()必须检查标记的使用者是否提供了运行时刻的值。

public class TwaTEI extends TagExtraInfo {
   //检验属性值
   public boolean isValid(Tagdata data) {
      Object o = data.getAttribute("attr1");

      if (o != null && o != TagData.REQUEST_TIME_VALUE) {
         if (o.toLowerCase().equals("true") ||
            o.toLowerCase().equals("false") )
            return true;
         else
            return false;
      }
      else
         return true;
   }
}

43.3.3.5. 带标记体的标记

处理带有标记体的标记处理类有两种不同的实现方式,实现方式的选择取决于是否需要和标记体交互。需要交互意味着标记处理类可以读取和更改标记体的内容。

标记处理类不与标记体交互

如果不需要和标记体交互,标记处理类应该实现Tag接口或继承TagSupport类。doStartTag返回EVAL_BODY_INCLUDE,代表标记体需要执行,否则返回SKIP_BODY。

如果标记处理类需要反复的执行标记体,应该实现IterationTag接口或继承TagSupport类。doStartTag和doAfterBody方法返回EVAL_BODY_AGAIN,确定标记体需要再次执行。

标记处理类和标记体交互

如果需要和标记体交互,标记处理类必须实现BodyTag接口或继承BodyTagSupport类。标记处理类一般通过实现doInitBody和doAfterBody方法来与JSP页面的实现servlet传递过来的标记体内容交互。

BodyContent支持几种读取和写入的方法。标记处理类使用BodyContent的getString或getReader方法读取标记体的内容信息,使用writeOut(out)方法向输出流写入标记体内容。

如果标记体需要执行,doStartTag方法返回EVAL_BODY_BUFFERED,否则返回SKIP_BODY。

doInitBody方法在BodyContent设置后、标记体执行之前被调用。通常用来完成依赖于标记体内容的初始化操作。

doAfterBody方法在标记体执行后被调用。象doStartTag一样,doAfterBody必须返回指示,确定是否需要继续执行标记体。如果需要再次执行标记体,返回EVAL_BODY_BUFFERED,否则doAfterBody返回SKIP_BODY。

标记处理类在release方法中重新设置状态,释放所有资源。

类型概览
处理类不与标记体交互
  • 实现Tag或继承TagSupport

  • doStartTag返回EVAL_BODY_INCLUDE或SKIP_BODY

  • doAfterBody返回 EVAL_BODY_AGAIN或SKIP_BODY

处理类和标记体交互
  • 实现 BodyTag或继承BodyTagSupport

  • doInitBody

  • doAfterBody返回EVAL_BODY_BUFFERED或SKIP_BODY

  • release

下面的例子读取了标记体的内容,然后传递给对象作为SQL语句执行。因为标记体不需要再次执行,doAfterBody返回SKIP_BODY。

public class QueryTag extends BodyTagSupport {

   public int doAfterBody() throws JspTagException {
      BodyContent bc = getBodyContent();

      // 得到标记体内容
      String query = bc.getString();
      // 清除
      bc.clearBody();
      try {
         //作为 SQL 语句执行
         Statement stmt = connection.createStatement();
         result = stmt.executeQuery(query);
      } catch (SQLException e) {
         throw new JspTagException("QueryTag: " +
             e.getMessage());
      }

      return SKIP_BODY;
   }
}

对于包含标记体的标记,开发人员必须在body-content指定标记体的类型。

<body-content>JSP|tagdependent</body-content>

包含扩展和标准标记、脚本元素、HTML文本的标记体归类为JSP,所有的其他类型归类为tagdependent 。

43.3.3.6. 定义脚本变量的标记

标记处理类能够创建和设置对象,然后通过页面运行环境(context)关联到脚本变量。使用pageContext.setAttribute(name,value, scope) 或pageContext.setAttribute(name, value)来完成这种关联。一般通过扩展标记的属性传递脚本变量的名称,然后可以调用属性的get方法得到这个名称。通常的处理过程是标记处理类得到脚本变量,执行一些处理,然后使用pageContext.setAttribute(name,object) 设置脚本变量值。

JSP页面包含了能够定义脚本变量的标记,在它转换成servlet的阶段,Web容器产生定义脚本变量、设置脚本变量到对象引用的代码。为了产生这些代码,Web容器需要确定脚本变量的信息:

  • 变量名

  • 变量类型

  • 变量引用新的对象或已存在的对象

  • 变量的可用范围

有两种方式可以提供这些信息:

  • 在标记库描述符中定义variable元素

  • 定义标记扩展信息类(TagExtraInfo )并在标记库描述符中定义tei-class元素

variable元素使用简单,但缺少一些弹性。variable元素包含下面的子元素:

  • name-given:给定常量作为变量名

  • name-from-attribute:属性的名称,属性值作为变量名

name-given和name-from-attribute之中一个是必须的。其他子元素是可选的:

  • variable-class:变量的类型,缺省是java.lang.String

  • declare:变量是否关联到新的对象,缺省是true

  • scope:变量的作用范围,缺省是NESTED

下面列出所有的变量作用范围。

可用性方法
NESTED开始和结束标记之间doInitBody 、doAfterBody 、doStartTag
AT_BEGIN从开始标记到页面结束doInitBody、doAfterBody、doEndTag 、doStartTag
AT_END从结束标记到页面结束doEndTag

开发人员可以继承javax.servlet.jsp.TagExtraInfo定义标记的扩展信息类。TagExtraInfo必须实现getVariableInfo方法,返回VariableInfo对象数组,包含了确定脚本变量的所有信息。例如下面的代码:

public class DefineTei extends TagExtraInfo {

   public VariableInfo[] getVariableInfo(TagData data) {
      String type = data.getAttributeString("type");

      if (type == null)
         type = "java.lang.Object";

      return new VariableInfo[] {
         new VariableInfo(data.getAttributeString("id"),
            type,
            true,
            VariableInfo.AT_BEGIN)
      };
   }
}

VariableInfo对象构造函数的参数为:脚本变量名称、类型、是否为新的变量和变量的作用范围。另外,需要在标记库描述符中定义tei-class元素指向扩展信息类:

<tei-class>com.apusic.test.DefineTagTei</tei-class>

43.3.3.7. 协作标记

标记可以通过共享对象进行协作。JSP技术支持两种形式的对象共享。

  • 使用page context命名和保存对象。page context在JSP页面和标记处理类中都是内置对象。

  • 对象由一组嵌套标记中的封装标记创建,在所有内部标记中也可用。这种方式的好处是使用了私有的对象命名空间,可以减少潜在的命名冲突。

访问由封装标记创建的对象,标记处理类必须首先通过静态方法TagSupport.findAncestorWithClass(from,class) 或TagSupport.getParent方法获得封装标记。

43.3.4. 标记处理类如何被调用

Tag接口定义了标记处理类和JSP页面的实现servlet之间的基础协议。JSP页面的实现servlet会按照一定的顺序去调用标记处理类的方法,典型的调用顺序为:

调用tag handler方法的典型顺序

BodyTag接口扩展了Tag ,定义了处理标记体的附加方法。

  • setBodyContent:创建BodyContent,加入tag handler

  • doInitBody:执行标记体之前调用

  • doAfterBody:执行标记体之后调用

JSP页面的实现servlet调用标记处理类的方法的典型顺序为:

调用 tag handler 方法的典型顺序