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