velocity的使用簡介
Velocity 是一個基於 Java 的模板引擎,它允許任何人僅僅簡單的使用模板語言來引用由 Java 程式碼定義的物件,從而實現介面和 Java 程式碼的分離,使得介面設計人員可以和 Java 程式開發人員同步開發一個遵循 MVC 架構的 web 站點。但是在實際應用過程中,Velocity 又不僅僅被用在了 MVC 的架構中。
Velocity 模板引擎介紹
在現今的軟體開發過程中,軟體開發人員將更多的精力投入在了重複的相似勞動中。特別是在如今特別流行的 MVC 架構模式中,軟體各個層次的功能更加獨立,同時代碼的相似度也更加高。所以我們需要尋找一種來減少軟體開發人員重複勞動的方法,讓程式設計師將更多的精力放在業務邏輯以及其他更加具有創造力的工作上。Velocity 這個模板引擎就可以在一定程度上解決這個問題。
Velocity 是一個基於 Java 的模板引擎框架,提供的模板語言可以使用在 Java 中定義的物件和變數上。Velocity 是 Apache 基金會的專案,開發的目標是分離 MVC 模式中的持久化層和業務層。但是在實際應用過程中,Velocity 不僅僅被用在了 MVC 的架構中,還可以被用在以下一些場景中。
1.Web 應用:開發者在不使用 JSP 的情況下,可以用 Velocity 讓 HTML 具有動態內容的特性。
2. 原始碼生成:Velocity 可以被用來生成 Java 程式碼、SQL 或者 PostScript。有很多開源和商業開發的軟體是使用 Velocity 來開發的。
3. 自動 Email:很多軟體的使用者註冊、密碼提醒或者報表都是使用 Velocity 來自動生成的。使用 Velocity 可以在文字檔案裡面生成郵件內容,而不是在 Java 程式碼中拼接字串。
4. 轉換 xml:Velocity 提供一個叫 Anakia 的 ant 任務,可以讀取 XML 檔案並讓它能夠被 Velocity 模板讀取。一個比較普遍的應用是將 xdoc 文件轉換成帶樣式的 HTML 檔案。
Hello Velocity
和學習所有新的語言或者框架的順序一樣,我們從 Hello Velocity 開始學習。首先在 Velocity 的官網上下載最新的釋出包,之後使用 Eclipse 建立普通的 Java 專案。引入解壓包中的 velocity-1.7.jar 和 lib 資料夾下面的 jar 包。這樣我們就可以在專案中使用 Velocity 了。
在做完上面的準備工作之後,就可以新建一個叫 HelloVelocity 的類,程式碼如下:
清單 1. HelloVelocity.java
public class HelloVelocity { public static void main(String[] args) { VelocityEngine ve = new VelocityEngine(); ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath"); ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName()); ve.init(); Template t = ve.getTemplate("hellovelocity.vm"); VelocityContext ctx = new VelocityContext(); ctx.put("name", "velocity"); ctx.put("date", (new Date()).toString()); List temp = new ArrayList(); temp.add("1"); temp.add("2"); ctx.put("list", temp); StringWriter sw = new StringWriter(); t.merge(ctx, sw); System.out.println(sw.toString()); } }
在 HelloVelocity 的程式碼中,首先 new 了一個 VelocityEngine 類,這個類設定了 Velocity 使用的一些配置,在初始化引擎之後就可以讀取 hellovelocity.vm 這個模板生成的 Template 這個類。之後的 VelocityContext 類是配置 Velocity 模板讀取的內容。這個 context 可以存入任意型別的物件或者變數,讓 template 來讀取。這個操作就像是在使用 JSP 開發時,往 request 裡面放入 key-value,讓 JSP 讀取一樣。
接下來就是寫 hellovelocity.vm 檔案了,這個檔案實際定義了 Velocity 的輸出內容和格式。hellovelocity.vm 的內容如下:
清單 2. Hellovelocity.vm
#set( $iAmVariable = "good!" ) Welcome $name to velocity.com today is $date. #foreach ($i in $list) $i #end $iAmVariable
輸出結果如下:
Welcome velocity to velocity.com today is Sun Mar 23 19:19:04 CST 2014. 1 2 good!
在輸出結果中我們可以看到,$name、$date 都被替換成了在 HelloVelocity.java 裡面定義的變數,在 foreach 語句裡面遍歷了 list 的每一個元素,並打印出來。而$iAmVariable 則是在頁面中使用 #set 定義的變數。
基本模板語言語法使用
在 hellovelocity.vm 裡面可以看到很多以 # 和$符開頭的內容,這些都是 Velocity 的語法。在 Velocity 中所有的關鍵字都是以 # 開頭的,而所有的變數則是以$開頭。Velocity 的語法類似於 JSP 中的 JSTL,甚至可以定義類似於函式的巨集,下面來看看具體的語法規則。
一、變數
和我們所熟知的其他程式語言一樣,Velocity 也可以在模板檔案中有變數的概念。
1. 變數定義
#set($name =“velocity”)
等號後面的字串 Velocity 引擎將重新解析,例如出現以$開始的字串時,將做變數的替換。
#set($hello =“hello $name”)
上面的這個等式將會給$hello 賦值為“hello velocity”
2. 變數的使用
在模板檔案中使用$name 或者${name} 來使用定義的變數。推薦使用${name} 這種格式,因為在模板中同時可能定義了類似$name 和$names 的兩個變數,如果不選用大括號的話,引擎就沒有辦法正確識別$names 這個變數。
對於一個複雜物件型別的變數,例如$person,可以使用${person.name} 來訪問 person 的 name 屬性。值得注意的是,這裡的${person.name} 並不是直接訪問 person 的 name 屬性,而是訪問 person 的 getName() 方法,所以${person.name} 和${person.getName()} 是一樣的。
3. 變數賦值
在第一小點中,定義了一個變數,同時給這個變數賦了值。對於 Velocity 來說,變數是弱資料型別的,可以在賦了一個 String 給變數之後再賦一個數字或者陣列給它。可以將以下六種資料型別賦給一個 Velocity 變數:變數引用, 字面字串, 屬性引用, 方法引用, 字面數字, 陣列列表。
#set($foo = $bar) #set($foo =“hello”) #set($foo.name = $bar.name) #set($foo.name = $bar.getName($arg)) #set($foo = 123) #set($foo = [“foo”,$bar])
二、迴圈
在 Velocity 中迴圈語句的語法結構如下:
#foreach($element in $list) This is $element $velocityCount #end
Velocity 引擎會將 list 中的值迴圈賦給 element 變數,同時會建立一個$velocityCount 的變數作為計數,從 1 開始,每次迴圈都會加 1.
三、條件語句
條件語句的語法如下
#if(condition) ... #elseif(condition) … #else … #end
四、關係操作符
Velocity 引擎提供了 AND、OR 和 NOT 操作符,分別對應&&、||和! 例如:
#if($foo && $bar) #end
五、巨集
Velocity 中的巨集可以理解為函式定義。定義的語法如下:
#macro(macroName arg1 arg2 …) ... #end
呼叫這個巨集的語法是:
#macroName(arg1 arg2 …)
這裡的引數之間使用空格隔開,下面是定義和使用 Velocity 巨集的例子:
#macro(sayHello $name) hello $name #end #sayHello(“velocity”)
輸出的結果為 hello velocity
六、#parse 和 #include
#parse 和 #include 指令的功能都是在外部引用檔案,而兩者的區別是,#parse 會將引用的內容當成類似於原始碼檔案,會將內容在引入的地方進行解析,#include 是將引入檔案當成資原始檔,會將引入內容原封不動地以文字輸出。分別看以下例子:
foo.vm 檔案:
#set($name =“velocity”)
parse.vm:
#parse(“foo.vm”)
輸出結果為:velocity
include.vm:
#include(“foo.vm”)
輸出結果為:#set($name =“velocity”)
以上內容包含了部分 Velocity 的語法,詳細的語法內容可以參考 Velocity 的官方文件。
自動生成程式碼的例子
在上個例子中我們可以生成任意的字串並且打印出來,那為什麼我們不能生成一些按照既定格式定義的程式碼並且寫入檔案呢。
在這裡我們以一個實際的 demo 來完成這部分內容。相關內容的原始碼可以參照附件。這個 demo 的功能是要實現一個學生和老師的管理,實際上都是單張表的維護。我們希望能夠只定義 model 層,來生成 MVC 的所有程式碼。在這個 demo 中,只自動生成 action 和 JSP 的內容,因為現在有很多工具都可以幫助我們自動生成這兩個包的程式碼。
首先在 eclipse 中建立一個 Java web 工程,在例子中為了方便管理 jar 包,使用的是 maven 來建立和管理工程。建立好的工程目錄結構如下圖所示:
Java Resource 中放的是 Java 原始碼以及資原始檔,Deployed Resources 中放的是 web 相關的檔案。在 Java 檔案中使用了類似 Spring 的 @Component 和 @Autowired 的註解來實現 IoC,使用 @Action 這樣的註解實現 MVC,而在 JSP 中則使用了 JSTL 來輸出頁面。在上圖所示的目錄中,annotation、filter、framework 和 util 這四個 package 是作為這個專案框架的,跟業務沒有關係,類似於 spring 和 struts 的功能。
在實際的專案中我們當然希望能夠一開始就編寫一個通用的模板檔案,然後一下子生成所有的程式碼,但是很多時候這樣做是不可能的,或者說比較困難。為了解決這個問題,我們可以在編寫 Velocity 模板檔案之前先按照原本的流程編寫程式碼,暫時先忘掉 Velocity。編寫的程式碼應該能夠在一個功能上完整的調通涉及 MVC 中所有層次的內容。在這個例子中,先編寫好 StudentAction.java 檔案,以及上圖中 webapp 目錄中所示的檔案。在寫好以上程式碼,同時也能順利執行之後,我們可以參照之前編寫的程式碼來寫模板檔案。這裡我們來分別看一個 Java 檔案和 JSP 的例子。
清單 3. ActionTemplate.vm
#parse ("macro.vm") @Action("${classNameLowCase}Action") public class ${classNameUpCase}Action extends BaseAction{ @Autowired public ${classNameUpCase}Dao ${classNameLowCase}Dao; private List<${classNameUpCase}> ${classNameLowCase}s; private ${classNameUpCase} ${classNameLowCase}; #foreach ($attr in ${attrs}) private ${attr[0]} ${attr[1]}; #end public String ${classNameLowCase}List() { ${classNameLowCase}s = ${classNameLowCase}Dao.retrieveAll${classNameUpCase}s(); return "${classNameLowCase}List.jsp"; } ... }
上面的程式碼展示了一個 Java 類轉換成 vm 模板之後的部分內容,完整內容請參考附件。
macro.vm 檔案中定義了一些使用的巨集。JSP 的改造相對於 Java 檔案來說稍微有點複雜,因為 JSP 中使用 JSTL 取 request 中的值也是使用${name} 這樣的語法,所以想要輸出${name} 這樣的字串而不是被模板引擎所替換,則需要使用轉義字元,就像這樣:\${name}。
為了能夠讓這個檔案中的 table 得到複用,我們將這個檔案中的表格單獨拿出來,使用 #parse 命令來包含。下面是 ListJspTemplate.vm 和 ListTableTemplate.vm 的內容:
清單 4. ListJspTemplate.vm
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <%@ include file="includeJS.jsp" %> <script type="text/javascript"> var pageConfig = { "list" : { "action" : "${classNameLowCase}Action!${classNameLowCase}List.action" } ... "idName" : "${classNameLowCase}Id" }; </script> <script type="text/javascript" src="common.js"></script> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>${classNameUpCase} List</title> </head> <body> <h1>${classNameUpCase} List</h1> <div><button id="addButton">Add</button></div> #parse ("ListTableTemplate.vm") <div id="modifyDiv"></div> <div id="addDiv"></div> </body> </html>
清單 5. ListTableTemplate.vm
#parse ("macro.vm") #set($plus = "status.index+1") <table border="1" style="width: 100%"> <thead> <tr><th>No.</th>#generateTH($attrs)</tr> </thead> <tbody> <c:forEach var="${classNameLowCase}" items="${${classNameLowCase}s }" varStatus="status" > <tr ${classNameLowCase}Id="${${classNameLowCase}.id }"> <td>${${plus}}</td>#generateTD($classNameLowCase $attrs)<td> <button class="modifyButton">Modify</button> <button class="deleteButton">Delete</button></td></tr> </c:forEach> </tbody> </table>
在定義好所有的模板檔案之後,需要做的是讀取這些檔案,然後根據這些檔案將 model 的資料型別以及名稱設定到 context 中,最後將解析出來的內容寫到相應的目錄中去。這些工作我們放在了一個叫做 VelocityGenerator 的類中來做,它的原始碼如下:
清單 6. TemplateGenerator.java
public class VelocityGenerator { public static void main(String[] args) { VelocityEngine ve = new VelocityEngine(); ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath"); ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName()); ve.init(); Template actionTpt = ve.getTemplate("ActionTemplate.vm"); Template listJspTpt = ve.getTemplate("ListJspTemplate.vm"); Template addTpt = ve.getTemplate("AddTemplate.vm"); Template modifyTpt = ve.getTemplate("ModifyTemplate.vm"); VelocityContext ctx = new VelocityContext(); ctx.put("classNameLowCase", "teacher"); ctx.put("classNameUpCase", "Teacher"); String[][] attrs = { {"Integer","id"}, {"String","name"}, {"String","serializeNo"}, {"String","titile"}, {"String","subject"} }; ctx.put("attrs", attrs); String rootPath = VelocityGenerator.class.getClassLoader().getResource("").getFile() + "../../src/main"; merge(actionTpt,ctx,rootPath+"/java/com/liuxiang/velocity/action/TeacherAction.java"); merge(listJspTpt,ctx,rootPath+"/webapp/teacherList.jsp"); merge(addTpt,ctx,rootPath+"/webapp/teacherAdd.jsp"); merge(modifyTpt,ctx,rootPath+"/webapp/teacherModify.jsp"); System.out.println("success..."); } private static void merge(Template template, VelocityContext ctx, String path) { PrintWriter writer = null; try { writer = new PrintWriter(path); template.merge(ctx, writer); writer.flush(); } catch (FileNotFoundException e) { e.printStackTrace(); } finally { writer.close(); } } }
在執行以上程式碼之後,專案資料夾中將會出現與 Teacher 相關的程式碼檔案。
在實際專案中可能不會出現很多這種單張表維護的情況,而且業務邏輯和系統架構會更加複雜,編寫模板檔案就更加不容易。但是無論多複雜的系統,不同的業務邏輯之間一定或多或少會有相似的程式碼,特別是在 JSP 和 JS 顯示端檔案中,因為我們在一個系統中要求顯示風格、操作方式一致的時候就免不了會有相似內容的程式碼出現。在總結這些相似性之後我們還是可以使用 Velocity 來幫助我們生成部分內容的程式碼,而且即使有一些非共性的內容,我們也可以在生成的程式碼中繼續修改。使用 Velocity 的另外一個好處是生成出來的程式碼更好維護,風格更加統一。
結束語
Velocity 可以被應用在各種各樣的情景下,本文介紹的只是它的一種用途而已,它還可以被用來做 MVC 結構中的 view 層,或者動態內容靜態化等。另外,Velocity 並不是唯一的模板框架,同樣很優秀的 Freemarker 也獲得了非常廣泛的應用,有興趣的讀者可以去深入研究更多的功能和用途。