一、前言
Velocity作为历史悠久的模板引擎不单单可以替代JSP作为Java Web的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。而且Velocity被移植到不同的平台上,如.Net的 NVelocity和js的Velocity.js,虽然各平台在使用和实现上略有差别,但大部分语法和引擎核心的实现是一致的,因此学习成本降低不少 哦。
最好的学习资源——官网:http://velocity.apache.org/
本系列打算采用如下结构对Velocity进行较为全面的学习,若有不妥或欠缺望大家提出,谢谢。
1. 入门示例
2. VTL语法详解
3. 模板与宿主环境通信
4. 基础配置项
5. 深入模板引擎及调优配置
二、VTL语法详解
VTL的语句分为4大类:注释、直接输出的内容、引用和指令。另外由于VTL中以 # 和 $ 作为关键字起始字符,因此输出它们时需要通过转义符 \ 来将其转换为普通字符。
由于内容较多,特设目录一坨!
三. 注释(行注释、 块注释、 文档注释)
四. 直接输出的内容
五. 引用(变量、属性、方法)
六. 指令(#set、#if、#foreach、#include、#parse、#break、#stop、#macro、#define、#evaluate)
七、转义符
三、注释
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对象。
$变量名.属性, 常规写法
${变量名.属性}, 正规写法
$!变量名.属性, 常规写法
$!{变量名.属性}, 正规写法
属性搜索规则:
Velocity采用一种十分灵活的方式搜索变量的属性, 具体如下:
// 假如引用$var.prop,那么Velocity将对prop进行变形,然后在$var对象上尝试调用
// 变形和尝试的顺序如下
1. $var.getprop()
2. $var.getProp()
3. $var.get("prop")
4. $var.isProp()
// 对于$var.Prop则如下
1. $var.getProp()
2. $var.getprop()
3. $var.get("Prop")
4. $var.isProp()
因此获取 java.util.Map 对象的键值时可以简写为 $map.key ,Velocity会自动转为 $map.get("key") 来搜索!
$变量名.方法([入参1[, 入参2]*]?), 常规写法
${变量名.方法([入参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入参
3. 副作用
// 若操作如java.util.Map.put方法,则会修改Java代码部分中的Map对象键值对
$map.put("key", "new value")
六、指令
指令主要用于定义重用模块、引入外部资源、流程控制。指令以 # 作为起始字符。
1. #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(判断条件)
.........
#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($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()
#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
//
#foreach($item in $items)
#if($item == "over")
#break;
#end
$item
#end
#set($cmd="stop")
$cmd
#if($cmd == "stop")
#stop
#end
$cmd // 该语句将不执行
6. #include:引入外部资源(引入的资源不被引擎所解析)
格式: #include(resource[ otherResource]*)
resource、otherResource可以为单引号或双引号的字符串,也可以为$变量,内容为外部资源路径。注意为相对路径,则以引擎配置的文件加载器加载路径作为参考系,而不是当前模板文件的路径为参考系。
7. #parse:引入外部资源(引入的资源将被引擎所解析)
格式: #parse(resource)
resource可以为单引号或双引号的字符串,也可以为$变量,内容为外部资源路径。注意为相对路径,则以引擎配置的文件加载器加载路径作为参考系,而不是当前模板文件的路径为参考系。
由于#parse指令可能会引起无限递归引入的问题,因此可通过配置项 directive.parse.max.depth来限制最大递归引入次数,默认值为10.
定义格式:
#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($log)
hello log!
#end
$log
可视为弱版#macro,一般不用,了解就好了。
示例:
#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
八、总结
VTL语法部分KO了,接下来就是模板与宿主环境通信——核心在引擎上下文对象(VelocityContext)上!