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來建立和管理工程。建立好的工程目錄結構如下圖所示:
圖1. 項目目錄結構
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也獲得了非常廣泛的應用,有興趣的讀者可以去深入研究更多的功能和用途。