我与Freemarker二三事

版权声明:懂我的人,无需声明。不懂我的人,何须声明。 https://blog.csdn.net/Earl_yuan/article/details/49978309

最近工作不是很忙,所以想利用这段空闲时间学习学习。之前在微信上看到一篇关于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>
	<h1>id:${student.id}</h1>
	<h1>name:${student.name}</h1>
</#list>
如果studentsList没有元素,那么依旧会有如下输出

id:

name:

所以为了避免这一问题。可以使用如下的表示:

<#list studentsList>
	<#items as student>
		<h1>id:${student.id}</h1>
		<h1>name:${student.name}</h1>
	</#items>
</#list>
如果集合元素为空,那么还可以使用<#else>来进行为空时的操作,这时就会去do something,例如:

<#list studentsList>
	<#items as student>
		<h1>id:${student.id}</h1>
		<h1>name:${student.name}</h1>
</#items>
<#else>
	Do Something…
</#list>

可以使用<#sep>something</#sep>来分割输出的内容。例如:

<p>Fruits: <#list misc.fruits as fruit>${fruit}<#sep>, </#list>
那么就会将输出的水果用逗号分开,like <p>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中使用!为其设定一个默认值来解决变量缺失的问题。例如:
<h1>Welcome,${user!”visitor”}</h1>
如果变量user缺失了,那么模版引擎在解析到这里时会输出visitor。如果user没有缺失,那么会依旧只输出user的变量值。
通过.来调用属性,例如animal.python.price,如果使用!来为其设置默认值,需要将整个表达式都括起来,像是这样(animal.python.price)!0。因为如果animal或者python缺失了,那么freemarker解析到这里时就会抛出未定义变量的错误。一旦将其括起来作为一个整体了,那么即使animal或者python缺失了,那么也会返回设置的默认值0。


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..<end或者start..!end:含头不含尾
start..*length:从start开始,找到length个元素。length为正向后查找,反之向前查找
--使用+连接多个序列。例如[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..<x>是正确的。
<#assgin myRange=[0..<x]>是不正确的。
<#assgin mySeq[0..<x]>是正确的。
--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<Person> persons=new ArrayList<Person>();
		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<String, Object> personMap=new HashMap<String, Object>();
		personMap.put("persons", persons);
		templateProcess(personMap, "d://persons.xml", "person_list_test.flt");
	}
	
	/**
	 * 向模板传递一个对象,测试${}
	 */
	@Test
	public void testOnePerson(){
		Person p1=new Person("王麻子", 1, 20);
		Map<String, Object> personMap=new HashMap<String, Object>();
		personMap.put("person", p1);
		templateProcess(personMap,"d://person.xml","person_test.flt");
	}
	
	/**
	 * 向模板传递一个集合,并将模板返回的html代码输出
	 */
	@Test
	public void testHtml(){
		List<Person> persons=new ArrayList<Person>();
		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<String, Object> personMap=new HashMap<String, Object>();
		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<String, Object> paraMap=new HashMap<String, Object>();
		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<String, Object> paraMap=new HashMap<String, Object>();
		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<String, Object> 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<String, Object> 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<String, Object> paraMap=new HashMap<String, Object>();
		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<String, Object> paraMap=new HashMap<String, Object>();
		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用来做一些小的,例如生成一个邮件模板等我觉得更为合适。


源代码和模板可以点击这里下载

猜你喜欢

转载自blog.csdn.net/Earl_yuan/article/details/49978309