本文配套源碼
大多數程序都需要輸出一些文本,比如郵件消息、HTML文件或控制台輸出。但是,計算機本質上只能處理二進制數據,程序員必須讓軟件來生成可理解的文本。在這篇文章中,我要介紹的是在生成和輸出文本時,為何使用模板引擎能夠節省時間。你將了解模板的優點,如何針對不同的情形創建高效的模板。和System.println說再見!
雖然程序員可以很輕松地編寫出輸出文字信息的代碼(因為這畢竟是從Hello World范例學到的第一件事情),但通常而言,程序員不是寫作或組織文字信息(如郵件)的最佳人選。因此,我們常常讓市場部門或公關部門去做那些事情。但遺憾的是,即使對於最普通的郵件,編寫者也常常依賴程序輸出來完成任務。無論是對於郵件編寫者還是程序員,這種合作方式都很容易帶來誤解和造成失誤。
請看一個例子:一個Java程序從某個數據源收集一些客戶信息,通過email給公司的每一個客戶發送帳戶余額信息。下面是完成這個任務的Java程序(完整的示例程序代碼可以從本文最後下載):
for (int i=0; i
{
Customer customer = (Customer)customers.get(i);
StringBuffer message = new StringBuffer();
message.append ("尊敬的先生/女士:");
message.append (customer.getCustName());
message.append ("\n");
message.append ("\n");
message.append ("您的帳戶余額是 ");
message.append (customer.getAccountTotal());
message.append ("\n");
message.append ("\n");
message.append ("致禮!");
message.append ("\n");
message.append ("某某裝飾品公司");
// 發送email
mm.sendMail (customer.getFirstName(), customer.getEmail(),
"Account", message.toString());
}
上面的例子可謂發送消息最差勁的方法之一。由於消息嵌入到了程序代碼之中,如果沒有程序員的幫助,其他人幾乎不可能對消息進行編輯。同時,即使對於專業的程序員,如果他不了解代碼,要進行編輯也很困難。如果你預見了這些麻煩,把代碼寫成下面這種形式:
static public final String STR_HELLO="尊敬的先生/女士:";
static public final String STR_MESSAGE="您的帳戶余額是 ";
static public final String STR_BEY="致禮!\n某某裝飾品公司";
如果說上述代碼使得消息編輯更容易,那麼這種幫助也不會很多。很難要求一個不搞程序設計的人理解static和final的含義。此外,如果要改變消息的結構,上面這種代碼也不夠靈活。例如,人們可能要求你在郵件消息中加入更多來自數據源的信息,這時,你就得修改構造郵件的代碼,或許還要添加更多的static final String對象。
模板簡介
從文本文件裝入消息文本可以解決部分問題——但不能提供動態內容,而這對於系統來說是很重要的。你需要有一種方法把動態內容插入到預先編寫好的文本消息。但是,如果使用某種文本模板引擎,它就能夠幫助你完成所有復雜的工作。
模板引擎解決了把動態內容插入文本消息的問題。使用模板引擎時,我們不再把消息直接嵌入程序,而是創建一個包含文本內容的簡單文本文件,稱為“文本模板”。模板引擎解析文本模板,借助一些簡單的模板指令,把動態內容插入模板輸出結果。
我選擇的模板引擎是Jakarta Project的Velocity,但你可以任意選擇其他許多模板引擎之一。Velocity和WebMacro或許是當前功能最豐富、最受歡迎的兩個引擎,而且兩者都按照源代碼開放協議免費提供。雖然我在本文例子中使用Velocity,你可以方便地把這些例子移植到不同的模板引擎,只需遵照目標引擎的語法即可。
我們來看看用Velocity完成的email程序例子。要編譯和運行修改後的程序,你必須下載Velocity並把它加入到classpath。如果要讓email部分也能正常運行,你還需要JavaMail。
for (int i=0; i<customers.size(); i++)
{
Customer customer = (Customer)customers.get(i);
// 創建一個環境,並加入所有的對象
VelocityContext context = new VelocityContext();
context.put ("CustName",customer.getCustName());
context.put ("total", new Double (customer.getAccountTotal()));
context.put ("customer", customer );
// 解析模板,生成結果字符串
StringWriter message = new StringWriter();
template.merge(context, message);
// 發送email
mm.sendMail (customer.getFirstName(), customer.getEmail(),
"Account", message.toString());
}
首先,你應該理解上面的Java源代碼。這裡我們不再象第一個例子那樣生成文本,上面的代碼引用一個稱為“Velocity模板”的文本文件,然後把結果發送給收件人。Velocity模板可以是任何文本文件,但一般它包含一些用來插入動態內容的指令。
和VelocityContext相關的部分是上述代碼中最值得注意的地方。VelocityContext提供了Java程序和Velocity文本模板之間的連接,而Velocity文本模板可以由其他人來編寫。在模板中,所有加入到VelocityContext的對象都可以通過put()方法第一個參數指定的名字訪問。為了解其工作過程,請看下面的模板文件:
尊敬的先生/女士:$CustName
您的帳戶余額是 $total
致禮!
某某裝飾品公司
Velocity引擎讀取模板文件時,它直接輸出文件中所有的文本,但以$字符開頭的除外。$符號標識著一個位置,在模板的輸出結果中,對象的值應該插入到$符號所指示的位置。例如,Java代碼中有一個context.put ("CustName",customer.getCustName())語句,當Velocity模板引擎解析並輸出模板的結果時,模板中所有出現$CustName的地方都將插入客戶的名字;即,被加入到VelocityContext的對象的toString()方法返回值將替代Velocity變量(模板中以$開頭的變量)。
模板引擎中最強大、使用最頻繁的功能之一是它通過內建的映像(Reflection)引擎查找對象信息的能力。這個映像引擎允許用一種方便的Java“.”類似的操作符,提取任意加入到VelocityContext的對象的任何公用方法的值,或對象的任意數據成員。映像引擎還帶來了另外一個改進:快速引用JavaBean的屬性。使用JavaBean屬性的時候,我們可以忽略get方法和括號(欲知詳細信息,請參考模板引擎的說明文檔)。請看下面這個模板的例子。由於在前面的Java代碼示例中,我把Customer對象加入到了VelocityContext,所以我可以把模板改寫成下面這種形式:
尊敬的先生/女士:$customer.CustName
您的帳戶余額是 $customer.AccountTotal
致禮!
某某裝飾品公司
除了替換變量之外,象Velocity和WebMacro這類高級引擎還能做其他許多事情。它們有用來比較和迭代的內建指令(盡管比較和迭代功能是兩個模板引擎之間的共同點,但它們的語法差異很大,不能完全兼容。在選擇模板引擎或者更換模板引擎時,務必注意這一點)。
舉一個例子。十二月份,你的老板想要向所有的客戶發一個聖誕節問候的email。你可以把這個消息加入到模板,以後再刪除它。但這樣的話,你得在新年那一天上班工作,以便刪除聖誕問候消息。
一種更好的辦法是指示模板何時顯示聖誕問候消息。為此,你首先要把當前的月份加入到VelocityContext:
int month = (new GregorianCalendar()).get(Calendar.MONTH);
// 把month值加1,因為它從0開始計算
context.put ("month", new Integer(month+1) );
現在,你只需在模板中進行比較:
尊敬的先生/女士:$customer.CustName
您的帳戶余額是 $customer.AccountTotal
致禮!
某某裝飾品公司
#if ($month == 12)
祝您和您的家人聖誕節快樂!
#end
#if指令的作用很清楚:對一個邏輯表達式進行測試,從而決定是否在模板輸出結果中包含該指令塊內的內容。除了簡單的等於比較之外,你還可以執行更復雜的比較,但這方面的功能都與特定的模板引擎密切關聯,所以這裡我不再介紹。
迭代指令和#if指令一樣簡單。模板引擎支持迭代Java Collections Framework的任意實現,包括Array、List和Iterator。對於JDK 1.2或者更高版本,Java的Vector和ArrayList都實現了List接口,因此它們也適合在模板引擎的迭代指令中使用。
假設我們現在不想輸出帳戶余額,而是想通過迭代遍歷帳戶的交易記錄,輸出詳細的報表。Customer對象的getTransactions()方法(參見下載包中完整的示例代碼)返回一個List對象,List對象包含零個或者多個Transaction對象。由於List屬於Java Collections Framework的一部分,我們可以用#foreach指令迭代其內容。我們不用擔心如何定型對象的類型——映像引擎會為我們完成這個任務。從下面這個例子中,我們可以看出迭代的工作過程:
尊敬的先生/女士:$customer.CustName
#foreach ($transaction in $customer.Transactions)
$transaction.Description $transaction.Amount
#end
您的帳戶余額是 $customer.AccountTotal
致禮!
某某裝飾品公司
#foreach指令的一般格式是“#foreach <item> in <list>”。#foreach指令迭代list,把list中的每個元素放入item參數,然後解析#foreach塊內的內容。對於list內的每個元素,#foreach塊的內容都會重復解析一次。從效果上看,它相當於告訴模板引擎說:“把list中的每一個元素依次放入item變量,每次放入一個元素,輸出一次#foreach塊的內容”。
MVC設計模型
在看下一個例子之前,請回顧一下前面我們所討論的內容。使用模板引擎最大的好處在於,它分離了代碼(或程序邏輯)和表現(輸出)。由於這種分離,你可以修改程序邏輯而不必擔心郵件消息本身;類似地,你(或公關部門的職員)可以在不重新編譯程序的情況下,重新編寫郵件消息。
實際上,我們分離了系統的數據模式(Data Model,即提供數據的類)、控制器(Controller,即郵件程序)以及視圖(View,即模板)。這種三層體系稱為Model-View-Controller模型(MVC)。如果遵從MVC模型,代碼分成三個截然不同的層,簡化了軟件開發過程中所有相關人員的工作(MVC的出現已經有一段時間,參見本文最後的“參考資源”了解更多信息)。
結合模板引擎使用的數據模式可以是任何Java對象,最好是使用Java Collection Framework的對象。控制器只要了解模板的環境(如VelocityContext),一般這種環境都很容易使用。一些關系數據庫的“對象-關系”映射工具能夠和模板引擎很好地協同,簡化JDBC操作;對於EJB,情形也類似。
模板引擎與MVC中視圖這一部分的關系更為密切。模板語言的功能很豐富、強大,足以處理所有必需的視圖功能,同時它往往很簡單,不熟悉編程的人也可以使用它。模板語言不僅使得設計者從過於復雜的編程環境中解脫出來,而且它保護了系統,避免了有意或無意帶來危險的代碼。例如,模板的編寫者不可能編寫出導致無限循環的代碼,或侵占大量內存的代碼。不要輕估這些安全機制的價值;大多數模板編寫者不懂得編程,從長遠來看,避免他們接觸復雜的編程環境相當於節省了你自己的時間。
許多模板引擎的用戶相信,在采用模板引擎的方案中,控制器部分和視圖部分的明確分離,再加上模板引擎固有的安全機制,使得模板引擎足以成為其他內容發布系統(比如JSP)的替代方案。因此,Java模板引擎最常見的用途是替代JSP也就不足為奇了。
HTML處理
由於人們總是看重模板引擎用來替換JSP的作用,有時他們會忘記模板還有更廣泛的用途。到目前為止,模板引擎最常見的用途是處理HTML Web內容。但我還用模板引擎生成過SQL、email、XML甚至Java源代碼。在這裡我只能涉及模板的部分應用,但你可以從本文最後的參考資源找到更多的例子。
我將在下面的HTML例子中使用前面email例子的數據模式。這個HTML頁面是一個假想的企業Intranet頁面,它顯示出客戶帳戶的詳細信息。本例中的控制器類是一個Java Servlet,視圖部分則包含一個HTML模板。下面的代碼顯示了Servlet類中最主要的代碼。為使這個例子更具有代表性,我從頭開始手工編寫這個Servlet。然而,一般情況下,模板會提供一些Servlet工具,幫助用戶減輕一些編寫代碼的負擔。
// 裝入模板
Template template = Velocity.getTemplate("html.vm");
// 創建環境
VelocityContext context = new VelocityContext();
context.put ("customers",Customer.getCustomers());
// 解析模板,輸出應答
ServletOutputStream output = response.getOutputStream();
Writer writer = new OutputStreamWriter (output);
template.merge(context, writer);
writer.flush();
這個例子也沒有什麼令人驚異的地方。和前面的例子一樣,我只是把必需的對象加入到VelocityContext,然後輸出解析模板的結果。但是請注意,在前面的例子中,我只把一個Customer加入到VelocityContext,這裡加入到VelocityContext的卻是一組Customer對象。我可以用#foreach指令迭代訪問所有的Customer對象。下面是相應的HTML模板:
<html>
<body>
<h1>客戶報告</h1>
#foreach ($customer in $customers)
<h2>$customer.CustName<h2>
<table>
#foreach ($transaction in $customer.Transactions)
<tr>
<td width="200">
$transaction.Date
</td>
<td width="150">
$transaction.Description
</td>
<td width="100">
$transaction.Amount
</td>
</tr>
#end
</tr>
<td></td>
<td></td>
<td><b>$customer.AccountTotal</b></td>
<tr>
</table>
#end
</body>
</html>
如果你正在規劃一個工程,這個工程的需求遠遠超過幾個HTML模板,請考慮眾多以模板為基礎的框架之一。這些框架不僅為生成HTML提供了模板引擎所帶來的便利,而且提供許多實用工具,比如數據庫連接池和安全。兩個常見的例子是Turbine和Melati,它們都和Velocity以及WebMacro兼容,都是免費且源代碼開放的產品。
性能和配置
對於大多數程序來說,模板的速度看來已經足夠快;但對於大容量的Web網站,你可能要認真地考慮一下性能問題。在性能方面,模板引擎最大的特點在於模板緩沖。在模板緩沖機制的作用下,模板不再是每次出現請求的時候從磁盤讀取,而是以最理想的方式在內存中保存和解析。在開發期間,模板緩沖通常處於禁用狀態,因為這時請求數量較少,而且要求對頁面的修改立即產生效果。部署完畢之後,模板一般不再改變,性能就成了優先考慮的問題。因此,這時你應該啟用模板緩沖功能。
對於大多數模板引擎,你可以通過應用一個設置選項或編輯Java屬性文件方便地啟用模板緩沖功能。在Velocity中,你可以通過Properties對象初始化模板。至於Properties對象的創建方法,你既可以手工創建,就象我前面所做的那樣;或者也可以從屬性文件裝入。在實際應用中,後者也許是較為理想的方法。
Properties props = new Properties();
props.setProperty( "file.resource.loader.cache", "true" );
props.setProperty( "file.resource.loader.modificationCheckInterval", "3600" );
Velocity.init (props);
通過file.resource.loader.cache屬性可以把緩沖設置成true或false,而file.resource.loader.modificationCheckInterval屬性設置的是檢查文件是否改變的間隔秒數。在這裡我無法詳細介紹所有的屬性,請參考模板引擎的文檔了解更多信息。
■ 結束語
免費的高級模板引擎使我們能夠把模板功能加入到幾乎所有的Java應用。這些模板引擎為程序員提供了易用的工具,為模板編寫者提供了簡單的模板語言,使得開發者更有信心編寫出高質量的代碼。
模板分離了程序代碼和應用的表現部分,極大地方便了程序員和內容制作者的工作。模板把程序員從混合了大量文本信息的雜亂代碼中解放出來;使得制作文本內容的人無需面對程序邏輯,就可以輕松地編寫和修改內容。
模板清楚地分離了程序邏輯和文本表現代碼,從而也為設計更好的MVC系統提供了方便。因此,模板為替換其他內容發布系統(比如JSP)提供了一種有吸引力的方案,因為它能夠在不增加復雜性的情況下,改進應用的整體設計。