相关文章推荐
至今单身的茄子  ·  Attempts to generate ...·  9 月前    · 
威武的板凳  ·  yarn命令问题:error ...·  1 年前    · 

1、程序开发入门

1.1 创建配置实例

首先,你应该创建一个freemarker.template.Configuration的实例,然后调整它的设置。Configuration实例是存储FreeMarker应用级设置的核心部分。同时,它也处理创建和缓存预解析模板的工作。也许你只在应用(可能是servlet)生命周期的开始执行它一次:

Configuration cfg = new Configuration(); // 指定模板文件从何处加载的数据源,这里设置成一个文件目录。 cfg.setDirectoryForTemplateLoading( new File("/where/you/store/templates")); // 指定模板如何检索数据模型,这是一个高级的主题了… // 但先可以这么来用: cfg.setObjectWrapper(new DefaultObjectWrapper());

从现在开始,应该使用单实例配置。要注意不管一个系统有多少独立的组件来使用FreeMarker,它们都会使用他们自己私有的Configuration实例。

1.2 创建数据模型

在简单的示例中你可以使用java.lang和java.util包下的类,还有用户自定义的Java Bean来构建数据对象。      使用java.lang.String来构建字符串。      使用java.lang.Number来派生数字类型。      使用java.lang.Boolean来构建布尔值。      使用java.util.List或Java数组来构建序列。      使用java.util.Map来构建哈希表。      使用你自己定义的bean类来构建哈希表,bean中的项和bean的属性对应。例如product中的price属性可以用product.price来获取

构建数据模型的java代码

// 创建根哈希表 Map root = new HashMap(); // 在根中放入字符串"user" root.put("user", "Big Joe"); // 为"latestProduct"创建哈希表 Map latest = new HashMap(); // 将它添加到根哈希表中 root.put("latestProduct", latest); // 在latest中放置"url"和"name" latest.put("url", "products/greenmouse.html"); latest.put("name", "green mouse");

对于latestProduct你也可以使用有url和name属性的Java Bean(也就是说,对象要有公共的String getURL()和String getName()方法);它和模板的观点相同。

1.3 获得模板

模板代表了freemarker.template.Template的实例。典型的做法是从Configuration实例中获取一个Template实例。无论什么时候你需要一个模板实例,都可以使用它的getTemplate方法来获取。在之前设置的目录中,用test.ftl存储示例模板,那么就可以这样来做:

Template temp = cfg.getTemplate("test.ftl");

当调用这个方法的时候,将会创建一个test.ftl的Template实例,通过文件读取,然后解析(编译)它。Template实例以解析后的形式存储模板,而不是以源文件的文本形式。 Configuration缓存Template实例,当再次获得test.ftl时,它可能不会创建新的Template实例(因此不会读取和解析文件),而是返回第一次创建的实例。

1.4 合并模板和数据模型

我们都已经知道的,数据模型+模板=输出,我们已经有了一个数据模型(root)和一个模板(temp)了,所以为了得到输出就需要合并它们。这是由模板的process方法完成的。它用数据模型的根和Writer对象作为参数,然后向Writer对象写入产生的内容。为简单起见,这里我们只做标准的输出:

Writer out = new OutputStreamWriter(System.out); temp.process(root, out); out.flush();

一旦获得了Template实例,就能将它和不同的数据模型进行不限次数(Template实例是无状态的)的合并。此外,当Template实例创建之后test.ftl文件才能访问,而不是调用处理方法时。

2、 数据模型

现在,我们已经知道如何使用基本的Java类(Map,String等)构建一个数据模型了。在内部,模板中可用的变量都是实现了freemarker.template.TemplateModel接口的Java对象。但在你自己的数据模型中,可以使用基本的Java集合类作为变量,因为这些变量会在内部被替换为适当的TemplateModel类型。这种功能特性被称作是object wrapping对象包装。对象包装功能可以透明地把任何类型的对象转换为实现了TemplateModel接口类型的实例。这就使得下面的转换成为可能,如在模板中把java.sql.ResultSet转换为序列变量,把javax.servlet.ServletRequest对象转换成包含请求属性的哈希表变量,甚至可以遍历XML文档作为FTL变量。包装(转换)这些对象,需要使用合适的,也就是所谓的对象包装器实现(可能是自定义的实现);这将在后面讨论。现在的要点是想从模板访问任何对象,它们早晚都要转换为实现了TemplateModel接口的对象。那么首先你应该熟悉来写TemplateModel接口的实现类。

有一个freemarker.template.TemplateModel粗略的子接口对应每种基本变量类型(TemplateHashModel对应哈希表,TemplateSequenceModel对应序列,TemplateNumberModel对应数字等等)。例如,想为模板使用java.sql.ResultSet变量作为一个序列,那么就需要编写一个TemplateSequenceModel的实现类,这个类要能够读取java.sql.ResultSet中的内容。我们常这么说,你使用TemplateModel的实现类包装了java.sql.ResultSet,基本上只是封装java.sql.ResultSet,来提供使用普通的TemplateSequenceModel接口访问它。要注意一个类可以实现多个TemplateModel接口,这就是为什么FTL变量可以有多种类型

注意这些接口的一个细小的实现是和freemarker.template包一起提供的。例如,将一个String转换成FTL的字符串变量,可以使用SimpleScalar,将java.util.Map转换成FTL的哈希表变量,可以使用SimpleHash等等。 如果想尝试自己的TemplateModel实现,一个简单的方式是创建它的实例,然后将这个实例放入数据模型中(也就是把它放在哈希表的根上)。对象包装器将会给模板提供它的原状,因为它已经实现了TemplateModel接口,所以没有转换(包装)的需要。(这个技巧当你不想用对象包装器来包装(转换)某些对象时仍然有用。

(1)标量

有4种类型的标量:     布尔值     数字     字符串     日期 每一种标量类型都是TemplateTypeModel接口的实现,这里的Type就是类型的名称。这些接口只定义了一个方法type getAsType();它返回变量的Java类型(boolean,Number,String和Date各自代表的值)的值。

注意:由于历史遗留的原因,字符串标量的接口是TemplateScalarModel,而不是TemplateStringModel。 这些接口的一个细小的实现和SimpleType类名在freemarker.template包中是可用的。但是却没有SimpleBooleanModel类型;为了代表布尔值,可以使用TemplateBooleanModel.TRUE和TemplateBooleanModel.FALSE来单独使用。 注意:由于历史遗留的原因,字符串标量的实现类是SimpleScalar,而不是SimpleString。 在FTL中标量是一成不变的。当在模板中设置变量的值时,使用其他的实例来替换TemplateTypeModel实例时,是不用改变原来实例中存储的值的。

(2)数据类型的难点

数据类型还有一些复杂,因为Java API通常不区别java.util.Date,只存储日期部分(April 4, 2003),时间部分(10:19:18 PM),或两者都存(April 4, 2003 10:19:18 PM)。为了用文本正确显示一个日期变量,FreeMarker必须知道java.util.Date的哪个部分存储了有意义上的信息,哪部分没有被使用。不幸的是,Java API在这里明确的说,由数据库控制(SQL),因为数据库通常有分离的日期,时间和时间戳(又叫做日期-时间)类型,java.sql有3个对应的java.util.Date子类和它们相匹配。 TemplateDateModel接口有两个方法:分别是java.util.Date getAsDate()和int getDateType()。这个接口典型的实现是存储一个java.util.Date对象,加上一个整数来辨别“数据库存储的类型”。这个整数的值也必须是TemplateDateModel接口中的常量之一:DATE,TIME,DATETIME和UNKNOWN。 什么是UNKNOWN呢?我们之前说过,java.lang和java.util下的类通常被自动转换成TemplateModel的实现类,就是所谓的对象包装器。当对象转换器面对一个java.util.Date对象时,而不是java.sql日期类的实例,它就不能确定“数据库存储的类型”是什么,所以就使用UNKNOWN。往后执行,如果模板需要使用这个变量,操作也需要使用“数据存储的类型”,那就会停止执行并抛出错误。为了避免这种情况的发生,对于那些可能有问题的变量,模板开发人员需要帮助FreeMarker决定“数据库存储的类型”,使用内建函数date,time或datetime就可以解决了。注意一下,如果对要格式化参数使用内建函数string,比如foo?string("MM/dd/yyyy"),那么FreeMarker就不必知道“数据库存储的类型”了。

(3)容器

容器包括哈希表,序列和集合三种类型。

哈希表: FreeMarker中的哈希表是实现了TemplateHashModel接口的Java对象。TemplateHashModel接口有两个方法:TemplateModel get(String key),这个方法根据给定的名称返回子变量,boolean isEmpty()这个方法表明哈希表是否含有子变量。get方法当在给定的名称没有找到子变量时返回null。 TemplateHashModelEx接口扩展了TemplateHashModel接口。它增加了更多的方法,使得可以使用内建函数values和keys来枚举哈希表中的子变量。 经常使用的实现类是SimpleHash,该类实现了TemplateHashModelEx接口。从内部来说,它使用一个java.util.Hash类型的对象存储子变量。SimpleHash类的方法可以添加和移除子变量。这些方法应该用来在变量被创建之后直接初始化。 在FTL中,容器是一成不变的。那就是说你不能添加,替换和移除容器中的子变量。

序列: 序列是实现了TemplateSequenceModel接口的Java对象。它包含两个方法:TemplateModel get(int index)和int size()。 经常使用的实现类是SimpleSequence,该类内部使用一个java.util.List类型的对象存储它的子变量。SimpleSequence有添加子元素的方法。在序列创建之后应该使用这些方法来填充序列。

集合: 集合是实现了TemplateCollectionModel接口的Java对象。这个接口只定义了一个方法:TemplateModelIterator iterator()。TemplateModelIterator接口和java.util.Iterator相似,但是它返回TemplateModel而不是Object,而且它能抛出TemplateModelException异常。 通常使用的实现类是SimpleCollection。

方法: 方法变量在存于实现了TemplateMethodModel接口的模板中。这个接口仅包含一个方法:TemplateModel exec(java.util.List arguments)。当使用方法调用表达式调用方法时,exec方法将会被调用。形参将会包含FTL方法调用形参的值。exec方法的返回值给出了FTL方法调用表达式的返回值。 TemplateMethodModelEx接口扩展了TemplateMethodModel接口。它没有任何新增的方法。事实上这个对象实现这个标记接口暗示给FTL引擎,形式参数应该直接以TemplateModel形式放进java.util.List。否则将会以String形式放入List。 一个很明显的原因是这些接口没有默认的实现。

例如这个方法,返回第一个字符串在第二个字符串第一次出现时的索引位置,如果第二个字符串中不包含第一个字符串,则返回“-1”:

[java] view plain copy 在CODE上查看代码片 派生到我的代码片
  • import freemarker.template.SimpleNumber;
  • import freemarker.template.TemplateMethodModel;
  • import freemarker.template.TemplateModel;
  • import freemarker.template.TemplateModelException;
  • public class IndexOfMethod implements TemplateMethodModel
  • @Override
  • public TemplateModel exec(List arg0) throws TemplateModelException
  • if(arg0.size() != 2)
  • throw new TemplateModelException( "Wrong argments !");
  • return new SimpleNumber(((String)arg0.get( 1)).indexOf((String)arg0.get( 0)));
  • test.ftl:
  • < h1 >welcome ${user} ! </ h1 >
  • < p >our latest products:
  • < a href= "${latestproduct.url}" >${latestproduct.name} </ a >!
  • <#macro repeat count >
  • <#list 1..count as x >
  • <#nested x, x/2, x==count >
  • </#list >
  • </#macro >
  • <@repeat count= 4 ; c, halfc, last >
  • ${c}. ${halfc} <#if last > Last! </#if >
  • </@repeat >
  • <#macro do_thrice >
  • <#nested 1 >
  • <#nested 2 >
  • <#nested 3 >
  • </#macro >
  • <@do_thrice ; x > <#-- 用户自定义指令 使用";"代替"as" -- >
  • ${x} Anything.
  • </@do_thrice >
  • <#import "lib/my_test.ftl" as my >
  • <@my.copyright date= "2014-04-01" />
  • ${my.mail}
  • <#assign x = "something" >
  • ${indexOf("met",x)}
  • ${indexOf("foo",x)}
  • test1.java:
  • import freemarker.template.Configuration;
  • import freemarker.template.DefaultObjectWrapper;
  • import freemarker.template.Template;
  • public class Test1
  • public static void main(String[] args) throws Exception
  • //Configuration实例是存储FreeMarker应用级设置的核心部分
  • Configuration cfg = new Configuration();
  • // 指定模板文件从何处加载的数据源,这里设置成一个文件目录。
  • File path1 = new File(System.getProperty( "user.dir")+ "/src/ceshi");
  • cfg.setDirectoryForTemplateLoading(path1);
  • // 指定模板如何检索数据模型,这是一个高级的主题了…
  • // 但先可以这么来用:
  • cfg.setObjectWrapper( new DefaultObjectWrapper());
  • //获得模板
  • Template temp = cfg.getTemplate( "test.ftl");
  • // 创建根哈希表
  • Map root = new HashMap();
  • // 在根中放入字符串"user"
  • root.put( "user", "xiaoming");
  • // 为"latestProduct"创建哈希表
  • Map latest = new HashMap();
  • // 将它添加到根哈希表中
  • root.put( "latestproduct", latest);
  • // 在latest中放置"url"和"name"
  • latest.put( "url", "products/greenmouse.html");
  • latest.put( "name", "green mouse");
  • root.put( "indexOf", new IndexOfMethod());
  • Writer out = new OutputStreamWriter(System.out);
  • // 将模板和数据模型合并 并输出
  • temp.process(root, out);
  • out.flush();
  • 最终运行结果:

    如果需要访问FTL运行时环境(读/写变量,获取本地信息等),则可以使用Environment.getCurrentEnvironment()来获取。

    指令: Java程序员可以使用TemplateDirectiveModel接口在Java代码中实现自定义指令。详情可以参加API文档。 注意: TemplateDirectiveModel在FreeMarker 2.3.11版本时才加入。用来代替快被废弃的TemplateTransformModel。

    实现一个指令,这个指令可以将在它开始标签和结束标签之内的字符都转换为大写形式。就像这个模板:

    [html] view plain copy 在CODE上查看代码片 派生到我的代码片
  • import freemarker.core.Environment;
  • import freemarker.template.TemplateDirectiveBody;
  • import freemarker.template.TemplateDirectiveModel;
  • import freemarker.template.TemplateException;
  • import freemarker.template.TemplateModel;
  • import freemarker.template.TemplateModelException;
  • public class UpperDirective implements TemplateDirectiveModel
  • @Override
  • public void execute(Environment env, Map params, TemplateModel[] loopVars,
  • TemplateDirectiveBody body) throws TemplateException, IOException
  • //检查参数是否传入
  • if(!params.isEmpty())
  • throw new TemplateModelException( "this directive doesn't allow parameters.");
  • if(loopVars.length != 0)
  • throw new TemplateModelException( "this directive doesn't allow loop variable.");
  • //是否有非空的嵌套内容
  • if(body != null)
  • //执行嵌入体部分,和FTL中的<#nested>一样,除了我们使用自己的writer来替代当前的output writer
  • body.render( new UpperCaseFilterWriter(env.getOut()));
  • throw new RuntimeException( "missing body");
  • * {@link Writer}改变字符流到大写形式,
  • * 而且把它发送到另一个{@link Writer}中
  • private static class UpperCaseFilterWriter extends Writer
  • private final Writer out;
  • public UpperCaseFilterWriter(Writer out)
  • this.out = out;
  • public void write( char[] cbuf, int off, int len) throws IOException
  • char[] transformedCbuf = new char[len];
  • for( int i = 0; i < len; i++)
  • transformedCbuf[i] = Character.toUpperCase(cbuf[i + off]);
  • out.write(transformedCbuf);
  • public void flush() throws IOException
  • out.flush();
  • public void close() throws IOException
  • out.close();
  • 现在我们需要创建这个类的实例,然后让这个指令在模板中可以通过名称“upper”来访问(或者是其它我们想用的名字)。一个可行的方案是把这个指令放到数据模型中:

    root.put("upper", new com.example.UpperDirective());

    但更好的做法是将常用的指令作为共享变量放到Configuration中。 当然也可以使用内建函数new将指令放到一个FTL库(宏的集,就像在模板中,使用include或import)中。 <#-- 也许在FTL中你已经有了实现了的指令 --> <#macro something> ... </#macro> <#-- 现在你不能使用<#macro upper>,但是你可以使用: --> <#assign upper = "com.example.UpperDirective"?new()>

    第二个示例,我们来创建一个指令,这个指令可以一次又一次地执行其中的嵌套内容,这个次数由指定的数字来确定(就像list指令),可以使用<hr>将输出的重复内容分开。这个指令我们命名为“repeat”。示例模板如下:

    [html] view plain copy 在CODE上查看代码片 派生到我的代码片
  • import freemarker.core.Environment;
  • import freemarker.template.SimpleNumber;
  • import freemarker.template.TemplateBooleanModel;
  • import freemarker.template.TemplateDirectiveBody;
  • import freemarker.template.TemplateDirectiveModel;
  • import freemarker.template.TemplateException;
  • import freemarker.template.TemplateModel;
  • import freemarker.template.TemplateModelException;
  • import freemarker.template.TemplateNumberModel;
  • public class RepeatDirective implements TemplateDirectiveModel
  • private static final String PARAM_NAME_COUNT = "count";
  • private static final String PARAM_NAME_HR = "hr";
  • @Override
  • public void execute(Environment env, Map params, TemplateModel[] loopVars,
  • TemplateDirectiveBody body) throws TemplateException, IOException
  • //处理参数:
  • int countParam = 0;
  • boolean countParamSet = false;
  • boolean hrParam = false;
  • Iterator paramIter = params.entrySet().iterator();
  • while(paramIter.hasNext())
  • Map.Entry ent = (Map.Entry)paramIter.next();
  • String paramName = (String)ent.getKey();
  • TemplateModel paramValue = (TemplateModel)ent.getValue();
  • if(paramName.equals(PARAM_NAME_COUNT))
  • if(!(paramValue instanceof TemplateNumberModel))
  • throw new TemplateModelException( "The \"" + PARAM_NAME_HR
  • + "\" parameter " + "must be a number.");
  • countParam = ((TemplateNumberModel)paramValue).getAsNumber().intValue();
  • countParamSet = true;
  • if (countParam < 0)
  • throw new TemplateModelException( "The \"" +
  • PARAM_NAME_HR + "\" parameter " + "can't be negative.");
  • else if(paramName.equals(PARAM_NAME_HR))
  • if (!(paramValue instanceof TemplateBooleanModel))
  • throw new TemplateModelException( "The \"" +
  • PARAM_NAME_HR + "\" parameter " + "must be a boolean.");
  • hrParam = ((TemplateBooleanModel)paramValue).getAsBoolean();
  • throw new TemplateModelException( "Unsupported parameter: " + paramName);
  • if(!countParamSet)
  • throw new TemplateModelException( "The required \""
  • + PARAM_NAME_COUNT + "\" paramter" + "is missing.");
  • if(loopVars.length > 1)
  • throw new TemplateModelException( "At most one loop variable is allowed.");
  • //执行真正指令的执行部分:
  • Writer out = env.getOut();
  • if(body != null)
  • for( int i = 0; i < countParam; i++)
  • //如果“hr”参数为真,那么就在所有重复部分之间打印<hr>:
  • if (hrParam && i != 0)
  • out.write( "<hr>");
  • //如果有循环变量,那么就设置它:
  • if(loopVars.length > 0)
  • loopVars[ 0] = new SimpleNumber(i + 1);
  • //执行嵌入体部分(和FTL中的<nested>一样)。
  • //这种情况下,我们不提供一个特殊的writer作为参数:
  • body.render(env.getOut());
  • 在test1.java中增加:root.put("repeat", new RepeatDirective());

    节点变量:

    节点变量体现了树形结构中的节点。节点变量的引入是为了帮助用户在数据模型中处理XML文档,但是它们也可以用于构建树状模型,节点变量有下列属性,它们都由TemplateNodeModel接口的方法提供。

     基本属性:      TemplateSequenceModel getChildNodes():一个节点的子节点序列(除非这个节点是叶子节点,这时方法返回一个空序列或者是null)。子节点本身应该也是节点变量。      TemplateNodeModel getParentNode():一个节点只有一个父节点(除非这个节点是节点树的根节点,这时方法返回null)。  可选属性。如果一个属性在具体的使用中没有意义,那对应的方法应该返回null:      String getNodeName():节点名称也是宏的名称,当使用recurse和visit指令时,它用来控制节点。因此,如果想通过节点使用这些指令,那么节点的名称是必须的。      String getNodeType():在XML技术中:"element","text","comment"等类型。如果这些信息可用,就是通过recurse和visit指令来查找节点的默认处理宏。而且,它对其他有具体用途的应用程序也是有用的。      String getNamespaceURI():这个节点所属的命名空间(和用于库的FTL命名空间无关)。例如,在XML中,这就是元素和属性所属XML命名空间的URI。这个信息如果可用,就是通过recurse和visit指令来查找存储控制宏的FTL命名空间。     在FTL这里,节点属性的直接使用可以通过内建函数node完成,还有visit和recurse宏。

    对象包装:

    当往容器中添加一些对象时,正如在FreeMarker API文档中看到的那样,它可以收到任意java对象类型的参数,而不一定是TemplateModel。这是因为模板实现时会默默地用合适的TemplateModel对象来替换原有对象。比如向容器中加入一个String,也许它将被替换为一个SimpleScalar实例来存储相同的文本。 至于替换什么时候发生,这就是容器业务处理的问题(类的业务实现了容器接口)所在,但是它在获取子变量时必须会发生,因为getter方法(依据接口而定)会返回TemplateModel,而不是Object。SimpleHash,SimpleSequence和SimpleCollection使用最懒的策略,当第一次获取子变量时,它们用一个适合的TemplateModel来替换一个非TemplateModel子变量。 至于什么类型的Java对象可以被替换,又使用什么样的TemplateModel来实现,它可以被实现的容器自身来控制,也可以委派给ObjectWrapper的一个实例。ObjectWrapper是一个接口,其中只定义了一个方法:TemplateModel wrap(java.lang.Object obj)。可以传递一个Object类型的参数,它会返回对应的TemplateModel对象,如果不行则抛出TemplateModelException异常。替换原则是在ObjectWrapper的实现类中编码实现的。

    最重要的ObjectWrapper实现类是FreeMarker核心包提供的:

     ObjectWrapper.DEFAULT_WRAPPER:它使用SimpleScalar来替换String,SimpleNumber来替换Number,SimpleSequence来替换List和数组,SimpleHash来替换Map,TemplateBooleanModel.TRUE或TemplateBooleanModel.FALSE来替换Boolean,freemarker.ext.dom.NodeModel来替换W3C组织定义的DOM模型节点类型。对于Jython类型的对象,包装器会调用freemarker.ext.jython.JythonWrapper。而对于其他对象,则会调用BEAN_WRAPPER。      ObjectWrapper.BEANS_WRAPPER:它可以通过Java 的反射机制来获取到Java Bean的属性和其他任意对象类型的成员变量。在最新的FreeMarker 2.3版本中,它是freemarker.ext.beans.BeansWrapper的实例。

    做一个具体的例子,让我们来看看SimpleXxx类型都是怎么工作的。SimpleHash,SimpleSequence和SimpleCollection使用DEFAULT_WRAPPER来包装子变量(除非在构造方法中传递另外一个包装器)。这个例子在实战中来展示DEFAULT_WRAPPER。

    Map map = new HashMap(); map.put("anotherString", "blah"); map.put("anotherNumber", new Double(3.14)); List list = new ArrayList(); list.add("red"); list.add("green"); list.add("blue"); SimpleHash root = new SimpleHash(); // 将会使用默认的包装器 root.put("theString", "wombat"); root.put("theNumber", new Integer(8)); root.put("theMap", map); root.put("theList", list);

    假设root是数据模型的root,那么得到的数据模型将是:

    注意在theMap和theList中的Object也可以作为子变量来访问。这是因为,当要访问theMap.anotherString时,SimpleHash(这里作为根哈希表)会静默地使用SimpleHash实例来替换Map(theMap),这个实例使用了和根哈希表相同的包装器。所以当访问其中的子变量anotherString时,就会使用SimpleScalar来替换它。 如果在数据模型中放了任意的对象,那么DEFAULT_WRAPPER就会调用BEANS_WRAPPER来包装这个对象:

    SimpleHash root = new SimpleHash(); // 可以拿到Java对象"simple": root.put("theString", "wombat"); // 可以拿到Java对象": root.put("theObject", new TestObject("green mouse", 1200));

    假设TestObject是这样的:

    [java] view plain copy 在CODE上查看代码片 派生到我的代码片

    ${theObject.name} ${theObject.price} ${theObject.sin(123)}

    输出将如下:

    green mouse 1200 -0,45990349068959124

    之前我们已经看到了,我们使用java.util.HashMap作为根哈希表,而不是SimpleHash或其他特定的FreeMarker类。因为Template.process(...)自动包装了给定的数据模型参数的对象,所以它才会起作用。它使用受Configuration级设置的对象包装器,object_wrapper(除非明确指定一个ObjectWrapper作为它的参数)。因此,编写简单的FreeMarker应用程序就不需要知道TemplateModel了。注意根的类型不需要一定是java.util.Map。它也可以是实现了TemplateHashModel接口的被包装的对象。 object_wrapper设置的默认值是ObjectWrapper.DEFAULT_WRAPPER。如果想改变它,比如换成ObjectWrapper.BEANS_WRAPPER,那么可以这样来配置FreeMarker引擎(在其它线程开始使用它之前): cfg.setObjectWrapper(ObjectWrapper.BEANS_WRAPPER);

    要注意我们可以在这里设置任何对象实现接口ObjectWrapper,当然也可以用来设置你自己定义的实现类。

    对于包装了基本Java容器类型(比如java.util.Map和java.util.List)的TemplateModel实现类,常规是它们使用像它们父容器那样的相同对象包装器来包装它们的子变量。从技术上讲,它们是被父容器(它对所创建的子类有全部的控制器)实例化的,因为父容器创建了它们,所以它们使用和父容器一样的对象包装器。如果BEANS_WRAPPER用来包装根哈希表,那么它也会被用来包装子变量(子变量的子变量也是如此,以此类推)。这个之前看到的theMap.anotherString是同样的现象。