后端|Velocity开发指南

后端|Velocity开发指南
文章图片

简介 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以上

后端|Velocity开发指南
文章图片

Velocity2.2的maven 依赖

org.apache.velocity velocity-engine-core 2.2 ? org.apache.velocity.tools velocity-tools-generic 3.0 ? org.apache.velocity.tools velocity-tools-view 3.0 ? org.apache.velocity.tools velocity-tools-view-jsp 3.0


其它所有版本官方下载地址: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 baseCtx = new HashMap(); baseCtx.put("version", "1"); VelocityContext ctx = new VelocityContext(baseCtx); ? // 2. 通过put传值 ctx.put("author", "fsjohnhuang");

【后端|Velocity开发指南】
注意键值对中值的数据类型为
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")
.........

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 people = new HashMap(); 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

velocityorg.apache.velocity.tools.view.VelocityViewServlet?org.apache.velocity.toolbox/WEB-INF/tools.xml ? org.apache.velocity.properties/WEB-INF/velocity.properties ?velocity *.vm


tools.xml就像定义了一个工具箱,里面放着很多工具,比如有个“扳手”。
具体示例:考虑考虑让我们的朋友乔恩(Jon)从真实的工具箱中抓取我们的“扳手”。乔恩只需要知道我们想要哪个扳手。他不需要知道扳手做什么,也不需要知道我们打算如何做。
Velocity Toolbox的工作方式与上面的例子相同,我们仅需指定所需的工具,然后Velocity引擎就可以在vm模板中使用任何在工具箱Tool.xml中定义好的公共方法来处理其余的工作。

PipeWrench.java
public class PipeWrench { public String getSize() { return "Large Pipe Wrench!"; } }

tools.xml

.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对象上尝试调用 // 变形和尝试的顺序如下
  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") 来搜索!
3. 方法(就是引擎上下文对象的属性的方法)
$变量名.方法([入参1[, 入参2]*]?), 常规写法 ${变量名.方法([入参1[, 入参2]*]?)}, 正规写法 $!变量名.方法([入参1[, 入参2]*]?), 常规写法 $!{变量名.方法([入参1[, 入参2]*]?)}, 正规写法

引用方法实际就是方法调用操作,关注点返回值、入参和副作用的情况如下:
  1. 方法的返回值将输出到最终结果中
  1. 入参的数据类型
$变量 或 $属性,数据类型参考第一小节; 范围操作符(如:[1..2]或[$arg1..$arg2]),将作为java.util.ArrayList处理 字典字面量(如:{a:"a",b:"b"}),将作为java.util.Map处理 数字字面量(如:1),将自动装箱或拆箱匹配方法定义中的int或Integer入参

  1. 副作用
// 若操作如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 v = new Vector(); 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表单:

引入依赖项——velocity-1.7-dep.jar
模板文件frm.vm
##表单模板 ##@author fsjohnhuang ##@version 1.0 ## 引入外部模板文件 #parse('macro.vm') ## 主逻辑
#foreach($input in $inputs) #input($input.title $input.id) #end #foreach($select in $selects) #select($select.title $select.id $select.items) #end

模板文件macro.vm
## 生成input表单元素区域的宏 #macro(input $title $id) #end ## 生成select表单元素区域的宏 #macro(select $title $id $items)## VTL指令紧贴左侧才能确保结果的排版正常(不会有多余空格) #foreach($key in $items.keySet()) #end#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 inputs = new ArrayList(); HashMap input1 = new HashMap(); input1.put("id", "title"); input1.put("title", "标题:"); inputs.add(input1); HashMap input2 = new HashMap(); input2.put("id", "brief"); input2.put("title", "摘要:"); inputs.add(input2); ctx.put("inputs", inputs); ArrayList selects = new ArrayList(); HashMap select1 = new HashMap(); selects.add(select1); select1.put("id", "sex"); select1.put("title", "性别:"); HashMap kv1 = new HashMap(); kv1.put(0, "男"); kv1.put(1, "女"); select1.put("items", kv1); HashMap select2 = new HashMap(); selects.add(select2); select2.put("id", "job"); select2.put("title", "职业:"); HashMap kv2 = new HashMap(); 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

    推荐阅读