簡介
Velocity 曆史悠久的免費java模闆引擎。官網:http://velocity.apache.org/
Velocity是基于Java的模闆引擎,這是一種簡單而強大的開發工具,可讓您輕松建立和呈現用于格式化和顯示資料的文檔
*.vm :Velocity 模闆檔案
VTL : Velocity Template Language
使用Velocity模闆引擎時的需要關注兩部分:Velocity模闆和Java代碼調用。
Velocity模闆由VTL和引擎上下文對象構成;
Java代碼調用部分則負責初始Velocity引擎、建構引擎上下文對象、加載Velocity模闆和啟動模版渲染。
而Velocity模闆與Java代碼調用部分通信的紐帶就是引擎上下文對象了。
Velocity被移植到不同的平台上,如.Net的NVelocity和js的Velocity.js,雖然各平台在使用和實作上略有差别,但大部分文法和引擎核心的實作是一緻的,是以學習成本降低不少哦。
版本要求
Velocity2.1以上版本要求jdk1.8以上
Velocity2.2的maven 依賴
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>org.apache.velocity.tools</groupId>
<artifactId>velocity-tools-generic</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity.tools</groupId>
<artifactId>velocity-tools-view</artifactId>
<version>3.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity.tools</groupId>
<artifactId>velocity-tools-view-jsp</artifactId>
<version>3.0</version>
</dependency>
其它所有版本官方下載下傳位址:http://archive.apache.org/dist/velocity/engine/
Velocity1.7要求jdk1.4版本以上。官網位址:http://velocity.apache.org/engine/1.7/
1.7 api doc位址:https://tool.oschina.net/apidocs/apidoc?api=velocity-1.7
快速入門
Example2.java
import java.io.StringWriter;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.VelocityContext;
public class Example2
{
public static void main( String args[] )
{
/* 首先,初始化運作時引擎,使用預設的配置 */
Velocity.init();
/* 建立Context對象,然後把資料放進去 */
VelocityContext context = new VelocityContext();
context.put("name", "Velocity");
context.put("project", "Jakarta");
/* 渲染模闆 */
StringWriter w = new StringWriter();
Velocity.mergeTemplate("testtemplate.vm", context, w );
System.out.println(" template : " + w );
/* 渲染字元串 */
String s = "We are using $project $name to render this.";
w = new StringWriter();
Velocity.evaluate( context, w, "mystring", s );
System.out.println(" string : " + w );
}
}
在運作程式之前,我們需要把模闆檔案testtemplate.vm放到與程式相同的目錄下(因為我們使用預設配置,預設配置中指定的模闆加載路徑就是程式的目前目錄)
testtemplate.vm
Hi! This $name from the $project project.
總結
首先,你還是需要先建立一個context,放進你需要的資料。
然後合并内容
Velocity的Java編碼
以下内容轉自:https://www.cnblogs.com/fsjohnhuang/p/4112866.html 肥仔John
模闆與宿主環境通信
模闆指的是使用VTL編寫的Velocity模闆,宿主環境指的是Java代碼調用部分。而兩者通信的紐帶就是引擎上下文對象( VelocityContext執行個體 ),下面是常用的 VelocityContext執行個體 方法。
// 構造函數,入參為上下文的鍵值對集
VelocityContext(Map context)
// 添加上下文的鍵值對
Object put(String key, Object value)
// 從上下文擷取指定鍵的值
Object get(String key)
// 檢查上下文中是否存在指定的鍵值對
boolean containsKey(Object key)
// 擷取所有鍵
Object[] getKeys()
// 移除指定鍵
Object remove(Object key)
// 擷取上下文鍊中鄰近的上下文對象
Context getChainedContext()
宿主環境向模闆傳值
// 1. 通過構造函數傳值
HashMap<String, String> baseCtx = new HashMap<String, String>();
baseCtx.put("version", "1");
VelocityContext ctx = new VelocityContext(baseCtx);
// 2. 通過put傳值
ctx.put("author", "fsjohnhuang");
注意鍵值對中值的資料類型為
Integer、Long等簡單資料類型的裝箱類型;
String類型;
Object子類;
Object[] 數組類型,從1.6開始Velocity将數組類型視為 java.util.List 類型看待,是以模闆中可調用 size() 、 get(intindex) 和 isEmpty() 的變量方法;
java.util.Collection子類;
java.util.Map子類;
java.util.Iterator對象;
java.util.Enumeration對象。
除此之外,我們還可以将一個靜态類賦予到上下文對象中,如 java.lang.Math靜态類
ctx.put("Math", java.lang.Math.class);
模闆向宿主環境傳值
1. 通信示例1——通過引擎上下文對象擷取變量
模闆檔案frm.vm
#set($action="./submit")
<form action="$action">
.........
</form>
Java代碼部分
VelocityContext ctx = new VelocityContext();
VelocityEngine ve = new VelocityEngine();
StringWriter sw = new StringWriter();
ve.mergeTemplate("frm.vm", ctx, sw);
String actionStr = ctx.get("action");
System.out.println(actionStr); // 顯示./submit
2. 通信示例2——通過副作用修改變量、屬性值
模闆檔案change.vm
$people.put("john", "john huang")
#set($version = $version + 1)
Java代碼部分
VelocityContext ctx = new VelocityContext();
ctx.put("version", 1);
HashMap<String, String> people = new HashMap<String,String>();
ctx.put("people", people);
VelocityEngine ve = new VelocityEngine();
StringWriter sw = new StringWriter();
ve.mergeTemplate("change.vm", ctx, sw);
System.out.println(ctx.get("version")); // 顯示2
System.out.println(people.get("john")); //顯示john huang
上述示例表明在模闆中對引用類型執行個體進行操作時,操作結果将影響到該引用類型執行個體本身,是以必須謹慎操作。
引擎上下文鍊
也叫容器鍊,目前最常用的就是提供層次資料通路和工具箱
VelocityContext context1 = new VelocityContext();
context1.put("name","Velocity");
context1.put("project", "Jakarta");
context1.put("duplicate", "I am in context1");
VelocityContext context2 = new VelocityContext( context1 );
context2.put("lang", "Java" );
context2.put("duplicate", "I am in context2");
template.merge( context2, writer );
所謂引擎上下文鍊就是将原有的上下文對象賦予給建立的上下文對象,進而達到上下文内的鍵值對複用。具體代碼如下:
VelocityContext ctx1 = new VelocityContext();
ctx1.put("name", "fsjohuang");
ctx1.put("version", 1);
VelocityContext ctx2 = new VelocityContext(ctx1);
ctx2.put("version", 2);
System.out.println(ctx2.get("name")); // 顯示fsjohnhuang
System.out.println(ctx2.get("version")); // 顯示2
就是目前上下文對象沒有該鍵值對時,則查詢上下文鍊的對象有沒有該鍵值對,有則傳回,無則繼續找鍊上的其他上下文對象,直到找到該鍵值對或周遊完所有鍊上的上下文對象。
但VelocityContext執行個體除了put、get方法外,還有remove、getKeys、containsKey方法,它們的行為又是如何的呢?下面我們通過源碼來了解吧!
官網中涉及的java編碼部分
自定義屬性
/opt/templates
...
import java.util.Properties;
...
public static void main( String args[] )
{
/* 首先,我們還是初始化運作時引擎 */
Properties p = new Properties();
p.setProperty("file.resource.loader.path", "/opt/templates");
Velocity.init( p );
...
雖然Velocity允許你建立自己的容器類來滿足特殊的需求和技術(比如像一個直接通路LDAP伺服器的容器),一個叫VelocityContext的基本實作類已經作為發行版的一部分提供給你。
VelocityContext适合所有的一般需求,是以我們強烈推薦你使用VelocityContext這個容器。隻有在特殊情況和進階應用中,才需要你擴充或者建立你自己的容器實作。
for和foreach()周遊對象的支援
Velocity支援幾種集合類型在VTL中使用foreach()語句:
Object []
普通對象數組 如果一個類中提供了疊代器接口,Velocity會自動包裝你的數組
Velocity現在允許模闆設計者把數組當作定長連結清單來處理(Velocity 1.6中就是這樣)
java.util.Collection
Velocity通過iterator()方法傳回一個疊代器在循環中使用
java.util.Map
Velocity通過接口的values()方法傳回一個Collection接口,iterator()方法在它上面調用來檢索用于循環的疊代器。
java.util.Iterator
目前隻是暫時支援,疊代器不能重置
如果一個未初始化的疊代器被放進了容器,并且在多個foreach()語句中使用,如果第一個foreach()失敗了,後面的都會被阻塞,因為疊代器不會重新開機
java.util.Enumeration
和java.util.Iterator一樣
對于Iterator和Enumeration,推薦隻有在萬不得已的情況下才把它們放進容器,你也應該盡可能地讓Velocity找到合适的、可複用的疊代接口。
Vector v = new Vector();
v.addElement("Hello");
v.addElement("There");
context.put("words", v.iterator() );//不推薦
context.put("words", v );
對靜态類的支援
context.put("Math", Math.class);
這樣你就可以在模闆中用$Math引用調用java.lang.Math中的任何公有靜态方法。
java.lang.Math這樣的類不提供任何公有的構造函數,但是它包含了有用的靜态方法
Servlet使用Velocity
web.xml 中配置Velocity
http://velocity.apache.org/tools/devel/view-servlet.html
<!-- Define Velocity template handler -->
<servlet>
<servlet-name>velocity</servlet-name>
<servlet-class>
org.apache.velocity.tools.view.VelocityViewServlet
</servlet-class>
<!--
Unless you plan to put your tools.xml and velocity.properties
under different folders or give them different names, then these
two init-params are unnecessary. The
VelocityViewServlet will automatically look for these files in
the following locations.
-->
<init-param>
<param-name>org.apache.velocity.toolbox</param-name>
<param-value>/WEB-INF/tools.xml</param-value>
</init-param>
<init-param>
<param-name>org.apache.velocity.properties</param-name>
<param-value>/WEB-INF/velocity.properties</param-value>
</init-param>
</servlet>
<!-- Map *.vm files to Velocity -->
<servlet-mapping>
<servlet-name>velocity</servlet-name>
<url-pattern>*.vm</url-pattern>
</servlet-mapping>
tools.xml就像定義了一個工具箱,裡面放着很多工具,比如有個“扳手”。
具體示例:考慮考慮讓我們的朋友喬恩(Jon)從真實的工具箱中抓取我們的“扳手”。喬恩隻需要知道我們想要哪個扳手。他不需要知道扳手做什麼,也不需要知道我們打算如何做。
Velocity Toolbox的工作方式與上面的例子相同,我們僅需指定所需的工具,然後Velocity引擎就可以在vm模闆中使用任何在工具箱Tool.xml中定義好的公共方法來處理其餘的工作。
PipeWrench.java
public class PipeWrench {
public String getSize() {
return "Large Pipe Wrench!";
}
}
tools.xml
<?xml version="1.0"?>
<tools>
<toolbox scope="application">
<tool key="wrench" class="PipeWrench"/>
</toolbox>
</tools>
.vm模闆中可以使用:
$wrench.size
.
VM模闆
官方VTL指南:
http://velocity.apache.org/engine/2.2/vtl-reference.html
VTL: Velocity Template Language
以下内容轉自:https://www.cnblogs.com/fsjohnhuang/p/4112866.html 肥仔John
注釋
1. 行注釋
## 行注釋内容
2. 塊注釋
#*
塊注釋内容1塊注釋内容2
*#
3. 文檔注釋
#**
文檔注釋内容1
文檔注釋内容2
*#
踩過的坑
塊注釋和文檔注釋雖然均不輸出到最終結果上,但會導緻最終結果出現一行空行。使用行注釋則不會出現此情況。
直接輸出的内容
也就是不會被引擎解析的内容。
#[[
直接輸出的内容1
直接輸出的内容2
]]#
引用
引用語句就是對引擎上下文對象中的屬性進行操作
文法方面分為正常文法( $屬性 )和正規文法( ${屬性} )
在普通模式下上述兩種寫法,當引擎上下文對象中沒有對應的屬性時,最終結果會直接輸出 $屬性 或 ${屬性} ,若要不輸出則需要改寫為 $!屬性 和 $!{屬性} 。
1. 變量(就是引擎上下文對象的屬性)
$變量名, 正常寫法,若上下文中沒有對應的變量,則輸入字元串"$變量名"
${變量名}, 正常寫法,若上下文中沒有對應的變量,則輸入字元串"${變量名}"
$!變量名, 正常寫法,若上下文中沒有對應的變量,則輸入空字元串""
$!{變量名}, 正常寫法,若上下文中沒有對應的變量,則輸入空字元串""
變量的命名規則:
由字母、下劃線(_)、破折号(-)和數字組成,而且以字母開頭。
變量的資料類型為:
Integer、Long等簡單資料類型的裝箱類型;
String類型;
Object子類;
Object[] 數組類型,從1.6開始Velocity将數組類型視為 java.util.List 類型看待,是以模闆中可調用 size() 、 get(int index) 和 isEmpty() 的變量方法;
java.util.Collection子類;
java.util.Map子類;
java.util.Iterator對象;
java.util.Enumeration對象。
2. 屬性(就是引擎上下文對象的屬性的屬性)
$變量名.屬性, 正常寫法
${變量名.屬性}, 正規寫法
$!變量名.屬性, 正常寫法
$!{變量名.屬性}, 正規寫法
屬性搜尋規則:
Velocity采用一種十分靈活的方式搜尋變量的屬性, 具體如下:
// 假如引用$var.prop,那麼Velocity将對prop進行變形,然後在$var對象上嘗試調用 // 變形和嘗試的順序如下
- $var.getprop()
- $var.getProp()
- $var.get("prop")
- $var.isProp()
// 對于$var.Prop則如下
- $var.getProp()
- $var.getprop()
- $var.get("Prop")
- $var.isProp()
是以擷取 java.util.Map 對象的鍵值時可以簡寫為 $map.key ,Velocity會自動轉為 $map.get("key") 來搜尋!
3. 方法(就是引擎上下文對象的屬性的方法)
$變量名.方法([入參1[, 入參2]*]?), 正常寫法
${變量名.方法([入參1[, 入參2]*]?)}, 正規寫法
$!變量名.方法([入參1[, 入參2]*]?), 正常寫法
$!{變量名.方法([入參1[, 入參2]*]?)}, 正規寫法
引用方法實際就是方法調用操作,關注點傳回值、入參和副作用的情況如下:
- 方法的傳回值将輸出到最終結果中
- 入參的資料類型
$變量 或 $屬性,資料類型參考第一小節;
範圍操作符(如:[1..2]或[$arg1..$arg2]),将作為java.util.ArrayList處理
字典字面量(如:{a:"a",b:"b"}),将作為java.util.Map處理
數字字面量(如:1),将自動裝箱或拆箱比對方法定義中的int或Integer入參
- 副作用
// 若操作如java.util.Map.put方法,則會修改Java代碼部分中的Map對象鍵值對
$map.put("key", "new value")
指令
指令主要用于定義重用子產品、引入外部資源、流程控制。指令以 # 作為起始字元。
#set:向引擎上下文對象添加屬性或對已有屬性進行修改
格式: #set($變量 = 值) ,具體示例如下:
#set($var1 = $other)
#set($var1.prop1 = $other)
#set($var = 1)
#set($var = true)
#set($var = [1,2])
#set($var = {a:"a", b:"b"})
#set($var = [1..3])
#set($var = [$arg1..$arg2])
#set($var = $var1.method())
#set($var = $arg1 + 1)
#set($var = "hello")
#set($var = "hello $var1") // 雙引号可實作字元串拼接(coffeescript也是這樣哦!),假設$var1為fsjohnhuang,則$var為hello fsjohnhuang
#set($var = 'hello $var1') // 單引号将不解析其中引用,假設$var1為fsjohnhuang,則$var為hello $var1
作用域明顯是全局有效的。
#if:條件判斷
格式:
#if(判斷條件)
.........
#elseif(判斷條件)
.........
#else
.........
#end
通過示例了解判斷條件:
#if($name) //$name不為false、null和undefined則視為true
$name
#elseif($job == "net") // ==和!=兩邊的變量将調用其toString(),并對比兩者的傳回值
Net工程師
#elseif($age <= 16) // 支援<=,>=,>,<邏輯運算符
未成年勞工
#elseif(!$married) // 支援!邏輯運算符
未婚
#elseif($age >= 35 && !$married) // 支援&&,||關系運算符
大齡未婚青年
#end
#foreach:循環
格式:
#foreach($item in $items)
..........
#end
$item 的作用範圍為#foreach循環體内。
$items 的資料類型為 Object[]數組 、 [1..2] 、 [1,2,3,4] 、 {a:"a",b:"b"} 和含 public Iterator iterator() 方法的對象,具體如下:
java.util.Collection子類,Velocity會調用其iterator方法擷取Iterator對象
java.util.Map子類,Velocity會調用value()擷取Collection對象,然後調用調用其iterator方法擷取Iterator對象
java.util.Iterator對象,直接将該Iterator對象添加到上下文對象中時,由于Iterator對象為隻進不退的操作方式,是以無法被多個#foreach指令周遊
java.util.Enumeration對象,直接将該Enumeration對象添加到上下文對象中時,由于Iterator對象為隻進不退的操作方式,是以無法被多個#foreach指令周遊
内置屬性$foreach.count ,用于訓示目前循環的次數,從0開始。可以通過配置項 directive.foreach.maxloops 來限制最大的循環次數,預設值為-1(不限制)。
示例——使用Vector和Iterator的差別:
模闆:
#macro(show)
#foreach($letter in $letters)
$letter
#end
#end
#show()
java代碼:
Vector<String> v = new Vector<String>();
v.add("a");
v.add("b");
VelocityContext ctx = new VelocityContext();
ctx.put("letters",v);
Template t = Velocity.getTemplate("模闆路徑");
StringWriter sw = new StringWriter();
t.merge(ctx,sw);
System.out.println(sw.toString());
// 結果
// a
// b
// a
// b
ctx.put("letters",v.iterator());
// 結果
// a
//
#break:跳出循環
#foreach($item in $items)
#if($item == "over")
#break;
#end
$item
#end
#stop:中止模闆解析操作
#set($cmd="stop")
$cmd
#if($cmd == "stop")
#stop
#end
$cmd // 該語句将不執行
#include引入外部資源
(引入的資源不被引擎所解析)
格式: #include(resource[ otherResource]*)
resource、otherResource可以為單引号或雙引号的字元串,也可以為$變量,内容為外部資源路徑。注意為相對路徑,則以引擎配置的檔案加載器加載路徑作為參考系,而不是目前模闆檔案的路徑為參考系。
#parse引入外部資源
(引入的資源将被引擎所解析)
格式: #parse(resource)
resource可以為單引号或雙引号的字元串,也可以為$變量,内容為外部資源路徑。注意為相對路徑,則以引擎配置的檔案加載器加載路徑作為參考系,而不是目前模闆檔案的路徑為參考系。
由于#parse指令可能會引起無限遞歸引入的問題,是以可通過配置項 directive.parse.max.depth來限制最大遞歸引入次數,預設值為10.
#macro:定義重用子產品(可帶參數)
定義格式:
#macro(宏名 [$arg[ $arg]*]?)
.....
#end
調用格式:
#宏名([$arg[ $arg]]?)
示例1——定義與調用位于同一模闆檔案時,無需遵守先定義後使用的規則:
#log("What a happy day")
#macro(log $msg)
log message: $msg
#end
示例2——定義與調用位于不同的模闆檔案時,需要遵守先定義後使用的規則:
## 模闆檔案macro.vm#macro(log $msg)
log message: $msg
#end
## 模闆檔案main.vm
#parse("macro.vm")
#log("What a happy day")
原了解析:Velocity引擎會根據模闆生成文法樹并緩沖起來然後再執行,是以宏定義和調用位于同一模闆檔案時,調用宏的時候它已經被引擎識别并初始化了(類似js中的hosit)。
若定義與調用位于不同的模闆檔案中時,由于 #parse 是引擎解析模闆檔案時才被執行來引入外部資源并對其中的宏定義進行初始化,是以必須遵循先定義後使用的規則。
我們可配置全局宏庫,配置方式如下:
Properties props = new Properties();
// velocimacro.library的值為模闆檔案的路徑,多個路徑時用逗号分隔
// velocimacro.library的預設值為VM_global_library.vm
props.setProperty("velocimacro.library", "global_macro1.vm,global_macro2.vm");
VelocityEngine ve = new VelocityEngine(props);
另外#macro還有另一種傳參方式——$!bodyContent
#macro(say)
$!bodyContent
#end
#@say()Hello World#end
// 結果為Hello World
#define:定義重用子產品(不帶參數)
#define($log)
hello log!
#end
$log
可視為弱版#macro,一般不用,了解就好了。
#evaluate:動态計算
示例:
#set($name = "over")
#evalute("#if($name=='over')over#{else}not over#end") // 輸出over
一般不用,了解就好了。
轉義符
通過 \ 對 $ 和 #進行轉義,導緻解析器不對其進行解析處理。
#set($test='hello')
$test ## 結果:hello
\$test ## 結果:$test
\\$test ## 結果:\hello
\\\$test ## 結果:\$test
$!test ## 結果:hello
$\!test ## 結果:$!test
$\\!test ## 結果:$\!test
$\\\!test ## 結果:$\\!test
模闆實踐
内容引自:https://www.cnblogs.com/fsjohnhuang/p/4112328.html 肥仔
示例結果是生成如下的html表單:
<form action="./submit">
<div>
<label for="title">标題:</label>
<input type="text" id="title" name="title"/>
</div>
<div>
<label for="brief">摘要:</label>
<input type="text" id="brief" name="brief"/>
</div>
<div>
<label for="sex">性别:</label>
<select id="sex" name="sex">
<option value="0">男</option>
<option value="1">女</option>
</select>
</div>
<div>
<label for="job">職業:</label>
<select id="job" name="job">
<option value="0">Java工程師</option>
<option value="1">Net工程師</option>
</select>
</div>
</form>
引入依賴項——velocity-1.7-dep.jar
模闆檔案frm.vm
##表單模闆
##@author fsjohnhuang
##@version 1.0
## 引入外部模闆檔案
#parse('macro.vm')
## 主邏輯
<form action="$action">
#foreach($input in $inputs)
#input($input.title $input.id)
#end
#foreach($select in $selects)
#select($select.title $select.id $select.items)
#end
</form>
模闆檔案macro.vm
## 生成input表單元素區域的宏
#macro(input $title $id)
<div>
<label for="$id">$title</label>
<input type="text" id="$id" name="$id"/>
</div>
#end
## 生成select表單元素區域的宏
#macro(select $title $id $items)
<div>
<label for="$id">$title</label>
<select id="$id" name="$id">
## VTL指令緊貼左側才能確定結果的排版正常(不會有多餘空格)
#foreach($key in $items.keySet())
<option value="$key">$items.get($key)</option>
#end
</select>
</div>
#end
Java代碼
public static void main(String[] args) {
// 初始化模闆引擎
Properties props = new Properties();
props.put("file.resource.loader.path", ".\\vm");
VelocityEngine ve = new VelocityEngine(props);
// 配置引擎上下文對象
VelocityContext ctx = new VelocityContext();
ctx.put("action", "./submit");
ArrayList<HashMap<String, String>> inputs = new ArrayList<HashMap<String,String>>();
HashMap<String, String> input1 = new HashMap<String, String>();
input1.put("id", "title");
input1.put("title", "标題:");
inputs.add(input1);
HashMap<String, String> input2 = new HashMap<String, String>();
input2.put("id", "brief");
input2.put("title", "摘要:");
inputs.add(input2);
ctx.put("inputs", inputs);
ArrayList<HashMap<String, Object>> selects = new ArrayList<HashMap<String,Object>>();
HashMap<String, Object> select1 = new HashMap<String, Object>();
selects.add(select1);
select1.put("id", "sex");
select1.put("title", "性别:");
HashMap<Integer, String> kv1 = new HashMap<Integer, String>();
kv1.put(0, "男");
kv1.put(1, "女");
select1.put("items", kv1);
HashMap<String, Object> select2 = new HashMap<String, Object>();
selects.add(select2);
select2.put("id", "job");
select2.put("title", "職業:");
HashMap<Integer, String> kv2 = new HashMap<Integer, String>();
kv2.put(0, "Java工程師");
kv2.put(1, "Net工程師");
select2.put("items", kv2);
ctx.put("selects", selects);
// 加載模闆檔案
Template t = ve.getTemplate("test.vm");
StringWriter sw = new StringWriter();
// 渲染模闆
t.merge(ctx, sw);
System.out.print(sw.toString());
}
參考文章
開發指南原文位址:
http://velocity.apache.org/engine/devel/developer-guide.html#Introduction
使用者指南原文位址:
http://velocity.apache.org/engine/devel/user-guide.html
中文翻譯開發指南位址:
https://ifeve.com/velocity-guide/
肥仔 john優秀網文位址:
https://www.cnblogs.com/fsjohnhuang/p/4114653.html