(從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 代碼中插入業務邏輯是如此的容易。