最近工作不是很忙,所以想利用这段空闲时间学习学习。之前在微信上看到一篇关于FreeMarker的文章(点击这里查看),加之现在的项目中使用到了FreeMarker,当时为了急于完成在网上找了些速成的资料,所以我决定利用这段时间学习学习FreeMarker。阅读了官方文档,总结了一些自己觉得还挺有用的东西,整理在这里。
一、FreeMarker是个啥玩意儿?
官方文档上这么说:
FreeMarker是一个模板引擎,基于模板和交换数据的用于创建文本输出的工具。作为一个java类库,可以作为一个组件供程序员嵌入他们的产品中。
例如,众所周知的MVC架构模式,在这其中FreeMarker的作用其实相当于JSP,可以作为view层进行数据的展示。
既然可以MVC了,那么view层和controller层就可以分开开发了,所以我们需要了解一下模板设计者和程序设计者都需要干点啥。
二、模板设计者需要干点啥?
官方文档上这么说:
模板+数据模型=输出。
所以一个有点牛逼的模板设计者需要知道下面的这些东西。
1.关于模板那些事
FreeMarker的模板使用的是FTL(FreeMarker Tag Language),类似于HTML的语法,归纳如下:
1)模板上有点啥?
${}:表达式输出,使用${}会将其中的内容进行输出
FTL tags:标签。与html标签类似,但是必须以#开头,例如<#list>#list>。Freemaker自带的标签以#开头,用户自定义的标签以@开头。
注释:与html注释类似,使用<#-- -->,但与html注释不同的是,html注释会将注释体也输出到页面上,但是freemaker不会输出注释体。
在模版中,只要不是${},FTL tags,注释的内容,freemaker都会认为其是静态的文本,会直接将其输出。
2)模板咋写啊?
通过第一点可以知道FreeMarker类似于HTML,但是它又与HTML有些许的不同,FreeMarker有自己的一些语法,总结如下:
i.If判断:
语法:
<#if expression>
<#elseif expression >
<#else>
#if>
ii.集合迭代:
<#list 集合名 as 变量名>
${变量名.属性}
#list>
如果集合里元素为0,那么迭代集合的时候,依然会输出list标签中的内容。例如:
<#list studentsList as student>
id:${student.id}
name:${student.name}
#list>
如果studentsList没有元素,那么依旧会有如下输出
id:
name:
所以为了避免这一问题。可以使用如下的表示:
<#list studentsList>
<#items as student>
id:${student.id}
name:${student.name}
#items>
#list>
如果集合元素为空,那么还可以使用<#else>来进行为空时的操作,这时就会去do something,例如:
<#list studentsList>
<#items as student>
id:${student.id}
name:${student.name}
#items>
<#else>
Do Something…
#list>
Fruits: <#list misc.fruits as fruit>${fruit}<#sep>, #list>
那么就会将输出的水果用逗号分开,like Fruits: orange, banana
对于集合而言他有下面的属性:
集合本身有以下属性:
index:索引,从0开始
counter:计数,从1开始
item_parity:奇偶性,返回当前counter的奇偶性
iii.引用<#include url>
使用这个标签,可以引入外部的另一段内容到当前的文件中。
iv.内嵌函数:
Freemaker内置了很多方便好用的函数,不同于java的以.调用,freemaker以?调用。例如:
upper_case:大写转换
cap_first:首字母大写
length:获取长度
size:获取集合大小
3)要是有变量找不到咋整?
在freemarker中,一个不存在的变量a和a被赋值为null是一样的。
在freemarker中使用!为其设定一个默认值来解决变量缺失的问题。例如:
4)一个很重要但是很简单的东西,数据模型,data-model:
Freemarker的数据模型其实就是一个hash,可以将其理解为容器,存放了很多的变量与值,和java的collection是一个概念。
5)FreeMarker的几员虾兵蟹将(数据类型):
FreeMarker中的数据类型和其他编程语言的变量类型类似,但是它还可以为一个变量设置多个类型。例如,mouse既可以是字符串类型,又可以是一个hash。就像是这样,mouse=”Jerry”,mouse.size=”small”,mouse.price=20。那么有如下的输出:
${mouse}作为字符串输出为Jerry,${mouse.size}作为hash输出为small,${mouse.prince}作为hash输出为20。
i.基本变量类型:
字符串
--使用索引来获取字符串的单个字符,例如:user=”Tom”,user[0]就输出T。
--使用字符串+range的形式来获取子字符串。例如:a=”ABCDEF”,a[1..3]就输出BCD
--字符串操作可以使用+来进行拼接。但是可能会因为数值格式,日期格式等因素而导致拼接的结果不正确,或者解析异常。例如:”url?id”+id有可能会出现url?id=1 234的情况。为了避免这种情况的发生,可以使用FreeMarker内置的函数?c来将其进行转换,就像是这样,”url?id”+id?c或者”url?id=${id?c}”。
--数字字符串不能与数值进行运算。只能使用+操作进行字符串拼接。
数值
--数值表达0.02时,必须有小数点前的0,例如.02是错误的。
布尔类型Boolean
日期类型Date
ii.容器:
hash
--使用+连接多个hash
序列:类似于java的list,序列中的元素以逗号分隔
--在序列中会使用到range,有以下用法:
start..end:含头含尾
start..
--使用+连接多个序列。例如[1,2]+[4,5]生成的新序列就是[1,2,4,5]。
--使用序列[range]的形式来获取子序列。例如:<#list [1,2,3,4,5][1..3] as i>${i}#list>输出的就是[2,3,4]
集合:类似于java的map
6)还有一些面包渣:
i.关于range:
--range表达式本身不用带方括号,只有跟在序列后才需要方括号。
例如:<#assgin myRange=0..
<#assgin myRange=[0..
<#assgin mySeq[0..
--range表达式操作符两边可以进行计算,而不需要将两边的计算使用圆括号包裹起来。
例如,x+1..y-2
--操作符是一体的,他们之间不能有空格。
--range并不真正存储它所包含的数值,因此0..1和0..100000000在创建和内存消耗上是一样的。
ii.FreeMarker Template的结构:
包含文本,插值,FTL标签和注释。
FTL区分大小写。
FTL标签内部不允许再使用标签,例如:
<#if <#include “foo”>=”bar”>…#if>,这是错误的用法。
注释可以写在任何正常的位置,只要保持正确的注释格式即可。
iii.注意:
--最好是将复杂的表达式用括号括起来,以便阅读和理解。
--对于一个负的序列索引,FreeMarker将会解析错误。
--使用??来判断一个值是否为空,返回true和false两个值。
--如果向自定义标签传值,不需要${},直接将值的变量代入即可。
三、程序开发者需要干点啥?
首先介绍两个非常重要的类:
freemarker.template.Configuration:模板,环境等的配置对象。
freemarker.template.Template:模板对象
首先来看一段代码:
public class MyTest {
private Configuration configuration=new Configuration(Configuration.VERSION_2_3_23);
private Template template=null;
/*
* configuration加载模版的三种方式:
1.根据类路径加载模版
public void setClassForTemplateLoading(Class clazz, String pathPrefix);
2.根据文件系统加载模版
public void setDirectoryForTemplateLoading(File dir) throws IOException;
3.根据servlet上下文加载模版
public void setServletContextForTemplateLoading(Object servletContext, String path);
*
*/
public MyTest() {
configuration.setClassForTemplateLoading(MyTest.class, "/config/template");
configuration.setDefaultEncoding("UTF-8");
//开发阶段使用
configuration.setTemplateExceptionHandler(TemplateExceptionHandler.DEBUG_HANDLER);
/*
* 生产阶段使用
configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
*/
try {
//通过setSharedVariable可以将一些变量设置为公共的共享的变量,在任何使用该configuration获取template的模版中都可以访问共享变量
configuration.setSharedVariable("author", "Earl");
} catch (TemplateModelException e) {
e.printStackTrace();
}
}
/**
* 获取模板
* @param templatePath
* @return
* @throws TemplateNotFoundException
* @throws MalformedTemplateNameException
* @throws ParseException
* @throws IOException
*/
private Template getTemplate(String templatePath) throws TemplateNotFoundException, MalformedTemplateNameException, ParseException, IOException{
template=configuration.getTemplate(templatePath);
return template;
}
/**
* 向模板传递集合,迭代生成xml文件
*/
@Test
public void testManyPerson(){
List persons=new ArrayList();
Person p1=new Person("王一", 1, 20);
Person p2=new Person("张二", 2, 21);
Person p3=new Person("刘三", 1, 22);
Person p4=new Person("李四", 2, 23);
Person p5=new Person("赵五", 2, 24);
Person p6=new Person("孙六", 1, 25);
Person p7=new Person("杨七", 2, 26);
Person p8=new Person("孔八", 1, 27);
persons.add(0,p1);
persons.add(1,p2);
persons.add(2,p3);
persons.add(3,p4);
persons.add(4,p5);
persons.add(5,p6);
persons.add(6,p7);
persons.add(7,p8);
Map personMap=new HashMap();
personMap.put("persons", persons);
templateProcess(personMap, "d://persons.xml", "person_list_test.flt");
}
/**
* 向模板传递一个对象,测试${}
*/
@Test
public void testOnePerson(){
Person p1=new Person("王麻子", 1, 20);
Map personMap=new HashMap();
personMap.put("person", p1);
templateProcess(personMap,"d://person.xml","person_test.flt");
}
/**
* 向模板传递一个集合,并将模板返回的html代码输出
*/
@Test
public void testHtml(){
List persons=new ArrayList();
Person p1=new Person("王一", 1, 20);
Person p2=new Person("张二", 2, 21);
Person p3=new Person("刘三", 1, 22);
Person p4=new Person("李四", 2, 23);
Person p5=new Person("赵五", 2, 24);
Person p6=new Person("孙六", 1, 25);
Person p7=new Person("杨七", 2, 26);
Person p8=new Person("孔八", 1, 27);
persons.add(0,p1);
persons.add(1,p2);
persons.add(2,p3);
persons.add(3,p4);
persons.add(4,p5);
persons.add(5,p6);
persons.add(6,p7);
persons.add(7,p8);
Map personMap=new HashMap();
personMap.put("persons", persons);
personMap.put("title", "person");
StringWriter sw = templateProcess(personMap,"html_test.flt");
System.out.println(sw.toString());
}
/**
* 测试方法的调用
*/
@Test
public void testMethodCall(){
String s1="hello,world";
String s2="你好,世界";
Map paraMap=new HashMap();
paraMap.put("s1", s1);
paraMap.put("s2", s2);
paraMap.put("StringLength", new StringLengthMethod());
System.out.println(templateProcess(paraMap, "method_call_test.flt"));
}
/**
* 测试用户自定义标签
*/
@Test
public void testUserDirective(){
String s1="hello,world";
Map paraMap=new HashMap();
paraMap.put("s1", s1);
paraMap.put("uppercase", new UpperCaseDirective());
System.out.println(templateProcess(paraMap, "user_directive_test.flt"));
}
/**
* 将生成的内容写入字符串
* @param paraMap 数据集
* @param templateName 模版名称
* @return 生成的内容
*/
private StringWriter templateProcess(Map paraMap,String templateName) {
StringWriter sw=new StringWriter();
try {
template=getTemplate(templateName);
template.process(paraMap, sw);
} catch (TemplateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
try {
sw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sw;
}
/**
* 将数据集与模版结合,生成文件
* @param paraMap 数据集
* @param outputPath 生成文件输出路径
* @param templateName 模版名称
*/
private void templateProcess(Map paraMap,String outputPath,String templateName) {
Writer out=null;
try {
out=new FileWriter(outputPath);
template=getTemplate(templateName);
template.process(paraMap, out);
} catch (IOException e) {
e.printStackTrace();
} catch (TemplateException e) {
e.printStackTrace();
}finally{
if(out!=null){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FreeMarker的开发使用其实还是蛮简单的,大概过程是使用Configuration配置模板路径,再使用Template得到模板,得到模板之后,就可以根据需求对模板进行操作了。基本的使用可以参考上面代码中的testOnePerson(),testManyPerson(),testHtml()。
下面来说说方法的调用以及用户的自定义标签。
--方法的调用:
用户自定义的方法的开发需要实现freemarker.template.TemplateMethodModelEx,并实现其exec(java.util.List arg0)。开发还是比较简单的,如下:
public class StringLengthMethod implements TemplateMethodModelEx {
@Override
public Object exec(List arg0) throws TemplateModelException {
//arg0就是调用方法的参数集合。
//在模版中无需关心参数,只需将需要的参数进行传递即可
//在此处根据需要取得集合中的参数
String paraStr=(String) arg0.get(0).toString();
SimpleNumber length=new SimpleNumber(paraStr.length());
return length;
}
}
在使用时,需要将方法对象传递到模板上,可参考如下的代码:
/**
* 测试方法的调用
*/
@Test
public void testMethodCall(){
String s1="hello,world";
String s2="你好,世界";
Map paraMap=new HashMap();
paraMap.put("s1", s1);
paraMap.put("s2", s2);
paraMap.put("StringLength", new StringLengthMethod());
System.out.println(templateProcess(paraMap, "method_call_test.flt"));
}
同时在模板上如下使用即可:${StringLength(s1)}
这样的话就可以通过${}输出字符串s1的长度了。
--用户自定义标签:
用户自定义标签类需要实现freemarker.template.TemplateDirectiveModel,并实现其execute(Environment arg0,Map arg1, TemplateModel[] arg2, TemplateDirectiveBody arg3)方法,如下:
public class UpperCaseDirective implements TemplateDirectiveModel {
@Override
public void execute(Environment arg0, Map arg1, TemplateModel[] arg2,
TemplateDirectiveBody arg3) throws TemplateException, IOException {
//arg1为自定义标签的属性,key存储的是属性名,value存储的是相应的值。
//arg0为系统环境变量,用于输出
//arg3为标签体,生成标签
//arg2为循环代替变量
String attr=arg1.get("name").toString();
//官方文档推荐使用DefaultObjectWrapper或者用户自定义的继承自DefaultObjectWrapper的子类wrapper
//官方文档不推荐使用DefaultObjectWrapper的构造方法创建wrapper对象,推荐使用DefaultObjectWrapperBuilder,以保证wrapper为单例
DefaultObjectWrapper wrapper=new DefaultObjectWrapperBuilder(new Version("2.3.23")).build();
arg0.setVariable("upperName", wrapper.wrap(attr.toUpperCase()));
arg3.render(arg0.getOut());
}
}
在使用时,同样需要将自定义标签对象传递到模板上,如下:
/**
* 测试用户自定义标签
*/
@Test
public void testUserDirective(){
String s1="hello,world";
Map paraMap=new HashMap();
paraMap.put("s1", s1);
paraMap.put("uppercase", new UpperCaseDirective());
System.out.println(templateProcess(paraMap, "user_directive_test.flt"));
}
同时在模板上使用<@uppercase name=s1>${upperName}@uppercase>,upperName就是在自定义标签类中处理完成输出的数据。使用用户自定义的标签,一定是以@开头,不是以#开头了。
总结:以上便是我最近一小段时间对FreeMarker的一些了解。本文开头我提到的那篇关于FreeMarker的文章,里面对比了FreeMarker和jsp。其实在我看来,实际应用中,解决问题的办法很多,没有必要非得说哪个更好,两者在解决各自擅长的问题上都是有很大的优势的。但是可能我的认识里,我感觉在我们伟大的祖国应该是鲜有公司使用FreeMarker作为jsp的替代品的,毕竟中国的程序员是需要前端后端通吃的。这就勾起了我曾经看过的一篇关于印度程序员的文章的回忆。为何说印度程序员很多,抛开英语是他们的第二语言,就他们本身在工作中的角色而言,他们的分工是很明确的。做前端的就是做前端的,写后台的就是写后台的,他们只需要关心自己的那一部分就好了。但是中国的程序员必须两者兼顾,所以就我目前的工作经验来说,我觉得使用jsp来作为展示,开发调试更加的方便和快捷,而FreeMarker用来做一些小的,例如生成一个邮件模板等我觉得更为合适。
源代码和模板可以点击这里下载