(从csdn的blog上同步过来)
(本文发于java emag第一期)

关于 Template JSP 的起源还要追述到 Web 开发的远古年代,那个时候的人们用 CGI 来开发 web 应用,在一个 CGI 程序中写 HTML 标签。

在这之后世界开始朝不同的方向发展: sun 公司提供了类似于 CGI servlet 解决方案,但是无论是 CGI 还是 servlet 都面对同一个问题:在程序里写 html 标签,无论如何都不是一个明智的解决方案。于 sun 公司 1999年推出了JSP技术。 而在另一个世界里,以 PHP ASP 为代表的 scriptlet 页面脚本技术开始广泛应用。

不过即便如此,问题并没有结束,新的问题出现了:业务和 HTML 标签的混合,这个问题不仅导致页面结构的混乱,同时也使代码本身难以维护。

于是来自起源于 70 年代后期的 MVC 模式被引入开发。 MVC 的三个角色: Model ——包含除 UI 的数据和行为的所有数据和行为。 View 是表示 UI 中模型的显示。任何信息的变化都由 MVC 中的第三个成员来处理——控制器

在之后的应用中,出现了技术的第一次飞跃:前端的显示逻辑和后端的业务逻辑分离, COM 组件或 EJB CORBA 用于处理业务逻辑, ASP JSP 以及 PHP 被用于前端的显示。这个就是 Web 开发的 Model 1 阶段(页面控制器模式)。

不过这个开发模式有很多问题:

1.       页面中必须写入 Scriptlet 调用组件以获得所必需的数据。

2.       处理显示逻辑上 Scriptlet 代码和 HTML 代码混合交错。

3.       调试困难。 JSP 被编译成 servlet ,页面上的调试信息不足以定位错误。

这一切都是因为在 Model 1 中并没有分离视图和控制器。完全分离视图和控制器就成了必须。这就是 Model 2 。它把 Model 1 中未解决的问题——分离对组件(业务逻辑)的调用工作,把这部分工作移植到了控制器。现在似乎完美了,不过等等,原来的控制器从页面中分离后,页面所需的数据怎么获得,谁来处理页面显示逻辑?两个办法: 1. 继续利用 asp php 或者 jsp 等机制,不过由于它们是运行在 web 环境下的,他们所要显示的数据(后端逻辑产生的结果)就需要通过控制器放入 request 流中; 2. 使用新手法——模板技术,使用独立的模板技术由于脱离的 web 环境,会给开发测试带来相当的便利。至于页面所需数据传入一个 POJO 就行而不是 request 对象。

模板技术最先开始于 PHP 的世界,出现了 PHPLIB Template FastTemplate这两位英雄。不久模板技术就被引入到java web开发世界里。目前比较流行的模板技术有:XSTLVelocity JDynamiTe Tapestry等。另外因为JSP技术毕竟是目前标准,相当的系统还是利用JSP来完成页面显示逻辑部分,在Sun公司的JSTL外,各个第三方组织也纷纷推出了自己的Taglib,一个代表是struts tablib

模板技术从本质上来讲,它是一个占位符动态替换技术。一个完整的模板技术需要四个元素: 0. 模板语言, 1. 包含模板语言的模板文件, 2. 拥有动态数据的数据对象, 3. 模板引擎。以下就具体讨论这四个元素。(在讨论过程中,我只列举了几个不同特点技术,其它技术或有雷同就不重复了)

模板语言包括:变量标识和表达式语句。根据表达式的控制力不同,可以分为强控制力模板语言和弱控制力模板语言。而根据模板语言与 HTML 的兼容性不同,又可以分为兼容性模板语言和非兼容性模板语言。

模板语言要处理三个要点:

1. 标量标记。把变量标识插入 html 的方法很多。其中一种是使用类似 html 的标签;另一种是使用特殊标识,如 Velocity 或者 JDynamiTe ;第三种是扩展 html 标签,如 tapestry 。采用何种方式有着很多考虑,一个比较常见的考虑是“所见即所得”的要求。

2. 条件控制。这是一个很棘手的问题。一个简单的例子是某物流陪送系统中,物品数低于一定值的要高亮显示。不过对于一个具体复杂显示逻辑的情况,条件控制似乎不可避免。当你把类似于 <IF condition=”$count < 40”><then><span class=”highlight”>count </span></then></IF> 引入,就象我们当初在 ASP PHP 中所做得一样,我们将不得不再一次面对 scriptlet 嵌入网页所遇到的问题。我相信你和我一样并不认为这是一个好得编写方式。实际上并非所有的模板技术都使用条件控制,很多已有的应用如 PHP 的以及我曾见过一个基于 ASP.NET 的应用,当然还有 Java JDynamiTe 。这样网页上没有任何逻辑,不过这样做的代价是把高亮显示的选择控制移交给编程代码。你必需做个选择。也许你也象我一样既不想在网页中使用条件控制,也不想在代码中写 html 标记,但是这个显示逻辑是无可逃避的(如果你不想被你的老板抄鱿鱼的话),一个可行的方法是用 CSS ,在编程代码中决定采用哪个 css 样式。特别是 CSS2 技术,其 selector 机制,可以根据 html 类型甚至是 element attributes apply 不同的样式。

3. 迭代(循环)。在网页上显示一个数据表单是一个很基本的要求,使用集合标签将不可避免,不过幸运的是,它通常很简单,而且够用。特别值得一提的是 PHP 的模板技术和 JDynamiTe 技术利用 html 的注释标签很简单的实现了它,又保持了“所见既所得”的特性。

下面是一些技术的比较:

Velocity

变量定义:用 $ 标志

表达式语句:以 # 开始

强控制语言:变量赋值: #set $this = "Velocity"

            外部引用: #include ( $1 )

            条件控制: #if …. #end

非兼容语言

JDynamiTe

变量定义:用 {} 包装

表达式语句:写在注释格式( <!--  à )中

弱控制语言

兼容语言

XSLT

变量定义: xml 标签

表达式: xsl 标签

强控制语言:外部引用: import include

            条件控制: if   choose…when…otherwise

非兼容语言

Tapestry

采用 component 的形式开发。

变量定义(组件定义):在 html 标签中加上 jwcid

表达式语句: ognl 规范

兼容语言

 

模板文件指包含了模板语言的文本文件。

模板文件由于其模板语言的兼容性导致不同结果。与 HTML 兼容性的模板文件只是一个资源文件,其具有良好的复用性和维护性。例如 JDynamiTe 的模板文件不但可以在不同的项目中复用,甚至可以和 PHP 程序的模板文件互用。而如 velocity 的非兼容模板文件,由于其事实上是一个脚本程序,复用性和可维护性大大降低。

模板文件包含的是静态内容,那么其所需的动态数据就需要另外提供。根据提供数据方式的不同可以分为 3 种:

1.       Map :利用 key/value 来定位。这个是最常见的技术。如 velocity VelocityContext 就是包含了 map 对象。

Example.vm

Hello from $name in the $project project.

 

Example.java

VelocityContext context = new VelocityContext();

context.put("name", "Velocity");

context.put("project", "Jakarta");

 

2.       DOM :直接操作 DOM 数据对象,如 XSLT 利用 XPath 技术。

3.       POJO :直接利用反射取得 DTO 对象,利用 JavaBean 机制取得数据。如 Tapestry

模板引擎的工作分为三步:

1. 取得模板文件并确认其中的模板语言符合规范。

比如 velocity ,确定 #if 有对应得 #end 等。 Xml xslt 的模型中, xml 文件标签是否完整等。在完成这些工作后,模板引擎通常会把模板文件解析成一颗节点树(包含模板文件的静态内容节点和模板引擎所定义的特殊节点)。

2. 取得数据对象。

         该数据对象一般通过程序传递引用实现。现有的大量框架在程序底层完成,处理方式也各自不同,有两种技术分别为推技术和拉技术。推技术: controller 调用 set 方法把动态数据注入,模板引擎通过 get 方法获得,典型代表: Struts ;拉技术:模板引擎根据配置信息,找到与 view 对应的 model ,调用 model get 方法取得数据,典型代表: Tapestry

3. 合并模板文件(静态内容)和数据对象(动态内容),并生成最终页面。

         合并的机制一般如下,模板引擎遍历这颗节点树的每一个节点,并 render 该节点,遇到静态内容节点按正常输入,遇到特殊节点就从数据对象中去得对应值,并执行其表达式语句(如果有的话)。

以下详细说明:

Velocity

Template template = Velocity.getTemplate("test.wm");

Context context = new VelocityContext();

context.put("foo", "bar");

context.put("customer", new Customer());

template.merge(context, writer);

当调用 Velocity.getTemplate 方法时,将调用 ResourceManger 的对应方法。

ResourceManger 先查看该模板文件是否在 cache 中,如果没有就去获取,生成 resource 对象并调用 process() 方法,确定该模板是否有效,如果有效,则在内存中生成一个 Node 树。

当调用 template.merge() 时,遍历这颗 Node 树,并调用每个 Node render 方法。对于模板中的变量和对象 Node ,还将调用 execute() 方法,从 context 中取得 value

   注: ResourceManger runtime\resource 包下, Node runtime\parser\node 包下

Tapestry

Tapestry 比较麻烦,先介绍一下 http 请求的处理过程。

httprequest 请求到达时。该请求被 ApplicationServlet 捕获,随后 ApplicationServlet 通过 getEngine 取到对应的 Engine ,通过该 engine getService 拿到对应的 service ,调用其 service 方法执行 http 请求。

每个 service 通过 RequestCycle 对象的 getPage 方法取得 Page 对象,并将其设置为该 Cycle 对象的 active Page 。之后 service 调用 renderResponse 方法执行输出。

renderResponse 调用 page getResponseWriter(output) 取得 writer 对象,并把它传给 cycle.renderPage(writer) 方法,该方法调用 page renderPage 方法。

Page 执行 renderPage 时,首先判断是否有 listener 的请求,如果有则处理 listener 请求;然后调用 BaseComponentTemplateLoader process 方法把模板文件载入并形成一个 component 节点树,依次执行节点的 renderComponent 方法。

每个 component 对象将通过 ongl 的机制取得对象属性。并把该值写入输入流。

例如: insert component

protected void renderComponent(IMarkupWriter writer, IRequestCycle cycle) {

        if (cycle.isRewinding())

            return;

        Object value = getValue();

        if (value == null)

            return;

        String insert = null;

        Format format = getFormat();

        if (format == null) {

            insert = value.toString();

        }

        else{

            try{

                insert = format.format(value);

            }

            catch (Exception ex) {

                throw new ApplicationRuntimeException(

Tapestry.format("Insert.unable-to-format",value),this, getFormatBinding().getLocation(), ex);

            }

        }

        String styleClass = getStyleClass();

        if (styleClass != null) {

            writer.begin("span");

            writer.attribute("class", styleClass);

            renderInformalParameters(writer, cycle);

        }

        if (getRaw())

            writer.printRaw(insert);

        else

            writer.print(insert);

        if (styleClass != null)

            writer.end(); // <span>

    }

getValue 为取得 insert value 属性。

 

技术分析

       技术:

       JSP ,一个伪装后的 servlet web server 会对任何一个 jsp 都生成一个对应 jsp 类,打开这个类,就会发现, jsp 提供的是一个代码生成机制,把 jsp 文件中所有的 scriptlet 原封不动的 copy 生成的 jsp 类中,同时调用 println 把所有的 html 标签输出。

Test.jsp

<html>

<head><title>jsp test</title></head>

<body>

<table width="226" border="0" cellspacing="0" cellpadding="0">

   <tr><td><font face="Arial" size="2" color="#000066">

             <b class="headlinebold">The jsp test file</b>

       </tr></td> </font>  

</table>

<body>

</html>

Test_jsp.java:

package org.apache.jsp;

import javax.servlet.*;

import javax.servlet.http.*;

import javax.servlet.jsp.*;

import org.apache.jasper.runtime.*;

 

public class Test _jsp extends HttpJspBase {

  private static java.util.Vector _jspx_includes;

  public java.util.List getIncludes() {

    return _jspx_includes;

  }

  public void _jspService(HttpServletRequest request, HttpServletResponse response)

        throws java.io.IOException, ServletException {

    JspFactory _jspxFactory = null;

    javax.servlet.jsp.PageContext pageContext = null;

    HttpSession session = null;

    ServletContext application = null;

    ServletConfig config = null;

    JspWriter out = null;

    Object page = this;

    JspWriter _jspx_out = null;

 

    try {

      _jspxFactory = JspFactory.getDefaultFactory();

      response.setContentType("text/html;charset=ISO-8859-1");

      pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true);

      application = pageContext.getServletContext();

      config = pageContext.getServletConfig();

      session = pageContext.getSession();

      out = pageContext.getOut();

      _jspx_out = out;

 

      out.write("<html>\r\n");

      out.write("<head><title>jsp test</title></head> \r\n");

      out.write("<body>\r\n");

      out.write("<table width=\"226\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\r\n   ");

      out.write("<tr><td><font face=\"Arial \" size=\"2\" color=\"#000066\"> \r\n\t      ");

      out.write("<b class=\"headlinebold\">The jsp test file");

      out.write("</b>\r\n\t      ");

      out.write("</tr></td></font>\r\n\t ");

      out.write("</table>\r\n");

      out.write("<body>\r\n");

      out.write("</html>");

    } catch (Throwable t) {

      out = _jspx_out;

      if (out != null && out.getBufferSize() != 0)

        out.clearBuffer();

      if (pageContext != null) pageContext.handlePageException(t);

    } finally {

      if (_jspxFactory != null) _jspxFactory.releasePageContext(pageContext);

    }

  }

}

 

       技术:

Taglib 作为 jsp 之上的辅助技术,其工作本质依托与 jsp 技术,也是自定义标签翻译成 java 代码,不过这次和 jsp 略有不同,它还要经过几个过程。

先来看一下,实现一个 tag 2 个要点:

1. 提供属性的set方法,此后这个属性就可以在jsp页面设置。以jstl标签为例 c:out value=""/,这个value就是jsp数据到tag之间的入口。所以tag里面必须有一个setValue方法,具体的属性可以不叫value。例如setValue(String data){this.data = data;}。这个“value”的名称是在tld里定义的。取什么名字都可以,只需tag里提供相应的set方法即可。

2. 处理 doStartTag doEndTag 。这两个方法是 TagSupport提供的。还是以c:out value=""/为例,当jsp解析这个标签的时候,在“<”处触发 doStartTag 事件,在“>”时触发 doEndTag 事件。通常在 doStartTag 里进行逻辑操作,在 doEndTag 里控制输出。

  在处理tag的时候:

  0. tagPool中取得对应tag

1.     为该tag设置页面上下文。

2.     为该tag设置其父tag,如果没有就为null

3.     调用setter方法传入标签属性值tag,如果该标签没有属性,此步跳过。

4.     调用doStartTag方法,取的返回值。

5.     如果该标签有body,根据doStartTag返回值确定是否pop该标签内容。如果要popbody,则:setBodyContent(),在之后,doInitBody()。如果该标签没有body,此步跳过。

6.     调用doEndTag()以确定是否跳过页面剩下部分。

7.     最后把tag类返还给tagPool

tag 类为:

package my.customtags;

import javax.servlet.jsp.JspWriter;

import javax.servlet.jsp.PageContext;

import javax.servlet.jsp.tagext.TagSupport;

 

public class Hidden extends TagSupport{

    String name;  

    public Hidden(){    name = "";    }

    public void setName(String name){    this.name = name;    }

    public void release(){   value = null;    }

    public int doStartTag(){   return EVAL_BODY_INCLUDE }

public int doEndTag() throws JspTagException{

   try{   pageContext.getOut().write(", you are welcome");   }

   catch(IOException ex){   throw new JspTagException("Error!");    }

   return EVAL_PAGE;

}

}

 

Jsp 页面:

<my:hidden name="testname"/>

 

生成的jsp代码:

my.customtags.Hidden _jspx_th_my_hidden_11 = (my.customtags.Hidden) _jspx_tagPool_my_hidden_name.get(my.customtags.Hidden.class);

_jspx_th_my_hidden_11.setPageContext(pageContext);

_jspx_th_my_hidden_11.setParent(null);

_jspx_th_my_hidden_11.setName("testname");

int _jspx_eval_my_hidden_11 = _jspx_th_my_hidden_11.doStartTag();

if (_jspx_th_my_hidden_11.doEndTag() == javax.servlet.jsp.tagext.Tag.SKIP_PAGE)

   return true;

_jspx_tagPool_my_hidden_name.reuse(_jspx_th_my_hidden_11);

return false;

 

   Taglib 技术提供两个机制,Bodynon-Body导致了taglib的出现了两个分支:Display TagControl Tag, 前者在java code中嵌入了html标签,相当与一个web component,而后者则是另一种模板脚本。

 

       1. 技术学习难易度

模板技术。使用模板技术,第一点就是必须学习模板语言,尤其是强控制的模板语言。于是模板语言本身的友好性变的尤为重要。以下依据友好性,表现力以及复用性三点为主基点比较了一下几种模板技术。

Velocity

Turbine 项目( http://jakarta.apache.org/Turbine )采用了 velocity 技术。

1.       友好性不够。理由: 强控制类型,出现页面显示控制代码和 html 混合。与 Html 的不兼容,无法所见即所得。遇到大的 HTML 页面,从一个 #if ”找到对应的 #end ”也是很痛苦的一件事情。

2.       表现力强。理由:强控制语言。

3.       复用性弱。理由:模板脚本和页面代码混合。

XSLT

Cocoon 项目( http://cocoon.apache.org/ )采用 XML + XSLT 的方法。 CSDN 社区也是采用此方案。

1.       内容和显示风格分离,这点 XSLT 做的最好。

2.       速度慢。理由: XSLT 的使用 XPath ,由于是要解析 DOM 树,当 XML 文件大时,速度很慢。

3.       友好性不够。理由:由于没有 HTML 文件,根本看不到页面结构、显示风格和内容。 XSL 语法比较难以掌握,由于没有“所见即所得”编辑工具,学习成本高。

4.       表现力强。理由:强控制语言。

5.       复用性弱。理由: xsl 标签和 html 标签混合。

JDynamiTe

1.       表现力中等。理由:弱控制语言。

2.       友好性强。理由:所见即所得的效果。在模板件中的 ignore block 在编辑条件下可展示页面效果,而在运行中不会被输出。

3.       复用性强。理由:利用 html 标签。

Tapestry

1.       友好性中等。理由:整个 Tapestry 页面文件都是 HTML 元素。但是由于 component 会重写 html 标签,其显示的样子是否正确,将不预测。

2.       表现力强。理由:强控制语言。

3.       复用性强。理由:扩展了 HTML 元素的定义。

 

 

JSP 中大量的使用 TagLib ,能够使得 JSP 的页面结构良好,更符合 XML 格式,而且能够重用一些页面元素。但 TagLib 的编译之后的代码庞大而杂乱。 TabLib 很不灵活,能完成的事情很有限。 TabLib 代码本身的可重用性受到 TagSupport 定义的限制,不是很好。 另外是,我不得不承认的一件事是, TagLib 的编写本身不是一件愉快的事情,事实我个人很反对这种开发方式。

 

       2. 技术使用难易度

       模板技术:模板技术本身脱离了 Web 环境,可以在不启动 Web server 得情况下进行开发和测试,一旦出错详细的信息易于错误的定位。由于模板引擎的控制,页面中将只处理显示逻辑(尽管其可能很复杂)

       JSP 技术:工作在 Web 环境下,开发测试一定要运行 web server 。此外,一些 TagLib 能够产生新的标签,页面的最终布局也必须在 web 环境下才可以确定。测试时出错信息不明确,特别是 TagLib 得存在,极不容易定位。由于其本质是程序,很容易在其中写入业务逻辑,甚至于数据库连接代码,造成解耦的不彻底。

 

3. 总结

模板技术更加专注于页面的显示逻辑,有效帮助开发人员分离视图和控制器。在学习,开发和测试都更加容易。

JSP 技术本身是一个早期的技术,本身并没有提出足够的方式来分离视图和控制器。相反,我认为其本身是鼓励开发人员不做解耦,因为在 JSP 代码中插入业务逻辑是如此的容易。