爱心技术专栏专题

使用定制标记控制JSP页面

摘录:java基础 来源:java基础 加入时间:2007年03月08日
摘要:
使用定制标记控制JSP页面

JavaServer Pages 技术对于 Web 开发人员起着至关重要的作用,但是许多人还没有充分地利用其全部功能。作者 Jeff Wilson 是电子商务设计师(并且是颇受尊敬的 DragonSlayers 小组的成员),他向您演示了如何定制 JSP 标记以更充分地挖掘这一技术的潜能。通过使用他在本文中详…

转载:转载请保留本信息,本文来自
http://www.51dibs.com
/html/2006/article/info8/a_508f5e58bccf7c3d.htm

使用定制标记控制JSP页面

站点:爱心种子小博士 关键字:使用定制标记控制JSP页面

   
使用定制标记控制JSP页面
JavaServer Pages 技术对于 Web 开发人员起着至关重要的作用,但是许多人还没有充分地利用其全部功能。作者 Jeff Wilson 是电子商务设计师(并且是颇受尊敬的 DragonSlayers 小组的成员),他向您演示了如何定制 JSP 标记以更充分地挖掘这一技术的潜能。通过使用他在本文中详细论述的技术,您可以向您的 JSP 添加更复杂的逻辑,更牢固地控制数据显示以及在标记之间共享数据 — 所有这些都无须要求前端 Web 开发人员学会如何编写 Java 代码。文章包含样本标记和标记处理类以使您了解这一技术的工作原理。请通过单击文章顶部或底部的讨论来在论坛上同作者及其他读者分享您对本文的想法。

如果您确实参加过 Web 开发,那么您会深有感触:当今基于 Web 的应用程序比以前需要更多动态生成的内容及个性化的数据。构造一个用户友好的界面意味着前端开发人员不仅得精通可视化设计的技巧,而且还要精通管理及利用内容流。

定制 JSP 标记使得那些前端开发人员有了控制数据在后端 Java 组件内的处理的方法,而在 JSP 页面内无需任何 Java 代码。

在本文中,我们将把精力集中在定制标记如何相互通信,以及将它们结合在一起是如何增加可重用性和灵活性的。我们也将涉及几个示例。

定制标记简要概述
比起象 <jsp:useBean .http://www.kissjava.com/doc/javamore/struts/images/h00/h39/CODE> 和 <jsp:getProperty .http://www.kissjava.com/doc/javamore/struts/images/h00/h39/CODE> 之类的典型 JSP 标记,定制标记更先进和更灵活。定制标记同典型的 JSP 标记比起来的一个主要优点是:通过使用定制标记,JSP 开发人员可以通过将数据放在标记属性内或是开始标记和结束标记之间来传递输入。

定制 JSP 标记由三部分组成:

  • 使用该标记的 JSP 页面
  • 标记处理程序,它是一个处理标记的 Java 类
  • 标记库描述符,它是一个 XML 文件,它将标记聚合成一个库并描述了每个标记的细节,诸如其属性、标记句柄名称和短名称等等

可以配置定制标记使之根据标记属性提供的输入运行其过程,而无须在 JSP 页面内嵌入 Java 代码。

让我们看一个示例。清单 1 演示了一个购物车标记,它为用户挑选的每一样产品生成一个表行:

清单 1. 购物车标记


<table width="100%" border="0">
  <user:getUserShoppingList userId="13">
  <tr>
    <td>
    <a href="../../../../servlet/productDataServlet">$_productName</a>
    </td>
    <td>$_productDescription</td>
  </tr>
  </user:getUserShoppingList>
</table>

在这个例子中,标记处理程序希望将 HTML 和要传入的用户标识作为由此产生的输出的模板(这里是一个表行)。标记处理程序循环过程中得到的产品的实际值将会替换各种 $_product... 引用 — 标记处理程序是实现标记的 Java 类。

这演示了定制标记的另外一个主要优点:在将数据发回客户机之前,Java 程序员无须知道如何格式化数据。此外,当开发需要内容类似但格式不同的数据的新页面时,或者当前页面的外观发生改变时,无须更新 Java 组件。

控制显示逻辑,而不仅仅是样式
定制标记的另外一个非常重要的优点是其在同一个页面内同其它标记通信的能力。使用传统 JSP 标记,开发人员可以设置特性来控制 JavaBean 组件的行为,但是 bean 仅仅只执行在其自身上所执行的操作。通过将过程分解成更小的组件,JSP 开发人员可以混合和匹配定制标记,以为进一步控制动态内容构建更复杂的过程。

将一个定制标记的输出作为另一个定制标记的输入增加了标记的可重用性。例如,在我们的前一个示例里,用户购物车标记存有关于购买的产品的所有信息。更好的设计可以让用户标记只含有产品标识,而让另一个标记 — 产品标记 — 管理所需的产品数据。一旦将收集产品详细信息的过程分离开来,您就有了一个产品标记,它可以与用于其它用途的其它标记一起使用。

这就产生了一个控制检索到的动态数据的面向对象方法 — 前端开发人员可以使用这一方法而无须懂得任何编程知识。

清单 2 中的代码演示了这一方法的实际使用。而且,请注意用户的购物列表标记实现了另外一个称为 error:setErrorTemplate 的标记。如果在这一示例中没有出现任何产品,我们可能希望显示一条错误消息。很容易发现,产品列表所需的含两个列的表不那么适合于错误消息。

清单 2. 面向对象方法


<table width="100%" border="0">
  <tr class="headerRow">
    <td>Product Name</td>
    <td>Product Description</td><tr>
  </tr>
  <user:getUserShoppingList userId="13">
    <products:getProductData productId="$_productId"/>
    <tr>
      <td>
        <a href="../../../../servlet/productDataServlet$_productId">$_productName</a>
      </td>
      <td>$_productDescription</td>
    </tr>
    <error:setErrorTemplate>
    <tr class="errorRow">
        <td colspan="2">You have nothing in your shopping cart...</td>
    </tr>
    </error:setErrorTemplate>
  </user:getUserShoppingList>
</table>

可以把标记处理程序类设计成在某种预定义环境下处理交替格式化。对于 JSP 开发人员如何能够不在 JSP 页面内使用 if 语句或其它 Java 代码就能控制数据的逻辑流,这是一个极好的示例。使用定制标记,JSP 开发人员可以决定如何确定显示什么,而不仅仅是如何显示。

在另一种情况下,产品数据标记可与用于列出产品的其它标记一起重用。在清单 3 中,请注意不仅另一个标记(<products:getProductData category="fitness">)启动了列表,而且由于在标记中传入了不同的 HTML,对输出的格式化也不同。

清单 3. 同产品列表一起使用的产品数据标记


<table width="100%" border="0">
  <products:getProductList category="fitness">
    <products:getProductData productId="$_productId"/>
    <tr>
      <td rowspan="2">
        <a href="../../../../servlet/productDataServlet$_productId"><img 
        src="$_productImage" border="0"></a></td>
      <td>
      <a href="../../../../servlet/productDataServlet$_productId">$_productName</a>
      </td>
    </tr>
    <tr>
      <td>$_productDescription: $_productPrice</td>
    </tr>
    <error:setErrorTemplate>
    <tr>
      <td colspan="2" class="errorRow">Sorry, no products in this category...</td>
    </tr>
    </error:setErrorTemplate>
  </user:getUserShoppingList>
</table>

标记通信的方法:优点和示例
定制标记可以以几种方法相互引用和共享数据。合适的方法当然取决于其情况。

嵌套标记
当一个标记完全被另一个标记包围时,就说它是嵌套的。

<outer:tag><inner:tag/></outer:tag>

将一个标记置于另一个标记之内无需特殊的设置或编码。可以在一个地方嵌套标记也可以在另一个地方嵌套标记本身。当然,有些标记设计成嵌套在其它标记内,但是却不需要任何东西来将标记显式地声明成是可嵌套的。

可以将 HTML 表、表行以及表单元格标记看成是嵌套标记。表标记中共享数据的示例是表的背景颜色(bgcolor 属性)。如果在表标记 <table bgcolor="blue">...</table> 内设置背景,除非个别标记重设了 bgcolor 属性(例如,<table bgcolor="blue">...<td bgcolor="red">...</td>...</table>),所有行和单元格都将被设置成蓝色。

在被求值的内部标记的最基本的实现中,该标记可能仅仅只是外部标记的主体输入。然而,嵌套的标记可能也会引用包含它的标记(诸如父标记和祖父标记),允许被链接的类相互调用对方的方法和特性。这样,子标记和父标记就可以共享数据。

使用如下两种方法之一,嵌套标记就可以引用祖先标记:

  • TagSupport.getParent():返回父标记;也就是包围该标记的最里层标记。

  • TagSupport.findAncestorWithClass(from,class):在特定的标记层次结构未知或必须预先设置时所用。findAncestorWithClass(from,class) 的参数分别表示从什么类开始以及搜索什么类。例如,在 HTML 表标记层次结构中,访问表标记的表单元格标记将类似于下面的样子:

    TableTag table = (TableTag)findAncestorWithClass(this, TableTag.class);

如果标记处理程序是指定的类(这里是 TableTag.class),其标记内没有包含当前标记,或者调用了 getParent() 但当前标记根本没有父标记,那么这两个方法都会返回 null。

使用标识引用标记
另外一个共享数据的方法是使用一个标识注册类,随后由另一个标记的处理程序类检索该标识。使用这一方法,如果没有对标记进行专门编程使之接受标识,则 JSP 开发人员不能简单地在任何定制标记中设置标识。

为了这一特别的目的,已经在 TagSupport 内声明了标识特性,并且该特性可以为任何标记处理程序类所用。然而,要使用标识特性来存储对象以供其它标记访问,必须采取两个步骤:

  1. 必须在标记库描述符中指定标识属性(可以将所需的节点设置成为 true 或 false)。

  2. 标记处理程序必须专门将其自身设置成 pageContext 的一个属性。
  • 如果不容易嵌套标记,则可能需要使用注册的标识共享标记对象。

    让我们重新引用用户的购物车示例。请考虑,下面清单 4 中的 <user:getUserShoppingList .http://www.kissjava.com/doc/javamore/struts/images/h00/h39/CODE> 含有关于购物列表的各种信息,包括一个返回产品标识列表的方法 getProductIds()。在该 JSP 页面的其它地方,<products:getProductData> ... </products:getProductData> 标记将获取产品标识的列表,检索每个产品的产品详细信息并在将它们发回客户机前对它们进行格式化。

    user:getUserShoppingList 标记会执行,并将其自己存储为名为 userShoppingListpageContext 属性。product:getProductData 标记将检索属性值并从 getUserShoppingList 对象调用方法 getProductIds()

    清单 4. getUserShoppingList

    
        <user:getUserShoppingList userId="13" id="userShoppingList"/>
        ...
        <products:getProductData productData="userShoppingList">
           <!-- Some formatting template -->
          ...
        </products:getProductData>
    

    标记 getUserShoppingList 的标记库描述符可能类似清单 5:

    清单 5. getUserShoppingList 库描述符

    
      <tag>
           <name>getUserShoppingList</name>
           <tagclass>com.taglib.UserShoppingListTag</tagclass>
            <bodycontent>JSP</bodycontent>
            <attribute>
                <name>userId</name>
                <required>true</required>
                <rtexprvalue>false</rtexprvalue>
            </attribute>
            <attribute>
               <name>id</name>
               <required>false</required>
               <rtexprvalue>false</rtexprvalue>
            </attribute>
        </tag>
    
    

    一旦 getShoppingList 标记(可能是在 doEndTag() 方法中)完成了其所有的处理,它的标记处理程序将含有下面这行:

    pageContext.setAttribute(getId(),this);

    getId() 检索标记(userShoppingList)中设置的标识,this 引用整个类本身。

    一旦在 pageContext 中存储了 userShoppingList 标记,getProductData 标记处理程序类就可以使用用 productData 属性传递给 userShoppingList 标记的标识来访问它。getProductData 标记的处理程序类将使用该标识(用方法 getProductData() 检索的)以找到 pageContext 中的属性并将其类型强制转换成 UserShoppingListTag 对象。清单 6 中的代码声明一个称为 productListList 并调用用户类方法 getProductIds() 来抽取产品列表。

    清单 6. 抽取产品列表

    
    UserShoppingListTag userShoppingList = 
            (UserShoppingListTag)pageContext.getAttribute(getProductData());
        List productList = (List)userShoppingList.getProductIds();
    

    在页面和会话上下文中引用标记
    存储整个标记对象对于让其它标记自由支配该对象内的属性和方法是有用的。但是,您也可以选择只暴露对象的部分。实际上,前面的示例的局限在于产品标记期望产品列表来自 UserShoppingListTag 对象。

    或许一个较好的方法是让用户标记 getShoppingList 只将产品列表导出到 pageContext 供任何其它标记使用。这样做的好处是其它标记不需要预先知道有关返回产品列表的方法,甚至也不需要知道产生列表的对象。如果存储在 pageContext 中的数据的标签由属性 productData 提供,那么产品标记的标记处理程序将如下所示:

    
    List productList = (List)pageContext.getAttribute(getProductData());
    

    这一技术将适用于购物列表标记以及其它类似于从给定的类别调用销售项目或产品的标记。

    如果数据存储在会话中,那么它将类似于下面的样子:

    
    HttpSession session = pageContext.getSession();
    List productList = (List)session.getAttribute(getProductData());
    

    遍历示例定制标记
    在下面的参考资料部分中,您会发现一个到一组示例定制标记的链接。请下载该代码软件包并查看一下。在本文的最后部分,我将使用这些示例向您演示使用定制标记所带来的好处。

    示例标记概述
    样本标记背后的想法十分简单、直接。总共有七个定制标记、一个 JSP 文件以及一个标记库描述符(.tld 文件)。

    这些标记相互协作以显示三项内容之一:一个欢迎屏幕、一个登录屏幕或者(如果出错的话)一条登录错误消息。(实际的登录过程并不是由标记处理的 — 为了简单起见,我们将通过设置会话和请求变量来模仿该过程,否则需要用 servlet 或 JavaBean 组件来进行设置)。

    如清单 7 所示,有两个主要的标记:getUserDatanestedLogin。第一个标记获取用户,第二个标记根据用户 John Q. Citizen 是否登录来显示适当的 HTML。

    这两个标记代表了一种方法,即一个标记 nestedLogin 可以访问另一个标记 getUserData,后者存储在 pageContext 中。

    nestedLogin 标记还演示了在一个标记内嵌套其它标记的过程;它允许其它标记访问它的方法。三个其它的标记 isLoggedInHTMLnotLoggedInHTMLlogInFailureHTML 代表了三个可能的显示。这三个标记提供对 nestedLogin 标记中的特性的访问;nestedLogin 将确定并显示适当的代码块。

    剩下的两个标记 getUserNamegetLoginError 演示了两种使用嵌套标记的方法:作为简单的主体内容和作为访问祖先标记中方法的方式。它们都不覆盖它们的祖先标记;它们只是从其祖先标记获取数据 — 也就是用户的名称和登录错误(如果设置了这两者的话)。

    清单 7. 示例 JSP 代码

    
    <HTML>
    <HEAD>
    <TITLE>Custom Tag Communication</TITLE>
    </HEAD>
    
    <BODY bgcolor="#ffffff">
    
    <!-- LOAD TAG LIBRARY -->
    <%@ taglib uri="goforit.tld" prefix="goforit" %>
    
    <!-- SET THE USER -->
    <goforit:getUserData id="user"/>
    
    <!-- SET THE LOGIN HTML BASED ON WHETHER OR NOT THE USER IS LOGGED IN -->
    <!-- ONE OF THE NESTED NODES WILL BE DISPLAYED ACCORDINGLY -->
    <goforit:nestedLogin userDataID="user">
    
        <goforit:isLoggedIn>
        <!-- THE HTML IN THIS NODE IS DISPLAYED IF THE USER IS LOGGED IN -->
        </goforit:isLoggedIn>
    
        <goforit:notLoggedIn>
        <!-- THE HTML IN THIS NODE IS DISPLAYED IF THE USER IS NOT LOGGED IN -->
        </goforit:notLoggedIn>
    
        <goforit:loginFailure>
        <!-- THE HTML IN THIS NODE IS DISPLAYED IF THERE WAS A LOGIN ERROR -->
        </goforit:loginFailure>
    
    </goforit:nestedLogin>
    
    </BODY>
    </HTML>
    

    标记库描述符
    清单 8 演示了两个主要标记的描述符。请注意必需的 getUserDataid 属性和 nestedLoginuserDataID 属性。它们用于在 pageContext 中注册用户对象并在其它类中检索该对象。

    清单 8. 标记描述符

      
      <tag>
        <name>getUserData</name>
        <tagclass>com.taglibrarycommunication.taglib.GetUserDataTag</tagclass>   
        <info></info>
        <bodycontent>JSP</bodycontent>    
        <attribute>
          <name>id</name>
          <required>true</required>
          <rtexprvalue>false</rtexprvalue>
        </attribute>     
      </tag>
    
      <tag>
        <name>nestedLogin</name>
        <tagclass>com.taglibrarycommunication.taglib.NestedLoginTag</tagclass>   
        <info></info>
        <bodycontent>JSP</bodycontent>    
        <attribute>
          <name>userDataID</name>
          <required>true</required>
          <rtexprvalue>false</rtexprvalue>
        </attribute>     
      </tag>
    

    标记处理程序
    下面的章节演示了类之间的通信的一些重要方面。

    GetUserData
    这个标记处理程序类检索其它标记使用的用户数据。

    注:因为我们的标记不处理登录过程,所以构造了这些标记来查找存储在会话中的登录用户以及存储在请求中的错误。因为我们实际上没有让人们登录,所以这个标记的开始部分通过将某人设置到对话或将错误设置到请求来模拟三种可能的方案。清单 9 中所示的代码控制这三种方案 — 如果将下面的两行都注释掉,那么就认为用户未登录:

    清单 9. 控制示例方案的代码

    
    // UNCOMMENT TO MIMIC A LOGGED IN USER STORED IN SESSION
    //session.setAttribute("user","John Q. Citizen");
    
    // UNCOMMENT TO MIMIC LOGIN ERROR STORED IN REQUEST
    //pageContext.getRequest().setAttribute("loginError","Password incorrect");
    
    

    注册用户数据的关键在 doStartTag() 方法中:

    清单 10. doStartTag()

    
    public int doStartTag() {
    
        session = pageContext.getSession();
        
        ... SET VARIOUS PROPERTIES BASED ON THE USER ...
    
        // THIS IS THE LINE THAT SAVES THIS CLASS TO pageContext
        pageContext.setAttribute(id,this);
    
        return SKIP_BODY;
    }
    

    NestedLoginTag
    这个类显示在清单 11 中,它根据用户是否登录或者是否出错来确定将三个特性中的哪一个返回给客户机。嵌套在 JSP 页面中的类内的其它标记确定这三个特性的值。NestedLoginTag 通过首先获取在前一个标记中注册到 pageContext 中的用户来确定显示哪个标记。然后它试图检索用户名和任何可能出现的错误。如果这两项都是空白,它就假定用户没有登录。如果有错误消息,无疑出错了。如果设置了用户的名称,那么用户就成功地登录了。

    清单 11. NestedLoginTag

    
    // PULL THE userData OUT OF THE pageContext 
    // WITH THE userDataID SUPPLIED THROUGH THE CUSTOM TAG
    GetUserDataTag userData = 
         (GetUserDataTag) pageContext.getAttribute(getUserDataID());
    
    // SET userName AND loginError FROM VALUES IN userData OBJECT
    setUserName(userData.getUserName());
    SetLoginError(userData.getLoginError());
    
    ...
    
    if (getUserName()!="" &&
        getLoginError()==""){
        // IF userName IS SET PERSON IS LOGGED IN
        pageContext.getOut().print(getIsLoggedInHTML());
    } else {
        if (getLoginError()=="")
            // IF NO userName SET BUT NO loginError SHOW LOGIN
            pageContext.getOut().print(getNotLoggedInHTML());
        else
            // IF loginError SHOW LOGIN AND ERROR
            pageContext.getOut().print(getLogInFailureHTML());
    }
    

    IsLoggedInTag、NotLoggedInTag 和 LogInFailureTag
    清单 12 中的三个标记嵌套在 nestedLogin 内。它们的功能都相似,但却设置不同的 nestedLogin 特性。其主体内容允许 JSP 开发人员访问并设置 nestedLoginisLoggedInHTMLnotLoggedInHTMLlogInFailureHTML 特性。

    清单 12. 访问父标记

    
        // THIS LINE ACCESSES THE PARENT CLASS NestedLoginTag
        NestedLoginTag parent = (NestedLoginTag) getParent();
    
        if (parent != null){
            BodyContent bc = getBodyContent();
            String body = bc.getString();
    
            // SET THE isLoggedInHTML PROPERTY OF THE PARENT CLASS
            // WITH THE BODY SUPPLIED THROUGH THE CUSTOM TAG
            parent.setIsLoggedInHTML(body);
        }
    

    GetUserNameTag 和 GetLoginErrorTag
    清单 13 中的标记仅仅从 pageContext 检索 userData 对象并获取它的一个特性。

    清单 13. 访问存储在 pageContext 中的对象

    
        GetUserDataTag userData = 
            (GetUserDataTag) pageContext.getAttribute(getUserDataID());
    
        ...
    
        if (userData.getUserName() !=null){
            pageContext.getOut().print(userData.getUserName());
        }
    

    结束语
    JSP 页面将客户端显示与服务器端逻辑分开,并让可能不是 Java 程序员的 Web 设计人员能够充分利用 Java 技术的功能。通过使用定制标记,您可以给予从事 Web 应用程序各层开发的开发人员更多选择,并为 Web 开发人员提供一种面向对象的方法,以鼓励重用代码模块和标记。一旦您检验了本文所包含的样本标记库,您应该可以开始在您自己的应用程序内使用定制标记了。


  • 客户服务中心信箱:[email protected] [email protected] 网站地图

    声明

    合作伙伴: