Java 模板技術
一、起源與現狀:
關於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的相容性不同,又可以分為相容性模板語言和非相容性模板語言。
模板語言要處理三個要點:
標量標記。把變數標識插入html的方法很多。其中一種是使用類似html的標籤;另一種是使用特殊標識,如Velocity或者JDynamiTe;第三種是擴充套件html標籤,如tapestry。採用何種方式有著很多考慮,一個比較常見的考慮是“所見即所得”的要求。
條件控制。這是一個很棘手的問題。一個簡單的例子是某物流陪送系統中,物品數低於一定值的要高亮顯示。不過對於一個具體複雜顯示邏輯的情況,條件控制似乎不可避免。當你把類似於<IF condition=”$count <= 40”><then><span class=”highlight”>count </span></then></IF>引入,就象我們當初在ASP和PHP中所做得一樣,我們將不得不再一次面對scriptlet嵌入網頁所遇到的問題。我相信你和我一樣並不認為這是一個好得的編寫方式。實際上並非所有的模板技術都使用條件控制,很多已有的應用如PHP上中的以及我曾見過一個基於
迭代(迴圈)。在網頁上顯示一個數據表單是一個很基本的要求,使用集合標籤將不可避免,不過幸運的是,它通常很簡單,而且夠用。特別值得一提的是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種:
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");
DOM:直接操作DOM資料物件,如XSLT利用XPath技術。
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技術分析
1. JSP技術:
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);
}
}
}
2. Taglib技術:
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。
� 為該tag設定頁面上下文。
� 為該tag設定其父tag,如果沒有就為null。
� 呼叫setter方法傳入標籤屬性值tag,如果該標籤沒有屬性,此步跳過。
� 呼叫doStartTag方法,取的返回值。
如果該標籤有body,根據doStartTag返回值確定是否pop該標籤內容。如果要pop其body,則:setBodyContent(),在之後,doInitBody()。如果該標籤沒有body,此步跳過。
� 呼叫doEndTag()以確定是否跳過頁面剩下部分。
� 最後把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,根據doStartTag返回值確定是否pop該標籤內容。如果要pop其body,則:setBodyContent(),在之後,doInitBody()。如果該標籤沒有body,此步跳過。
� 呼叫doEndTag()以確定是否跳過頁面剩下部分。
� 最後把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程式碼中插入業務邏輯是如此的容易。