使用 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 也獲得了非常廣泛的應用,有興趣的讀者可以去深入研究更多的功能和用途。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章