学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net
1.基本内容
在入门章节中, 我们已经知道如何使用基本的Java类(Map
, String
等)来构建数据模型了。在内部,模板中可用的变量都是实现了 freemarker.template.TemplateModel
接口的Java对象。 但在数据模型中,可以使用基本的Java集合类作为变量,因为这些变量会在内部被替换为适当的 TemplateModel
类型。这种功能特性被称作是 对象包装。对象包装功能可以透明地把 任何 类型的对象转换为实现了 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
实现, 一个简单的方式是创建它的实例,然后将这个实例放入数据模型中 (也就是把它 放到
哈希表的根root上)。 对象包装器将会给模板提供它的原状,因为它已经实现了 TemplateModel
接口,所以没有转换(包装)的需要。 (这个技巧当你不想用对象包装器来包装(转换)某些对象时仍然有用。)
2.标量
有4种类型的标量:
- 布尔值
- 数字
- 字符串
- 日期类型(子类型: 日期(没有时间部分),时间或者日期-时间)
每一种标量类型都是 TemplateTypeModel
接口的实现,这里的 Type
就是类型的名称。这些接口只定义了一个方法: type getAsType();
。 它返回变量的Java类型(boolean
, Number
, String
和 Date
各自代表的值)。
TemplateScalarModel
,而不是 TemplateStringModel
。 (因为早期的 FreeMarker 字符串就是标量。) 这些接口的一个细小的实现和 SimpleType
类名在 freemarker.template
包中是可用的。 但是却没有 SimpleBooleanModel
类型;为了代表布尔值, 可以使用 TemplateBooleanModel.TRUE
和 TemplateBooleanModel.FALSE
来单独使用。
SimpleScalar
,而不是 SimpleString
。 在FTL中标量是一成不变的。当在模板中设置变量的值时, 使用其他的实例来替换 TemplateTypeModel
实例时, 是不用改变原来实例中存储的值的。
"日期" 类型的难点
对于日期类型来说,有一些难题,因为Java API通常不区别 java.util.Date
只存储日期部分(April 4, 2003), 时间部分(10:19:18 PM),或两者都存(April 4, 2003 10:19:18 PM)。 为了用本文正确显示值(或者进行其它确定的操作),FreeMarker必须知道 java.util.Date
的哪个部分存储了有意义上的信息, 哪部分没有被使用(通常是标记为0的)。不幸的是, 通常该信息只是当值从数据库中取得时可用, 因为大多数数据库有独立的日期,时间和日期-时间(又叫做时间戳)类型, 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
的实现类,就是所谓的 对象包装器ObjectWrapper
(请参考之前的对象包装介绍)。 如果对象包装器要包装 java.util.Date
类, 它不是 java.sql
日期类的实例,那就不能决定子类型是什么, 所以使用 UNKNOWN
。之后,如果模板需要使用这个变量, 而且操作也需要子类型,那就会停止执行并抛出错误。为了避免这种情况的发生, 对于那些可能有问题的变量,模板开发人员必须明确地指定子类型,使用内建函数 date
, time
或 datetime
(比如 lastUpdated?datetime
)。请注意, 如果和格式化参数一起使用内建函数 string
, 比如foo?string("MM/dd/yyyy"),那么 FreeMarker 就不必知道子类型了。
3.容器
容器包括哈希表,序列和集合三种类型。
哈希表
哈希表是实现了 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
相似,但是它返回 TemplateModels
而不是 Object
, 而且它能抛出 TemplateModelException
异常。
通常使用的实现类是 SimpleCollection
。
4.方法
方法变量在存于实现了 TemplateMethodModel
接口的模板中。这个接口包含一个方法: TemplateModel exec(java.util.List arguments)
。 当使用 方法调用表达式 调用方法时,exec
方法将会被调用。 形参将会包含FTL方法调用形参的值。exec
方法的返回值给出了FTL方法调用表达式的返回值。
TemplateMethodModelEx
接口扩展了 TemplateMethodModel
接口。它没有添加任何新方法。 事实上这个对象实现这个 标记 接口是给FTL引擎暗示, 形式参数应该直接以 TemplateModel
的形式放进 java.util.List
。否则将会以 String
形式放入list。
出于这些很明显的原因,这些接口没有默认的实现。
例如:下面这个方法,返回第一个字符串在第二个字符串第一次出现时的索引位置, 如果第二个字符串中不包含第一个字符串,则返回-1:
public class IndexOfMethod implements TemplateMethodModel { public TemplateModel exec(List args) throws TemplateModelException { if (args.size() != 2) { throw new TemplateModelException("Wrong arguments"); } return new SimpleNumber( ((String) args.get(1)).indexOf((String) args.get(0))); } }
如果将一个实例放入根数据模型中,像这样:
root.put("indexOf", new IndexOfMethod());
那么就可以在模板中调用:
<#assign x = "something"> ${indexOf("met", x)} ${indexOf("foo", x)}
将会输出:
2 -1
如果需要访问FTL运行时环境(读/写变量,获取本地化信息等),则可以使用 Environment.getCurrentEnvironment()
来获取。
5.指令
Java程序员可以使用 TemplateDirectiveModel
接口在Java代码中实现自定义指令。详情可以参加API文档。
TemplateDirectiveModel
在 FreeMarker 2.3.11 版本时才加入, 来代替快被废弃的 TemplateTransformModel
。示例 1
我们要实现一个指令, 这个指令可以将在它开始标签和结束标签之内的字符都转换为大写形式。 就像这个模板:
foo <@upper> bar <#-- All kind of FTL is allowed here --> <#list ["red", "green", "blue"] as color> ${color} </#list> baaz </@upper> wombat
将会输出:
foo BAR RED GREEN BLUE BAAZ wombat
下面是指令类的源代码:
package com.example; import java.io.IOException; import java.io.Writer; import java.util.Map; import freemarker.core.Environment; import freemarker.template.TemplateDirectiveBody; import freemarker.template.TemplateDirectiveModel; import freemarker.template.TemplateException; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; /** * FreeMarker user-defined directive that progressively transforms * the output of its nested content to upper-case. * * * <p><b>Directive info</b></p> * * <p>Directive parameters: None * <p>Loop variables: None * <p>Directive nested content: Yes */ public class UpperDirective implements TemplateDirectiveModel { public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { // Check if no parameters were given: 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 variables."); } // If there is non-empty nested content: if (body != null) { // Executes the nested body. Same as <#nested> in FTL, except // that we use our own writer instead of the current output writer. body.render(new UpperCaseFilterWriter(env.getOut())); } else { throw new RuntimeException("missing body"); } } /** * A {@link Writer} that transforms the character stream to upper case * and forwards it to another {@link Writer}. */ private static class UpperCaseFilterWriter extends Writer { private final Writer out; 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
)中:
<#-- Maybe you have directives that you have implemented in FTL --> <#macro something> ... </#macro> <#-- Now you can't use <#macro upper>, but instead you can: --> <#assign upper = "com.example.UpperDirective"?new()>
示例 2
我们来创建一个指令,这个指令可以一次又一次地执行其中的嵌套内容, 这个次数由指定的数字来确定(就像 list
指令), 可以使用<hr>
将输出的重复内容分开。 这个指令我们命名为"repeat"。示例模板如下:
<#assign x = 1> <@repeat count=4> Test ${x} <#assign x++> </@repeat> <@repeat count=3 hr=true> Test </@repeat> <@repeat count=3; cnt> ${cnt}. Test </@repeat>
输出为:
Test 1 Test 2 Test 3 Test 4 Test <hr> Test <hr> Test 1. Test 2. Test 3. Test
指令的实现类为:
package com.example; import java.io.IOException; import java.io.Writer; import java.util.Iterator; import java.util.Map; 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; /** * FreeMarker user-defined directive for repeating a section of a template, * optionally with separating the output of the repetations with * <tt><hr></tt>-s. * * * <p><b>Directive info</b></p> * * <p>Parameters: * <ul> * <li><code>count</code>: The number of repetations. Required! * Must be a non-negative number. If it is not a whole number then it will * be rounded <em>down</em>. * <li><code>hr</code>: Tells if a HTML "hr" element could be printed between * repetations. Boolean. Optional, defaults to <code>false</code>. * </ul> * * <p>Loop variables: One, optional. It gives the number of the current * repetation, starting from 1. * * <p>Nested content: Yes */ public class RepeatDirective implements TemplateDirectiveModel { private static final String PARAM_NAME_COUNT = "count"; private static final String PARAM_NAME_HR = "hr"; public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { // --------------------------------------------------------------------- // Processing the parameters: 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(); } else { 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."); } // Yeah, it was long and boring... // --------------------------------------------------------------------- // Do the actual directive execution: Writer out = env.getOut(); if (body != null) { for (int i = 0; i < countParam; i++) { // Prints a <hr> between all repetations if the "hr" parameter // was true: if (hrParam && i != 0) { out.write("<hr>"); } // Set the loop variable, if there is one: if (loopVars.length > 0) { loopVars[0] = new SimpleNumber(i + 1); } // Executes the nested body (same as <#nested> in FTL). In this // case we don't provide a special writer as the parameter: body.render(env.getOut()); } } } }
注意
TemplateDirectiveModel
对象通常不应该是有状态的,这一点非常重要。 一个经常犯的错误是存储指令的状态然后在对象的属性中调用执行。 想一下相同指令的嵌入调用,或者指令对象被用作共享变量, 并通过多线程同时访问。
不幸的是, TemplateDirectiveModel
不支持传递参数的位置(而不是参数名称)。从 FreeMarker 2.4 版本开始,它将被修正。
6.结点变量
结点变量体现了树形结构中的结点。结点变量的引入是为了帮助用户 在数据模型中处理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
宏。
在很多用例中,实现了 TemplateNodeModel
接口和其它接口的变量,因为结点变量属性仅仅提供基本的结点间导航的方法。
7.对象包装
对象包装器是实现了 freemarker.template.ObjectWrapper
接口的类。它的目标是实现Java对象(应用程序中特定类等,比如 String
, Map
,List
实例)和FTL类型系统之间的映射。换句话说, 它指定了模板如何在数据模型(包含从模板中调用的Java方法的返回值)中发现Java对象。 对象包装器作为插件放入 Configuration
中,可以使用 object_wrapper
属性设置 (或者使用Configuration.setObjectWrapper
)。
从技术角度来说,FTL类型系统由之前介绍过的 TemplateModel
子接口 (TemplateScalarModel
,TemplateHashMode
, TemplateSequenceModel
等)来表示。要映射Java对象到FTL类型系统中, 对象包装器的 TemplateModel wrap(java.lang.Object obj)
方法会被调用。
有时FreeMarker需要撤回映射,此时 对象包装器ObjectWrapper
的 Object unwrap(TemplateModel)
方法就被调用了 (或其他的变化,请参考API文档来获取详细内容)。最后的操作是在 ObjectWrapperAndUnwrapper
中,它是 ObjectWrapper
的子接口。很多实际的包装器会实现 ObjectWrapperAndUnwrapper
接口。
我们来看一下包装Java对象并包含其他对象 (比如 Map
,List
,数组, 或者有JavaBean属性的对象)是如何进行的。可以这么说,对象包装器将 Object[]
数组包装成 TemplateSquenceModel
接口的一些实现。当FreeMarker需要FTL序列中项的时候,它会调用 TemplateSquenceModel.get(int index)
方法。该方法的返回值是 TemplateModel
,也就是说,TemplateSquenceModel
实现不仅仅可以从给定的数组序列获取 对象
, 也可以负责在返回它之前包装该值。为了解决这个问题,典型的 TemplateSquenceModel
实现将会存储它创建的 ObjectWrapper
,之后再调用该 ObjectWrapper
来包装包含的值。相同的逻辑代表了 TemplateHashModel
或其他的 TemplateModel
,它是其它 TemplateModel
的容器。 因此,通常不论值的层次结构有多深,所有值都会被同一个 ObjectWrapper
包装。(要创建 TemplateModel
的实现类,请遵循这个原则,可以使用 freemarker.template.WrappingTemplateModel
作为基类。)
数据模型本身(root变量)是 TemplateHashModel
。 在 Template.process
中指定的root对象将会被在 object_wrapper
配置中设置的对象包装器所包装,并产生一个 TemplateHashModel
。从此,被包含值的包装遵循之前描述的逻辑 (比如,容器负责包装它的子实例)。
行为良好的对象包装器都会绕过已经实现 TemplateModel
接口的对象。如果将已经实现 TemplateModel
的对象放到数据模型中 (或者从模板中调用的Java方法返回这个对象),那么就可以避免实际的对象包装。 当特别是通过模板访问创建的值时,通常会这么做。因此,要避免更多上面对象包装的性能问题, 但也可以精确控制模板可以看到的内容(不是基于当前对象包装器的映射策略)。 常见的应用程序使用该手法是使用 freemarker.template.SimpleHash
作为数据模型的根root(而不是Map
),当使用 SimpleHash
的 put
方法来填充(这点很重要,它不会复制已经填充并存在的 Map
)。这会加快顶层数据模型变量的访问速度。
默认对象包装器
object_wrapper
Configuration
的默认设置是 freemarker.template.DefaultObjectWrapper
实例。除非有特别的需求,那么建议使用这个对象包装器,或者是自定义的 DefaultObjectWrapper
的子类。
它会识别大部分基本的Java类型,比如 String
, Number
,Boolean
, Date
,List
(通常还有全部的 java.util.Collection
类型), 数组,Map
等。并把它们自然地包装成匹配 TemplateModel
接口的对象。它也会使用 freemarker.ext.dom.NodeModel
来包装W3C DOM结点, 所以可以很方便地处理XML, 在XML章节会有描述)。 对于Jython对象,会代理到 freemarker.ext.jython.JythonWrapper
上。 而对于其它所有对象,则会调用 BeansWrapper.wrap
(超类的方法), 暴露出对象的JavaBean属性作为哈希表项 (比如FTL中的 myObj.foo
会在后面调用 getFoo()
), 也会暴露出对象(比如FTL中的 myObj.bar(1, 2)
就会调用方法) 的公有方法(JavaBean action)。(关于对象包装器的更多信息,请参阅 该章节。)
关于 DefaultObjectWrapper
更多值得注意的细节:
不用经常使用它的构造方法,而是使用
DefaultObjectWrapperBuilder
来创建它。 这就允许 FreeMarker 使用单例。-
DefaultObjectWrapper
有incompatibleImprovements
属性, 这在 2.3.22 或更高版本中是极力推荐的。如何来设置:如果已经在 2.3.22 或更高版本的
Configuration
中设置了incompatible_improvements
选项, 而没有设置object_wrapper
选项(那么它就保留默认值), 我们就什么都做不了了,因为它已经使用了同等incompatibleImprovements
属性值的DefaultObjectWrapper
单例。-
另外也可以在
Configuration
中独立设置incompatibleImprovements
。基于如何创建/设置ObjectWrapper
,可以通过这样完成 (假设想要incompatibleImprovements
2.3.22):-
如果使用了构建器API:
... = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_22).build()
-
或者使用构造方法:
... = new DefaultObjectWrapper(Configuration.VERSION_2_3_22)
-
或者使用
object_wrapper
属性 (*.properties
文件或java.util.Properties
对象):object_wrapper=DefaultObjectWrapper(2.3.21)
-
或者通过
FreemarkerServlet
配置object_wrapper
和在web.xml
中的init-param
属性来配置:<init-param> <param-name>object_wrapper</param-name> <param-value>DefaultObjectWrapper(2.3.21)</param-value> </init-param>
-
在新的或测试覆盖良好的项目中,也建议设置
forceLegacyNonListCollections
属性为false
。 如果使用.properties
或FreemarkerServlet
初始化参数,就会像DefaultObjectWrapper(2.3.22, forceLegacyNonListCollections=false)
, 同时,使用Java API可以在DefaultObjectWrapperBuilder
对象调用build()
之前调用setForceLegacyNonListCollections(false)
。自定义
DefaultObjectWrapper
的最常用方法是覆盖handleUnknownType
方法。
自定义对象包装示例
我们假定有一个应用程序特定的类,像下面这样:
package com.example.myapp; public class Tupple<E1, E2> { public Tupple(E1 e1, E2 e2) { ... } public E1 getE1() { ... } public E2 getE2() { ... } }
若想让模板将它视作长度为2的序列,那么就可以这么来调用 someTupple[1]
, <#list someTupple ...>
, 或者 someTupple?size
。需要创建一个 TemplateSequenceModel
实现来适配 Tupple
到 TempateSequenceMoldel
接口:
package com.example.myapp.freemarker; ... public class TuppleAdapter extends WrappingTemplateModel implements TemplateSequenceModel, AdapterTemplateModel { private final Tupple<?, ?> tupple; public TuppleAdapter(Tupple<?, ?> tupple, ObjectWrapper ow) { super(ow); // coming from WrappingTemplateModel this.tupple = tupple; } @Override // coming from TemplateSequenceModel public int size() throws TemplateModelException { return 2; } @Override // coming from TemplateSequenceModel public TemplateModel get(int index) throws TemplateModelException { switch (index) { case 0: return wrap(tupple.getE1()); case 1: return wrap(tupple.getE2()); default: return null; } } @Override // coming from AdapterTemplateModel public Object getAdaptedObject(Class hint) { return tupple; } }
关于类和接口:
TemplateSequenceModel
: 这就是为什么模板将它视为序列WrappingTemplateModel
: 只是一个方便使用的类,用于TemplateModel
对象进行自身包装。通常仅对包含其它对象的对象需要。 参考上面的wrap(...)
调用。AdapterTemplateModel
: 表明模板模型适配一个已经存在的对象到TemplateModel
接口, 那么去掉包装就会给出原有对象。
最后,我们告诉 FreeMarker 用 TuppleAdapter
(或者,可以在将它们传递到FreeMarker之前手动包装它们) 包装 Tupple
。那样的话,首先创建一个自定义的对象包装器:
package com.example.myapp.freemarker; ... public class MyAppObjectWrapper extends DefaultObjectWrapper { public MyAppObjectWrapper(Version incompatibleImprovements) { super(incompatibleImprovements); } @Override protected TemplateModel handleUnknownType(final Object obj) throws TemplateModelException { if (obj instanceof Tupple) { return new TuppleAdapter((Tupple<?, ?>) obj, this); } return super.handleUnknownType(obj); } }
那么当配置 FreeMarker (关于配置,参考这里...) 将我们的对象包装器插在:
// Where you initialize the cfg *singleton* (happens just once in the application life-cycle): cfg = new Configuration(Configuration.VERSION_2_3_22); ... cfg.setObjectWrapper(new MyAppObjectWrapper(cfg.getIncompatibleImprovements()));
或者使用 java.util.Properties
来代替配置 FreeMarker (也就是 .properties
文件):
object_wrapper=com.example.myapp.freemarker.MyAppObjectWrapper(2.3.22)
译自 Email: ddekany at users.sourceforge.net