学习笔记,选自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类型(booleanNumberStringDate 各自代表的值)。

注意:由于历史遗留的原因,字符串标量的接口是 TemplateScalarModel,而不是 TemplateStringModel。 (因为早期的 FreeMarker 字符串就是标量。)

  这些接口的一个细小的实现和 SimpleType 类名在 freemarker.template 包中是可用的。 但是却没有 SimpleBooleanModel 类型;为了代表布尔值, 可以使用 TemplateBooleanModel.TRUETemplateBooleanModel.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 接口中的常量之一:DATETIMEDATETIMEUNKNOWN

  关于 UNKNOWN: java.langjava.util 下的类通常被自动转换成 TemplateModel 的实现类,就是所谓的 对象包装器ObjectWrapper(请参考之前的对象包装介绍)。 如果对象包装器要包装 java.util.Date 类, 它不是 java.sql 日期类的实例,那就不能决定子类型是什么, 所以使用 UNKNOWN。之后,如果模板需要使用这个变量, 而且操作也需要子类型,那就会停止执行并抛出错误。为了避免这种情况的发生, 对于那些可能有问题的变量,模板开发人员必须明确地指定子类型,使用内建函数 datetimedatetime (比如 lastUpdated?datetime)。请注意, 如果和格式化参数一起使用内建函数 string, 比如foo?string("MM/dd/yyyy"),那么 FreeMarker 就不必知道子类型了。

3.容器

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

哈希表

  哈希表是实现了 TemplateHashModel 接口的Java对象。TemplateHashModel 有两个方法: TemplateModel get(String key),这个方法根据给定的名称返回子变量, boolean isEmpty(),这个方法表明哈希表是否含有子变量。 get 方法当在给定的名称没有找到子变量时返回null。

  TemplateHashModelEx 接口扩展了 TemplateHashModel。它增加了更多的方法,使得可以使用内建函数 valueskeys 来枚举哈希表中的子变量。

  经常使用的实现类是 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库(宏的集合,就像在模板中, 使用 includeimport )中:

 <#-- 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>&lt;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(): 结点名称也是宏的名称,当使用 recursevisit指令时, 它用来控制结点。因此,如果想通过结点使用这些指令, 那么结点的名称是 必须的

    • String getNodeType():在XML中: "element""text""comment"等。如果这些信息可用, 就是通过 recursevisit 指令来查找结点的默认处理宏。而且,它对其他有具体用途的应用程序也是有用的。

    • String getNamespaceURI(): 这个结点所属的命名空间(和用于库的FTL命名空间无关)。例如,在XML中, 这就是元素和属性所属XML命名空间的URI。这个信息如果可用,就是通过 recursevisit 指令来查找存储控制宏的FTL命名空间。

  在FTL里,结点属性的直接使用可以通过 内建函数 node完成, 还有 visitrecurse 宏。

  在很多用例中,实现了 TemplateNodeModel 接口和其它接口的变量,因为结点变量属性仅仅提供基本的结点间导航的方法。

7.对象包装

  对象包装器是实现了 freemarker.template.ObjectWrapper 接口的类。它的目标是实现Java对象(应用程序中特定类等,比如 StringMapList 实例)和FTL类型系统之间的映射。换句话说, 它指定了模板如何在数据模型(包含从模板中调用的Java方法的返回值)中发现Java对象。 对象包装器作为插件放入 Configuration 中,可以使用 object_wrapper 属性设置 (或者使用Configuration.setObjectWrapper)。

  从技术角度来说,FTL类型系统由之前介绍过的 TemplateModel 子接口 (TemplateScalarModelTemplateHashModeTemplateSequenceModel等)来表示。要映射Java对象到FTL类型系统中, 对象包装器的 TemplateModel wrap(java.lang.Object obj) 方法会被调用。

  有时FreeMarker需要撤回映射,此时 对象包装器ObjectWrapperObject unwrap(TemplateModel) 方法就被调用了 (或其他的变化,请参考API文档来获取详细内容)。最后的操作是在 ObjectWrapperAndUnwrapper 中,它是 ObjectWrapper 的子接口。很多实际的包装器会实现 ObjectWrapperAndUnwrapper 接口。

  我们来看一下包装Java对象并包含其他对象 (比如 MapList,数组, 或者有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),当使用 SimpleHashput 方法来填充(这点很重要,它不会复制已经填充并存在的 Map)。这会加快顶层数据模型变量的访问速度。

默认对象包装器

  object_wrapper Configuration 的默认设置是 freemarker.template.DefaultObjectWrapper 实例。除非有特别的需求,那么建议使用这个对象包装器,或者是自定义的 DefaultObjectWrapper 的子类。

  它会识别大部分基本的Java类型,比如 StringNumberBooleanDateList (通常还有全部的 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 使用单例。

  • DefaultObjectWrapperincompatibleImprovements 属性, 这在 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。 如果使用 .propertiesFreemarkerServlet 初始化参数,就会像 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 实现来适配 TuppleTempateSequenceMoldel 接口:

 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

freeMarker(七)——程序开发指南之数据模型的更多相关文章

  1. python 整型--《Python 3程序开发指南》笔记

    参考:<Python 3程序开发指南> 整数转换函数: bin(i) 返回整数i的二进制表示(字符串) hex(i) 返回i的十六进制表示(字符串) int(x) 将x转换为整数,失败产生 ...

  2. freeMarker(八)——程序开发指南之配置(Configuration)

    学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net 1.基本内容 配置(configuration)就是 freemark ...

  3. freeMarker(六)——程序开发指南入门

    学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net 1.创建Configuration实例 首先,你应该创建一个 free ...

  4. FreeMarker:模板开发指南

    ylbtech-FreeMarker:模板开发指南 1.返回顶部 1. Section Contents 入门 模板 + 数据模型 = 输出 数据模型一览 模板一览 数值,类型 基本内容 类型 模板 ...

  5. freeMarker(九)——程序开发指南补充知识

    学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net 1.变量.范围 本章介绍当模板在访问变量时发生了什么事情,还有变量是如 ...

  6. QtQuick桌面应用程序开发指南 4)动态管理Note对象_B 5)加强外观 6)许多其他的改进

    4.2.2 Stateless(不管状态)JavaScript库 为了让开发轻松点, 使用一个JavaScript接口来和数据库交互是个好主意, 它在QML中提供了方便的方法; 在QtCreator中 ...

  7. 微信小程序 - 开发指南

    一.下载并安装开发工具 下载地址 二.创建项目 打开开发工具 添加项目 进入预览和调试界面 代码编辑器 编译并预览 三.启动流程 四.适用场景 五.技术框架 六.科普 [图片较大 - 点击查看]

  8. python3程序开发指南——第1章 笔记

    python文件的扩展名为.py ,但是python GUI程序的扩展名为.pyw 在python中,注释以#开始,作用范围为该行 IDLE提供了三个关键功能:输入python表达式与代码,并在pyt ...

  9. 微信小程序开发指南

    https://developers.weixin.qq.com/miniprogram/introduction/#%E4%BA%A7%E5%93%81%E5%AE%9A%E4%BD%8D%E5%8 ...

随机推荐

  1. Linux删除乱码文件的方法

    当文件名为乱码的时候,无法通过键盘输入文件名,所以在终端下就不能直接利用rm,mv等命令管理文件了. 我们可以通过以下几种方法删除linux下的乱码文件.(文件名为乱码) l  方法1 我们知道每个文 ...

  2. Linux nginx部署laravel

    Composer Composer 是 php 的一个依赖管理工具.它允许你申明项目所依赖的代码库,它会在你的项目中为你安装他们.运行 Composer 需要 PHP 5.3.2+ 以上版本.一些敏感 ...

  3. HDU 2588 GCD &amp;&amp; GCD问题总结

    GCD(一) 题目: The greatest common divisor GCD(a,b) of two positive integers a and b,sometimes written ( ...

  4. Python 基本数据类型和序列类型

    python 3.6.4 中,有9种数据类型: int, float, bool, complex, list, tuple, string, set, dict (1).int 整型,不可变 (2) ...

  5. Windows下安装appium桌面版和命令行版

    安装appium桌面版和命令行版   一 桌面版(打开很慢,常用于辅助元素定位) 1.官网下载window版本:  github search appium desktop download late ...

  6. Jmeter + Ant + Jenkins 接口/性能测试,持续集成环境搭建

    1. 安装Jmeter.jdk Jmeter 3.3版本 :<http://note.youdao.com/noteshare?id=5e3fd287db24d08386207a7de22d26 ...

  7. HealthKit详解

    1. 导入HealthKit框架 #import <HealthKit/HealthKit.h> 2. 判断设备是否支持HealthKit HealthKit是iOS8加入的API Hea ...

  8. The Princess and the Pea,摘自iOS应用Snow White and more stories

    Once upon a time there was a prince who wanted to marry a real princess.从前,有个王子想和真正的公主结婚. He looked ...

  9. SSH 占用数据库连接不释放问题

    SSH框架的项目在訪问数据库的时候.訪问完毕后一直占用链接,不释放.导致过了一段时间后,server没挂,就是有訪问数据库的连接是时候.一直卡住 解决的方法:   1.配置spring相应的hiber ...

  10. spring 3.2 后 annotation-driven 注册新的类

    DefaultAnnotationHandlerMapping 和 AnnotationMethodHandlerAdapter 的使用已经过时! DefaultAnnotationHandlerMa ...