(从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开发世界里。目前比较流行的模板技术有:XSTL,Velocity, 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该标签内容。如果要pop其body,则: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 技术提供两个机制,Body和non-Body导致了taglib的出现了两个分支:Display Tag和Control 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 代码中插入业务逻辑是如此的容易。