我是靠谱客的博主 鳗鱼画笔,最近开发中收集的这篇文章主要介绍20200426-27 关于Freemarker中的入门,数据模型,配置,其他 知识梳理1.入门2.数据模型3.配置,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

官方链接:http://freemarker.foofun.cn/dgui_template.html

目录

1.入门

1.1创建 Configuration 实例

1.2 创建数据模型

1.3 获取模板

1.4 合并模板和数据模型

1.5 将代码放在一起

2.数据模型

2.1 基本内容

2.2 标量

"日期" 类型的难点

2.3 容器(重要)

2.3.1 哈希表

2.3.2 序列

2.3.3 集合

2.4 方法

2.5 指令

示例 1

示例 2

注意

2.6 结点变量

2.7 对象包装

2.7.1 默认对象包装器

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

2.7.2 自定义对象包装示例

3.配置

3.1 基本内容

3.2 共享变量

3.3 配置设置

3.4 模板加载

3.4.1 模板加载器-内建模板加载器

3.4.2 模板加载器-从多个位置加载模板

3.4.3 模板加载器-从其他资源加载模板

3.4.4 模板加载器-模板名称(模板路径)

3.4.5 模板缓存

3.5 错误控制

3.5.1 可能的异常

3.5.2 根据TemplateException来自定义处理方式

3.5.3 在模板中明确地处理错误

3.6 "不兼容改进"设置

3.6.1 它要做什么

3.6.2 如何设置

4.其他

4.1 变量,范围

4.2 字符集问题

4.3 多线程

4.4 Bean的包装

4.4.1 模板哈希表模型功能(TemplateHashModel functionality)

4.4.2 说一点安全性

4.4.3 模板标量模型功能(TemplateScalarModel functionality)

4.4.4 模板数字模型功能(TemplateNumberModel functionality)

4.4.5 模板集合模型功能(TemplateCollectionModel functionality)

4.4.6 模板序列模型功能(TemplateSequenceModel functionality)

4.4.7 模板方法模型功能(TemplateMethodModel functionality)

4.4.8 解包规则

4.4.9 访问静态方法

4.4.10 访问枚举类型

4.5 日志

4.5.1 日志库选择

4.5.2 日志分类

4.6 在Servlet中使用FreeMarker

4.6.1 在"Model 2"中使用FreeMarker(重要)

4.6.2 包含其它Web应用程序资源中的内容 (重要)

4.6.3 在FTL中使用自定义JSP标签

4.6.4 在JSP页面中嵌入FTL

4.7 为FreeMarker配置安全策略

4.8 遗留的XML包装实现

4.8.1 模板标量模型(TemplateScalarModel)

4.8.2  模板集合模型(TemplateCollectionModel)

4.8.3  模板序列模型(TemplateSequenceModel)

4.8.4 模板哈希表模型(TemplateHashModel)

4.8.5 模板方法模型(TemplateMethodModel)

4.8.6 命名空间处理

4.9 和Ant一起使用FreeMarker

4.10 Jython包装器


1.入门

1.1创建 Configuration 实例

首先,你应该创建一个 freemarker.template.Configuration 实例, 然后调整它的设置。Configuration 实例是存储 FreeMarker 应用级设置的核心部分。同时,它也处理创建和 缓存 预解析模板(比如 Template 对象)的工作。

也许你只在应用(可能是servlet)生命周期的开始执行一次

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

当使用多线程应用程序(比如Web网站),Configuration 实例中的设置就不能被修改。它们可以被视作为 "有效的不可改变的" 对象, 也可以继续使用 安全发布 技术 (参考 JSR 133 和相关的文献)来保证实例对其它线程也可用。比如, 通过final或volatile字段来声明实例,或者通过线程安全的IoC容器,但不能作为普通字段。 (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 属性 (getProperty())可以通过 product.price 获取。(bean的action也可以通过这种方式拿到; 要了解更多可以参看 这里)

我们为 模板开发指南部分演示的第一个例子 来构建数据模型。为了方便说明,这里再展示一次示例:

在真实应用系统中,通常会使用应用程序指定的类来代替 Map, 它会有JavaBean规范规定的 getXxx/isXxx 方法。比如有一个和下面类似的类:

如果配置设置项 object_wrapper 的值是用于所有真实步骤, 这里描述的行为才好用。任何由 ObjectWrapper 包装成的哈希表 可以用作根root,也可以在模板中和点、 [] 操作符使用。 如果不是包装成哈希表的对象不能作为根root,也不能像那样在模板中使用。

1.3 获取模板

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

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

当调用这个方法的时候,将会创建一个 test.ftl 的 Template 实例,通过读取 /where/you/store/templates/test.ftl 文件,之后解析(编译)它。Template 实例以解析后的形式存储模板, 而不是以源文件的文本形式。

Configuration 缓存 Template 实例,当再次获得 test.ftl 的时候,它可能再读取和解析模板文件了, 而只是返回第一次的 Template 实例。

1.4 合并模板和数据模型

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

Writer out = new OutputStreamWriter(System.out);

temp.process(root, out)

这会向你的终端输出你在模板开发指南部分的 第一个示例 中看到的内容。

Java I/O 相关注意事项:基于 out 对象,必须保证 out.close() 最后被调用。当 out 对象被打开并将模板的输出写入文件时,这是很电影的做法。其它时候, 比如典型的Web应用程序,那就 不能 关闭 out 对象。FreeMarker 会在模板执行成功后 (也可以在 Configuration 中禁用) 调用 out.flush(),所以不必为此担心。

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

1.5 将代码放在一起

这是一个由之前的代码片段组合在一起的源程序文件。 千万不要忘了将 freemarker.jar 放到 CLASSPATH 中。

2.数据模型

2.1 基本内容

在 入门 章节中, 我们已经知道如何使用基本的Java类(MapString,等)来构建数据模型了。在内部,模板中可用的变量都是实现了 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.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 就不必知道子类型了。

 

2.3 容器(重要)

2.3.1 哈希表

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

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

经常使用的实现类是 SimpleHash,该类实现了 TemplateHashModelEx 接口。从内部来说,它使用一个 java.util.Hash 类型的对象存储子变量。 SimpleHash 类的方法可以添加和移除子变量。 这些方法应该用来在变量被创建之后直接初始化。

在FTL中,容器是一成不变的。那就是说你不能添加,替换和移除容器中的子变量。

2.3.2 序列

序列是实现了 TemplateSequenceModel 接口的Java对象。它包含两个方法:TemplateModel get(int index) 和 int size()

经常使用的实现类是 SimpleSequence。该类内部使用一个 java.util.List 类型的对象存储它的子变量。 SimpleSequence 有添加子元素的方法。 在序列创建之后应该使用这些方法来填充序列。

2.3.3 集合

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

通常使用的实现类是 SimpleCollection

2.4 方法

方法变量在存于实现了 TemplateMethodModel 接口的模板中。这个接口包含一个方法: TemplateModel exec(java.util.List arguments)。 当使用 方法调用表达式 调用方法时,exec 方法将会被调用。 形参将会包含FTL方法调用形参的值。exec 方法的返回值给出了FTL方法调用表达式的返回值。

TemplateMethodModelEx 接口扩展了 TemplateMethodModel 接口。它没有添加任何新方法。 事实上这个对象实现这个 标记 接口是给FTL引擎暗示, 形式参数应该直接以 TemplateModel 的形式放进 java.util.List。否则将会以 String 形式放入list。

2.5 指令

  • 示例 1

  • Java程序员可以使用 TemplateDirectiveModel 接口在Java代码中实现自定义指令。详情可以参加API文档。

  • TemplateDirectiveModel 在 FreeMarker 2.3.11 版本时才加入, 来代替快被废弃的 TemplateTransformModel

  • 示例 2

  • 注意

  • TemplateDirectiveModel 对象通常不应该是有状态的,这一点非常重要。 一个经常犯的错误是存储指令的状态然后在对象的属性中调用执行。 想一下相同指令的嵌入调用,或者指令对象被用作共享变量, 并通过多线程同时访问。

  • 不幸的是, TemplateDirectiveModel 不支持传递参数的位置(而不是参数名称)。从 FreeMarker 2.4 版本开始,它将被修正。

2.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 接口和其它接口的变量,因为结点变量属性仅仅提供基本的结点间导航的方法。 需要具体的例子,请参考 FreeMarker如何处理XML 部分。

2.7 对象包装

2.7.1 默认对象包装器

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

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

有时FreeMarker需要撤回映射,此时 对象包装器ObjectWrapper 的 Object 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),当使用 SimpleHash 的 put 方法来填充(这点很重要,它不会复制已经填充并存在的 Map)。这会加快顶层数据模型变量的访问速度。

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

会识别大部分基本的Java类型,比如 String, NumberBoolean, DateList (通常还有全部的 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 或更高版本中是极力推荐的(参看该效果的 API文档)。如何来设置:

  • 如果已经在 2.3.22 或更高版本的 Configuration 中设置了 incompatible_improvements 选项, 而没有设置 object_wrapper 选项(那么它就保留默认值), 我们就什么都做不了了,因为它已经使用了同等 incompatibleImprovements 属性值的 DefaultObjectWrapper 单例。

  • 另外也可以在 Configuration 中独立设置 incompatibleImprovements。基于如何创建/设置 ObjectWrapper,可以通过这样完成 (假设想要 incompatibleImprovements 2.3.22):

2.7.2 自定义对象包装示例

3.配置

3.1 基本内容

配置(configuration)就是 freemarker.template.Configuration 对象, 它存储了常用(全局,应用程序级)的设置,定义了想要在所有模板中可用的变量(称为共享变量)。 而且,它会处理 Template 实例的新建和缓存。

应用程序典型的用法是使用一个独立的共享 Configuration 实例。更精确来说, 典型的做法是每一个独立开发的组件(比如项目,模块等)都有一个 Configuration 实例,它在内部使用FreeMarker, 每一个都创建它自己的实例。

运行中的模板会受配置设置的影响,每个 Template 实例通过对应 Template 构造方法参数,都有和它相关联的 Configuration 实例。通常可以使用 Configuration.getTemplate (而不是直接调用 Template 的构造方法)来获得 Template 实例,此时,关联的 Configuration 实例就是调用 getTemplate 方法的。

3.2 共享变量

Shared variables (共享变量)是为所有模板定义的变量。可以使用 setSharedVariable 方法向配置中添加共享变量:

Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);

... cfg.setSharedVariable("warp", new WarpDirective());

cfg.setSharedVariable("company", "Foo Inc.");

在所有使用这个配置的模板中,名为 wrap 的用户自定义指令和一个名为 company 的字符串将会在数据模型的根root上可见, 那就不用在根哈希表上一次又一次的添加它们。在传递给 Template.process 的 根root对象里的变量将会隐藏同名的共享变量。

如果配置对象在多线程环境中使用,不要使用 TemplateModel 实现类来作为共享变量, 因为它是不是线程安全的! 这也是基于Servlet应用程序的典型情形。

出于向后兼容的特性,共享变量的集合初始化时 (就是对于新的 Configuration 实例来说)不能为空。 它包含下列用户自定义指令(用户自定义指令使用时需要用 @ 来代替#):

名称
capture_outputfreemarker.template.utility.CaptureOutput
compressfreemarker.template.utility.StandardCompress
html_escapefreemarker.template.utility.HtmlEscape
normalize_newlinesfreemarker.template.utility.NormalizeNewlines
xml_escapefreemarker.template.utility.XmlEscape

3.3 配置设置

Settings(配置设置) 是影响FreeMarker行为的已被命名的值。配置设置有很多, 例如:localenumber_format, default_encoding, template_exception_handler。可以参考 Configuration.setSetting(...)的Java API 文档 来查看配置设置的完整列表。

配置设置存储在 Configuration 实例中,可以在 Template 实例中被覆盖。比如,在配置中给 locale 设置为 "en_US", 那么使用该配置的所有模板中的 locale 都使用 "en_US", 除非在模板中locale被明确地设置成其它不同的值(参见 localization)。 因此,在 Configuration 中的值充当默认值, 这些值在每个模板中也可以被覆盖。在 Configuration 或 Template 实例中的值也可以在单独调用 Template.process 方法后被覆盖。 对于每个调用了 freemarker.core.Environment 对象的值在内部创建时就持有模板执行的运行时环境,也包括了那个级别被覆盖了的设置信息。 在模板执行时,那里存储的值也可以被改变,所以模板本身也可以设置配置信息, 比如在输出中途来变换 locale 设置。

配置信息可以被想象成3层(Configuration, TemplateEnvironment), 最高层包含特定的值,它为设置信息提供最有效的值。 比如(设置信息A到F仅仅是为这个示例而构想的):

Setting ASetting BSetting CSetting DSetting ESetting F
Layer 3: Environment1--1--
Layer 2: Template22--2-
Layer 1: Configuration3333--

配置信息的有效值为:A=1,B=2,C=3,D=1,E=2。 而F的设置则是 null,或者在你获取它的时候将抛出异常。

我们看看如何准确设置配置信息:

  • Configuration 层: 原则上设置配置信息时使用 Configuration 对象的setter方法,例如:

    Configuration myCfg = new Configuration(Configuration.VERSION_2_3_23);
    myCfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
    myCfg.setDefaultEncoding("UTF-8");
  • 在真正使用 Configuration 对象 (通常在初始化应用程序时)之前来配置它,后面必须将其视为只读的对象。

  • 在实践中,比如很多Web应用框架中,就应该使用这种框架特定的配置方式来进行配置, 比如使用成对的 String 来配置(像在 .properties 属性配置文件中那样)。 在这种情况下,框架的作者大多数使用 Configuration 对象的 setSetting(String name, String value) 方法。 这可以参考setSetting的API文档 部分来获取可用的设置名和参数的格式的信息。 而在Spring框架中,我们可以这样进行:

  • <bean id="freemarkerConfig"
        class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
      <property name="freemarkerSettings">
        <props>
          <prop key="incompatible_improvements">2.3.23</prop>
          <prop key="template_exception_handler">rethrow</prop>
          <prop key="default_encoding">UTF-8</prop>
        </props>
      </property>
    </bean>
  • 请注意,这种形式的配置( String 键-值对) 和直接使用 Configuration 的API相比, 很不幸地被限制了。

  • Template 层:对于被请求的本地化信息,模板的 locale 设置由 Configuration.getTemplate(...) 来设置。 否则,就不能在这里进行设置,除非想控制 Template 对象来代替 freemarker.cache.TemplateCache,这样的话, 应该在 Template 对象第一次被使用前就设置配置信息, 然后就将 Template 对象视为是只读的。

  • Environment 层:这里有两种配置方法:

  • 使用Java API:使用 Environment 对象的setter方法。当然想要在模板执行之前来做,然后当调用 myTemplate.process(...) 时会遇到问题, 因为在内部创建 Environment 对象后立即就执行模板了, 导致没有机会来进行设置。这个问题的解决可以用下面两个步骤进行:

  • Environment env = myTemplate.createProcessingEnvironment(root, out);
    env.setLocale(java.util.Locale.ITALY);
    env.setNumberFormat("0.####");
    env.process();  // process the template
  • 在模板中(通常这被认为是不好的做法)直接使用 setting 指令,例如:

  • <#setting locale="it_IT">
    <#setting number_format="0.####">
  • 在这层,当什么时候改变配置信息,是没有限制的。

  • 要知道 FreeMarker 支持什么样的配置信息还有它们的意义, 可以先看看FreeMarker Java API文档中的下面这部分内容:

  • 在三层中 freemarker.core.Configurable 的setter方法来配置。

  • 只在 Configuration 层可用的 freemarker.template.Configuration 的setter方法来配置。

  • 在三层中可用 String 键-值对书写的 freemarker.core.Configurable.setSetting(String,String) 配置。

  • 只在 Configuration 层中可用 String 键-值对书写的 freemarker.template.Configuration.setSetting(String, String) 配置

3.4 模板加载

模板加载器是加载基于抽象模板路径下,比如 "index.ftl" 或 "products/catalog.ftl" 的原生文本数据对象。 这由具体的模板加载器对象来确定它们取得请求数据时使用了什么样的数据来源 (文件夹中的文件,数据等等)。当调用 cfg.getTemplate (这里的 cfg 就是 Configuration 实例)时, FreeMarker询问模板加载器是否已经为 cfg 建立返回给定模板路径的文本,之后 FreeMarker 解析文本生成模板。

3.4.1 模板加载器-内建模板加载器

在 Configuration 中可以使用下面的方法来方便建立三种模板加载。 (每种方法都会在其内部新建一个模板加载器对象,然后创建 Configuration 实例来使用它。)

第一种方法在磁盘的文件系统上设置了一个明确的目录, 它确定了从哪里加载模板。不要说可能,File 参数肯定是一个存在的目录。否则,将会抛出异常。
void setDirectoryForTemplateLoading(File dir);

第二种调用方法使用了一个 Class 类型的参数和一个前缀。这是让你来指定什么时候通过相同的机制来加载模板, 不过是用Java的 ClassLoader 来加载类。 这就意味着传入的class参数会被 Class.getResource() 用来调用方法来找到模板。参数 prefix 是给模板的名称来加前缀的。在实际运行的环境中, 类加载机制是首选用来加载模板的方法,通常情况下,从类路径下加载文件的这种机制, 要比从文件系统的特定目录位置加载安全而且简单。在最终的应用程序中, 所有代码都使用 .jar 文件打包也是不错的, 这样用户就可以直接执行包含所有资源的 .jar 文件了。

void setClassForTemplateLoading(Class cl, String prefix);

第三种调用方式需要Web应用的上下文和一个基路径作为参数, 这个基路径是Web应用根路径(WEB-INF目录的上级目录)的相对路径。 那么加载器将会从Web应用目录开始加载模板。尽管加载方法对没有打包的 .war 文件起作用,因为它使用了 ServletContext.getResource() 方法来访问模板, 注意这里我们指的是“目录”。如果忽略了第二个参数(或使用了""), 那么就可以混合存储静态文件(.html.jpg等) 和 .ftl 文件,只是 .ftl 文件可以被送到客户端执行。 当然必须在 WEB-INF/web.xml 中配置一个Servlet来处理URI格式为 *.ftl 的用户请求,否则客户端无法获取到模板, 因此你将会看到Web服务器给出的秘密提示内容。在站点中不能使用空路径,这是一个问题, 你应该在 WEB-INF 目录下的某个位置存储模板文件, 这样模板源文件就不会偶然地被执行到,这种机制对servlet应用程序来加载模板来说, 是非常好用的方式,而且模板可以自动更新而不需重启Web应用程序, 但是对于类加载机制,这样就行不通了。

void setServletContextForTemplateLoading(Object servletContext, String path);

3.4.2 模板加载器-从多个位置加载模板

如果需要从多个位置加载模板,那就不得不为每个位置都实例化模板加载器对象, 将它们包装到一个称为 MultiTemplateLoader 的特殊模板加载器, 最终将这个加载器传递给 Configuration 对象的 setTemplateLoader(TemplateLoader loader)方法。 下面给出一个使用类加载器从两个不同位置加载模板的示例:

 

import freemarker.cache.*; // template loaders live in this package

 

FileTemplateLoader ftl1 = new FileTemplateLoader(new File("/tmp/templates"));

 

FileTemplateLoader ftl2 = new FileTemplateLoader(new File("/usr/data/templates"));

 

ClassTemplateLoader ctl = new ClassTemplateLoader(getClass(), "");

 

TemplateLoader[] loaders = new TemplateLoader[] { ftl1, ftl2, ctl };

 

MultiTemplateLoader mtl = new MultiTemplateLoader(loaders);

cfg.setTemplateLoader(mtl);

现在,FreeMarker将会尝试从 /tmp/templates 目录加载模板,如果在这个目录下没有发现请求的模板,它就会继续尝试从 /usr/data/templates 目录下加载,如果还是没有发现请求的模板, 那么它就会使用类加载器来加载模板。

 

3.4.3 模板加载器-从其他资源加载模板

如果内建的类加载器都不适合使用,那么就需要来编写自己的类加载器了, 这个类需要实现 freemarker.cache.TemplateLoader 接口, 然后将它传递给 Configuration 对象的 setTemplateLoader(TemplateLoader loader)方法。 可以阅读API JavaDoc文档获取更多信息。

如果模板需要通过URL访问其他模板,那么就不需要实现 TemplateLoader 接口了,可以选择子接口 freemarker.cache.URLTemplateLoader 来替代, 只需实现 URL getURL(String templateName) 方法即可。

 

3.4.4 模板加载器-模板名称(模板路径)

解析模板的名称(也就是模板路径)是由模板解析器来决定的。 但是要和其它对路径的格式要求很严格的组件一起使用。通常来说, 强烈建议模板加载器使用URL风格的路径。 在URL路径(或在UN*X路径)中符号有其它含义时,那么路径中不要使用 /(路径分隔符)字符,. (同目录符号)和..(父目录符号)。字符 *(星号)是被保留的, 它用于FreeMarker的 "模板获取" 特性。

://(或者使用 template_name_format 配置设置到 DEFAULT_2_4_0: (冒号) 字符)是被保留用来指定体系部分的,和URI中的相似。比如 someModule://foo/bar.ftl 使用 someModule,或者假定 DEFAULT_2_4_0 格式,classpath:foo/bar.ftl 使用 classpath 体系。解释体系部分完全由 TemplateLoader 决定。 (FreeMarker核心仅仅知道体系的想法,否则它不能正常处理相对模板名称。)

FreeMarker通常在将路径传递到 TemplateLoader 之前把它们正常化,所以路径中不会包含 /../ 这样的内容, 路径会相对于虚构的模板根路径(也就是它们不会以 / 开头)。 其中也不会包含 *,因为模板获取发生在很早的阶段。 此外,将 template_name_format 设置为 DEFAULT_2_4_0,多个连续的 / 将会被处理成单独的 / (除非它们是 :// 模式分隔符的一部分)。

请注意,不管主机运行的操作系统是什么, FreeMarker 模板加载时经常使用斜线(而不是反斜线)。

 

3.4.5 模板缓存

FreeMarker 是会缓存模板的(假设使用 Configuration 对象的方法来创建 Template 对象)。这就是说当调用 getTemplate方法时,FreeMarker不但返回了 Template 对象,而且还会将它存储在缓存中, 当下一次再以相同(或相等)路径调用 getTemplate 方法时, 那么它只返回缓存的 Template 实例, 而不会再次加载和解析模板文件了。

如果更改了模板文件,当下次调用模板时,FreeMarker 将会自动重新载入和解析模板。 然而,要检查模板文件是否改变内容了是需要时间的,有一个 Configuration 级别的设置被称作"更新延迟",它可以用来配置这个时间。 这个时间就是从上次对某个模板检查更新后,FreeMarker再次检查模板所要间隔的时间。 其默认值是5秒。如果想要看到模板立即更新的效果,那么就要把它设置为0。 要注意某些模板加载器也许在模板更新时可能会有问题。 例如,典型的例子就是在基于类加载器的模板加载器就不会注意到模板文件内容的改变。

当调用了 getTemplate 方法时, 与此同时FreeMarker意识到这个模板文件已经被移除了,所以这个模板也会从缓存中移除。 如果Java虚拟机认为会有内存溢出时,默认情况它会从缓存中移除任意模板。 此外,你还可以使用 Configuration 对象的 clearTemplateCache 方法手动清空缓存

何时将一个被缓存了的模板清除的实际应用策略是由配置的属性 cache_storage 来确定的,通过这个属性可以配置任何 CacheStorage 的实现。对于大多数用户来说, 使用 freemarker.cache.MruCacheStorage 就足够了。 这个缓存存储实现了二级最近使用的缓存。在第一级缓存中, 组件都被强烈引用到特定的最大数目(引用次数最多的组件不会被Java虚拟机抛弃, 而引用次数很少的组件则相反)。当超过最大数量时, 最近最少使用的组件将被送至二级缓存中,在那里它们被很少引用, 直到达到另一个最大的数目。引用强度的大小可以由构造方法来指定。 例如,设置强烈部分为20,轻微部分为250:

cfg.setCacheStorage(new freemarker.cache.MruCacheStorage(20, 250))

或者,使用 MruCacheStorage 缓存, 它是默认的缓存存储实现:

cfg.setSetting(Configuration.CACHE_STORAGE_KEY, "strong:20, soft:250");

当创建了一个新的 Configuration 对象时, 它使用一个 strongSizeLimit 值为0的 MruCacheStorage 缓存来初始化, softSizeLimit 的值是 Integer.MAX_VALUE (也就是在实际中,是无限大的)。但是使用非0的 strongSizeLimit 对于高负载的服务器来说也许是一个更好的策略,对于少量引用的组件来说, 如果资源消耗已经很高的话,Java虚拟机往往会引发更高的资源消耗, 因为它不断从缓存中抛出经常使用的模板,这些模板还不得不再次加载和解析。

 

3.5 错误控制

3.5.1 可能的异常

关于 FreeMarker 发生的异常,可以分为如下几类:

  • 当配置 FreeMarker 时发生异常:典型地情况,就是在应用程序初始化时, 仅仅配置了一次 FreeMarker。在这个过程中,异常就会发生, 从 FreeMarker 的API中,我们可以很清楚的看到这一点...

  • 当加载和解析模板时发生异常:调用了 Configuration.getTemplate(...) 方法, FreeMarker就要把模板文件加载到内存中然后来解析它 (除非模板已经在 Configuration 对象中被 缓存 了)。 在这期间,有两种异常可能发生:

  • 模板文件没有找到而发生的 IOException 异常 或在读取文件时发生其他的I/O问题。比如没有读取文件的权限,或者是磁盘错误。 这些错误的发出者是 TemplateLoader 对象,可以将它作为插件设置到 Configuration对象中。(为了正确起见:这里所说的”文件”, 是简化形式。例如,模板也可以存储在关系型数据库的表中。这是 TemplateLoader所要做的事。)

    • 根据FTL语言的规则,模板文件发生语法错误时会导致 freemarker.core.ParseException异常。当获得 Template对象 (Configuration.getTemplate(...))时, 这种错误就会发生,而不是当执行 (Template.process(...))模板的时候。 这种异常是 IOException 的一个子类。

  • 当执行(处理)模板时发生的异常,也就是当调用了 Template.process(...) 方法时会发生的两种异常:

  • 试图写入输出对象时发生错误而导致的 IOException 异常。

  • 执行模板时发生的其它问题而导致的 freemarker.template.TemplatException 异常。 比如,一个频繁发生的错误,就是当模板引用一个不存在的变量。 默认情况下,当 TemplatException 异常发生时, FreeMarker会用普通文本格式在输出中打印出FTL的错误信息和堆栈跟踪信息, 然后再次抛出 TemplatException 异常而中止模板的执行, 就可以捕捉到 Template.process(...) 方法抛出的异常了。而这种行为是可以定制的。FreeMarker也会经常写 TemplatException 异常的 日志。

3.5.2 根据TemplateException来自定义处理方式

TemplateException 异常在模板处理期间的抛出是由 freemarker.template.TemplateExceptionHandler 对象控制的,这个对象可以使用 setTemplateExceptionHandler(...) 方法配置到 Configuration 对象中。 TemplateExceptionHandler 对象只包含一个方法:

void handleTemplateException(TemplateException te, Environment env, Writer out) throws TemplateException;

无论 TemplateException 异常什么时候发生,这个方法都会被调用。 异常处理是传递的 te 参数控制的, 模板处理的运行时(Runtime,译者注)环境可以访问 env 变量, 处理器可以使用 out 变量来打印输出信息。 如果方法抛出异常(通常是重复抛出 te),那么模板的执行就会中止, 而且 Template.process(...) 方法也会抛出同样的异常。如果 handleTemplateException 对象不抛出异常,那么模板将会继续执行,就好像什么也没有发生过一样, 但是引发异常的语句将会被跳过(后面会详细说)。 当然,控制器仍然可以在输出中打印错误提示信息。

任何一种情况下,当 TemplateExceptionHandler 被调用前, FreeMarker 将会记录异常日志。

我们用实例来看一下,当错误控制器不抛出异常时, FreeMarker是如何跳过出错''语句''的。假设我们已经使用了如下模板异常控制器:

如果错误发生在非FTL标记(没有被包含在 <#...> 或 <@...>之间)的插值中, 那么整个插值将会被跳过。那么下面这个模板 (假设 badVar 在数据模型中不存在):

a${badVar}b

如果我们使用了 MyTemplateExceptionHandler,就会打印:

a[ERROR: Expression badVar is undefined on line 1, column 4 in test.ftl.]b

而下面这个模板也会打印相同信息(除了报错的列数位置会不同...):

a${"moo" + badVar}b

因为像这样来写时,只要插值内发生任何错误,整个插值都会被跳过。

如果错误发生在指令调用中参数的计算时,或者是指令参数列表发生问题时, 或在<@exp ...>中计算 exp时发生错误,或者 exp不是用户自定义的指令, 那么整个指令调用都会被跳过。例如:

a<#if badVar>Foo</#if>b
a[ERROR: Expression badVar is undefined on line 1, column 7 in test.ftl.]b

请注意,错误发生在 if 指令的开始标签 (<#if badVar>)中,但是整个指令的调用都被跳过了。 从逻辑上说,嵌套的内容(Foo)也被跳过了, 因为嵌套的内容是受被包含的指令(if)控制(打印)的。

下面这个的输出也是相同的(除了报错的列数会不同...):

a<#if "foo${badVar}" == "foobar">Foo</#if>b

因为,正如这样来写,在参数处理时发生任何一个错误, 整个指令的调用都将会被跳过。

如果错误发生在已经开始执行的指令之后,那么指令调用将不会被跳过。 也就是说,如果在嵌套的内容中发生任何错误:

a <#if true> Foo ${badVar} Bar </#if> c

或者在一个宏定义体内:

a <@test /> b <#macro test> Foo ${badVar} Bar </#macro>

那么输出将会是:

a Foo [ERROR: Expression badVar is undefined on line 4, column 5 in test.ftl.] Bar c

FreeMarker 本身带有这些预先编写的错误控制器:

  • TemplateExceptionHandler.DEBUG_HANDLER: 打印堆栈跟踪信息(包括FTL错误信息和FTL堆栈跟踪信息)和重新抛出的异常。 这是默认的异常控制器(也就是说,在所有新的 Configuration 对象中,它是初始化好的)。

  • TemplateExceptionHandler.HTML_DEBUG_HANDLER: 和 DEBUG_HANDLER 相同,但是它可以格式化堆栈跟踪信息, 那么就可以在Web浏览器中来阅读错误信息。 当你在制作HTML页面时,建议使用它而不是 DEBUG_HANDLER

  • TemplateExceptionHandler.IGNORE_HANDLER: 简单地压制所有异常(但是要记住,FreeMarker 仍然会写日志)。 它对处理异常没有任何作用,也不会重新抛出异常。

  • TemplateExceptionHandler.RETHROW_HANDLER: 简单重新抛出所有异常而不会做其它的事情。 这个控制器对Web应用程序(假设你在发生异常之后不想继续执行模板)来说非常好, 因为它在生成的页面发生错误的情况下,给了你很多对Web应用程序的控制权 (因为FreeMarker不向输出中打印任何关于该错误的信息)。 要获得更多在Web应用程序中处理错误的信息,可以 参见FAQ。

3.5.3 在模板中明确地处理错误

尽管它和 FreeMarker 的配置(本章的主题)无关,但是为了说明的完整性, 在这里提及一下,你可以在模板中直接控制错误。 通常这不是一个好习惯(尽量保持模板简单,技术含量不要太高),但有时仍然需要:

  • 处理不存在/为空的变量: 模板开发指南/模板/表达式/处理不存在的值

  • 在发生障碍的"porlets"中留存下来,还可以扩展参考: 模板语言参考 /指令参考/attempt, recover

3.6 "不兼容改进"设置

3.6.1 它要做什么

该设置指定了 FreeMarker 的版本号,那么就不会100%向后兼容bug修复和改进 你想要启用 已经实现的内容。 通常来说,默认把它留在2.3.0(最大向后兼容版本)是一个坏主意。

在新项目中,应该将它设置为实际使用的FreeMarker版本号。 而在老项目中,那么最好也将它设置的高一些,最好检查一下哪些修改是激活状态(可以在 Configuration(Version) 构造方法的API JavaDoc文档 中找到它们),至少不仅仅是 "不兼容改进" 第三版本号(小版本)设置提高。通常来讲, 只要为该设置增加最后的版本号,那么这些修改的风险就会小很多。

Bug修复和改进是完全向后兼容的,同样,它们也是重要的安全更新, 不管 "不兼容改进" 如何设置都是启用的。

该设置的一个重要结果是应用程序可以检查声明的 FreeMarker 最小版本需求是否达到。 比如你设置了2.3.22,但是应用程序却意外部署到了 FreeMarker 2.3.21, 那么FreeMarker就会出问题,告诉你需要一个更高的版本。 最后,请求的修复/改进不会作用于低版本。

3.6.2 如何设置

这个不兼容改进的设置在 Configuration 级别。 它可以在多种方式下设置(假设我们想将它设置为2.3.22):

  • 创建 freemarker.template.Configuration 对象,比如:

  • ... = new Configuration(Configuration.VERSION_2_3_22)
  • 或者,使用初始化设置来更改 Configuration 单例,比如:

  • cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_22)
  • 或者,如果使用properties文件来配置FreeMarker (*.properties 文件或者 java.util.Properties 对象),添加:

  • incompatible_improvements=2.3.22
  • 或者,如果通过 FreemarkerServlet 来配置FreeMarker,那么将 init-param 添加到 web.xml 中:

    <init-param>
        <param-name>incompatible_improvements</param-name>
        <param-value>2.3.22</param-value>
    </init-param>
  • 但是, 在应用程序中 如果设置了 object_wrapper (也就是 Configuration.setObjectWrapper(ObjectWrapper)), 那么要知道很重要的一点,BeansWrapper 和它的子类(最重要的是, DefaultObjectWrapper) 有它们自己独立的 incompatibleImprovements 属性,还有一些修复/改进会被它激活, 而不是通过 Configuration 的相似设置。 如果你没有在任何地方设置过 object_wrapper,那么不需要知道这点, 因为,和 Configuration 一样, 默认的 object_wrapper 也有相同的 "不兼容改进"。 但是,如果设置了 object_wrapper, 那么就不要忘了设置 ObjectWrapper 自己的 incompatibleImprovements 属性,此外还有 Configuration(请注意,对于 Configuration 来说, 有不同的 "不兼容改进" 也是可以的,而对于 ObjectWrapper, 则有它自己的选择。) 至于 DefaultObjectWrapper (对于 BeansWrapper 也是一样的,只是不同的类名而已) 参看这里如何来设置它。

4.其他

4.1 变量,范围

本章介绍当模板在访问变量时发生了什么事情,还有变量是如何存储的。

当调用 Template.process 方法时,它会在方法内部创建一个 Environment 对象,在 process 返回之前一直使用该对象存储模板执行时的运行状态信息。除了这些,它还存储由模板中指令,如 assign, macro, local 或 global 创建的变量。 它不会尝试修改传递给 process 的数据模型对象, 也不会创建或替换存储在配置中的共享变量。

当你想要读取一个变量时,FreeMarker 将会以这种顺序来查找, 直到发现了完全匹配的的变量名称才会停下来:

在 Environment 中

    1. 如果在循环中,在循环变量的集合中。 循环变量是由如 list 等指令来创建的。

    2. 如果在宏中,在宏的局部变量集合中。 局部变量可以由 local 指令创建。 而且,宏的参数也是局部变量。

    3. 在当前的 命名空间 中。 可以使用 assign 指令将变量放到一个命名空间中。

    4. 由 global 指令创建的变量集合中。 FTL将它们视为数据模型的普通成员变量一样来控制它们。 也就是说,它们在所有的命名空间中都可见, 你也可以像访问一个数据模型中的数据一样来访问它们。

  1. 在传递给 process 方法的数据模型对象中。

  2. 在 Configuration 对象存储的共享变量集合中。在实际操作中,来自模板设计者的观点是这6种情况应该只有4种, 因为从那种观点来看,后面3种(由 global 创建的变量, 真实的数据模型对象,共享变量)共同构成了全局变量的集合。

  3. 请注意,在FTL中可以从一个特定层面获取 特定变量

4.2 字符集问题

像其它大多数的Java应用程序一样,FreeMarker使用 "UNICODE 文本"(UTF-16)来工作。 不过,也有必须处理 字符集 的情况, 因为它不得不和外界交换数据,这就会使用到很多字符集。

4.2.1 输入的字符集

当 FreeMarker 要加载模板文件(或没有解析的文本文件)时, 那就必须要知道文件使用的字符集,因为文件的存储是原生的字节序列。 可以使用 encoding 配置 来确定字符集。 这个配置项只在 FreeMarker 使用 Configuration 对象的 getTemplate 方法加载模板(解析过的或没有解析过的)时起作用。请注意 include 指令 在内部也使用了这个方法,所以 encoding 的值对一个已经加载的模板,而且如果这个模板包含 include 指令的调用来说很重要。

encoding 配置的getter和setter方法在第一个(配置)层面很特殊。 getter方法猜想返回值是基于 Locale(本地化,译者注)传递的参数; 它在地图区域编码表(称为编码地图)中查询编码,如果没有找到该区域,就返回默认编码。 可以使用配置对象的 setEncoding(Locale locale, String encoding) 方法来填充编码表;编码表初始化时是空的。默认的初始编码是系统属性 file.encoding 的值,但是可以通过 setDefaultEncoding 方法来设置一个不同的默认值,而不是依赖它。 对于新项目来说,默认的编码设置就是 utf-8

也可以在模板层或运行环境层(当指定编码值作为 getTemplate 方法的参数时,应该在模板层覆盖 encoding 设置)直接给定值来覆盖 encoding 的设置。如果不覆盖它,那么 locale 设置的有效值将会是 configuration.getEncoding(Locale) 方法的返回值。

而且,代替这种基于字符集猜测的机制,也可以在模板文件中使用 ftl 指令, 比如 <#ftl encoding="utf-8"> 来指定特定的字符集。

请注意,模板使用的字符集和模板生成的输出内容的字符集是独立的 (除非包含 FreeMarker 的软件故意将设置输出内容的字符集和模板字符集设置成相同的)。

4.2.2 输出的字符集

output_encoding 设置/参数和 内建函数url 从FreeMarker 2.3.1版本开始才可以使用,而在2.3以前的版本中是不存在的。

原则上,FreeMarker 不处理输出内容的字符集问题, 因为 FreeMarker 将输出内容都写入了 java.io.Writer 对象中。而 Writer 对象是由封装了 FreeMarker(比如Web应用框架) 的软件生成的, 那么输出内容的字符集就是由封装软件来控制的。而FreeMarker有一个称为 output_encoding(开始于 FreeMarker 2.3.1 版本之后)的设置。 封装软件应该使用这个设置(Writer对象使用的字符集) 来通知 FreeMarker 在输出中(否则 FreeMarker 不能找到它)使用哪种字符集。 有一些新的特性,如内建函数url,特殊变量output_encoding 也利用这个信息。因此,如果封装软件没有设置字符集这个信息, 那么 FreeMarker 需要知道输出字符集的特性就不能被利用了。

如果你使用 FreeMarker 来编写软件, 你也许想知道在输出内容中到底选择了哪种字符集。 当然这取决于运行 FreeMarker 输出内容的计算机本身, 但是如果用户对这个问题可以变通, 那么通用的实践是使用模板文件的字符集作为输出的字符集,或者使用UTF-8。 通常使用UTF-8是最佳的实践,因为任意的文本可能来自数据模型, 那就可能包含不能被模板字符集所编码的字符。

如果使用了 Template.createProcessingEnvironment(...) 和 Environment.process(...) 方法来代替 Template.process(...) 方法, FreeMarker 的设置可以对任意独立执行的模板进行。 因此,你可以对每个独立执行的模板设置 output_encoding 信息

Writer w = new OutputStreamWriter(out, outputCharset);

Environment env = template.createProcessingEnvironment(dataModel, w);

env.setOutputEncoding(outputCharset); env.process();

4.3 多线程

在多线程运行环境中, Configuration 实例, Template 实例和数据模型应该是永远不能改变(只读)的对象。 也就是说,创建和初始化它们(如使用 set... 方法)之后,就不能再修改它们了(比如不能再次调用 set... 方法)。 这就允许我们在多线程环境中避免代价很大的同步锁问题。要小心 Template 实例; 当使用了 Configuration.getTemplate 方法获得 Template 一个实例时,也许得到的是从模板缓存中缓存的实例, 这些实例都已经被其他线程使用了,所以不要调用它们的 set... 方法 (当然调用 process 方法还是不错的)。

如果只从 同一个 独立线程中访问所有对象, 那么上面所述的限制将不会起作用。

使用FTL来修改数据模型对象或者 共享变量 是不太可能的, 除非将方法(或其他对象)放到数据模型中来做。 我们不鼓励你编写修改数据模型对象或共享变量的方法。多试试使用存储在环境对象 (这个对象是为独立的 Template.process 调用而创建的, 用来存储模板处理的运行状态)中的变量,所以最好不要修改那些由多线程使用的数据。 要获取更多信息,请阅读:变量,范围

4.4 Bean的包装

直接使用 BeansWrapper 是不推荐的。 而可以使用它的子类 DefaultObjectWrapper 来代替,要确保它的 incompatibleImprovements 属性至少是2.3.22DefaultObjectWrapper 给出了干净的数据模型(很少的容易混淆的多类型的FTL值)而且通常速度很快。关于更多 DefaultObjectWrapper 可以参看这里...

freemarker.ext.beans.BeansWrapper 是一个对象包装器, 最初加到FreeMarker中是为了将任意的POJO(Plan Old Java Objects,普通的Java对象) 包装成 TemplateModel 接口类型。 这样它就可以以正常的方式来进行处理,事实上 DefaultObjectWrapper 本身是 BeansWrapper 的扩展类。 这里描述的所有东西对 DefaultObjectWrapper 都是适用的, 除了 DefaultObjectWrapper 会用到 freemarker.template.SimpleXxx 类包装的StringNumber, Datearray, Collection(如List), MapBoolean 和 Iterator对象,会用 freemarker.ext.dom.NodeModel 来包装W3C的DOM结点(更多关于包装的W3C DOM 可以参考这里...), 所以上述这些描述的规则不适用。

当出现下面这些情况时, 你会想使用 BeansWrapper 包装器来代替 DefaultObjectWrapper

  • 在模板执行期间,数据模型中的 Collection 和 Map 应该被允许修改。 (DefaultObjectWrapper 会阻止这样做, 因为当它包装对象时创建了数据集合的拷贝,而这些拷贝都是只读的。)

  • 如果 arrayCollection 和 Map 对象的标识符当在模板中被传递到被包装对象的方法时, 必须被保留下来。也就是说,那些方法必须得到之前包装好的同类对象。

  • 如果在之前列出的Java API中的类(如 String, MapList 等)应该在模板中可见。 还有 BeansWrapper,默认情况下它们是不可见的, 但是可以设置获取的可见程度(后面将会介绍)。请注意这是一个不好的实践, 尽量去使用 内建函数 (如foo?sizefoo?upper_case, foo?replace('_', '-') 等)来代替Java API的使用。

  • 下面是对 BeansWrapper 创建的 TemplateModel 对象进行的总结。为了后续的讨论, 这里我们假设在包装之前对象都称为 obj, 而包装的后称为 model

4.4.1 模板哈希表模型功能(TemplateHashModel functionality)

所有的对象都将被包装成 TemplateHashModel 类型, 进而可以获取出JavaBean对象中的属性和方法。这样, 就可以在模板中使用 model.foo 的形式来调用 obj.getFoo() 方法或 obj.isFoo() 方法了。 (要注意公有的属性直接是不可见的,必须为它们编写getter方法才行) 公有方法通过哈希表模型来取得,就像模板方法模型那样, 因此可以使用 model.doBar() 来调用 object.doBar()。下面我们来更多讨论一下方法模型功能。

如果请求的键值不能映射到一个bean的属性或方法时, 那么框架将试图定位到"通用的get方法",这个方法的签名是 public any-return-type get(String) 或 public any-return-type get(Object), 使用请求键值来调用它们。请注意,这样就使得访问 java.util.Map 和其他类似类型的键值对非常便利。只要map的键是 String 类型的, 属性和方法名可以在映射中查到。(有一种解决方法可以用来避免在映射中遮挡名称,请继续来阅读。) 要而且注意 java.util.ResourceBundle 对象的方法使用 getObject(String) 方法作为通用的get方法。

如果在 BeansWrapper 实例中调用了 setExposeFields(true) 方法,那么它仍然会暴露出类的公有的, 非静态的变量,用它们作为哈希表的键和值。即如果 foo 是类 Bar 的一个公有的,非静态的变量,而 bar 是一个包装了 Bar 实例模板变量, 那么表达式 bar.foo 的值将会作为 bar 对象中 foo 变量的值。所有这个类的超类中公有变量都会被暴露出来。

4.4.2 说一点安全性

默认情况下,一些方法不能访问,它们被认为是模板中不安全的。 比如,不能使用同步方法(waitnotify, notifyAll),线程和线程组的管理方法(stop, suspendresumesetDaemon, setPriority),反射相关方法(Field, setXxx 方法, Method.invokeConstructor.newInstance, Class.newInstanceClass.getClassLoader等), System 和 Runtime 类中各种有危险性的方法 (execexithalt, load等)。BeansWrapper 也有一些安全级别 (被称作"方法暴露的级别"),默认的级别被称作为 EXPOSE_SAFE, 它可能对大多数应用程序来说是适用的。没有安全保证的级别称作是 EXPOSE_ALL,它允许你调用上述的不安全的方法。一个严格的级别是 EXPOSE_PROPERTIES_ONLY,它只会暴露出bean属性的getters方法。 最后,一个称作是 EXPOSE_NOTHING 的级别,它不会暴露任何属性和方法。 这种情况下,你可以通过哈希表模型接口访问的那些数据只是map和资源包中的项, 还有,可以从通用 get(Object) 方法和 get(String) 方法调用返回的对象,所提供的受影响的对象就有这样的方法。

4.4.3 模板标量模型功能(TemplateScalarModel functionality)

对于 java.lang.String 对象的模型会实现 TemplateScalarModel 接口,这个接口中的 getAsString() 方法简单代替了 toString() 方法。 要注意把 String 对象包装到Bean包装器中, 要提供比它们作为标量时更多的功能:因为哈希表接口描述了上述所需功能, 那么包装 String 的模型也会提供访问所有 String 的方法(indexOf, substring 等),尽管它们中很多都有内部的FreeMarker相同的实现, 最好使用它们(s?index_of(n)s[start..<end] 等)。

4.4.4 模板数字模型功能(TemplateNumberModel functionality)

对于是 java.lang.Number 的实例对象的模型包装器, 它们实现了 TemplateNumberModel 接口,接口中的 getAsNumber() 方法返回被包装的数字对象。 请注意把 Number 对象包装到Bean包装器中, 要提供比它们作为数字时更多的功能:因为哈希表接口描述了上述所需功能, 那么包装 Number 的模型也会提供访问所有他们的方法。

4.4.5 模板集合模型功能(TemplateCollectionModel functionality)

对于本地的Java数组和其他所有实现了 java.util.Collection 接口的类的模型包装器,都实现了 TemplateCollectionModel 接口, 因此也增强了使用 list 指令的附加功能。

4.4.6 模板序列模型功能(TemplateSequenceModel functionality)

对于本地的Java数组和其他所有实现了 java.util.List 接口的类的模型包装器,都实现了 TemplateSequenceModel 接口, 这样,它们之中的元素就可以使用 model[i] 这样的语法通过索引来访问了。 你也可以使用内建函数 model?size 来查询数组的长度和列表的大小。

而且,所有的方法都可指定的一个单独的参数,从 java.lang.Integer(即intlongfloatdoublejava.lang.Object, java.lang.Numberjava.lang.Integer) 中通过反射方法调用,这些类也实现了这个接口。 这就意味着你可以通过很方便的方式来访问被索引的bean属性: model.foo[i] 将会翻译为 obj.getFoo(i)

4.4.7 模板方法模型功能(TemplateMethodModel functionality)

一个对象的所有方法作为 TemplateMethodModelEx 对象的表述, 它们在对象模型的方法名中使用哈希表的键来访问。当使用 model.method(arg1arg2...) 来调用方法时,形参被作为模板模型传递给方法。 方法首先不会包装它们,后面我们会说到解包的详细内容。 这些不被包装的参数之后被实际方法来调用。以防止方法被重载, 许多特定的方法将会被选择使用相同的规则,也就是Java编译器从一些重载的方法中选择一个方法。 以防止没有方法签名匹配传递的参数,或者没有方法可以被无歧义地选择, 将会抛出 TemplateModelException 异常。

返回值类型为 void 的方法返回 TemplateModel.NOTHING,那么它们就可以使用 ${obj.method(args)} 形式的语法被安全地调用。

java.util.Map 实例的模型仍然实现了 TemplateMethodModelEx 接口,作为调用它们 get() 方法的一种方式。正如前面所讨论的那样, 你可以使用哈希表功能来访问"get"方法,但是它有一些缺点: 因为第一个属性和方法名会被键名来检查,所以执行过慢; 属性,方法名相冲突的键将会被隐藏;最终这种方法中你只可使用 String 类型的键。对比一下,调用 model(key) 方法,将直接翻译为 model.get(key):因为没有属性和方法名的查找, 速度会很快;不容易被隐藏;最终对非字符串的键也能正常处理, 因为参数没有被包装,只是被普通的方法调用。实际上, Map 中的 model(key) 和 model.get(key) 是相等的,只是写起来很短罢了。

java.util.ResourceBundle 类的模型也实现了 TemplateMethodModelEx 接口, 作为一种访问资源和信息格式化的方便形式。对资源包的单参数调用, 将会取回名称和未包装参数的 toString() 方法返回值一致的资源。 对资源包的多参数调用的情况和单参数一样,但是它会将参数作为格式化的模式传递给 java.text.MessageFormat,在第二个和后面的作为格式化的参数中使用未包装的值。 MessageFormat 对象将会使用它们原本的本地化资源包来初始化。

4.4.8 解包规则

当从模板中调用Java方法时,它的参数需要从模板模型转换回Java对象。 假设目标类型(方法常规参数被声明的类型)是用来 T 代表的, 下面的规则将会按下述的顺序进行依次尝试:

  • 对包装器来说,如果模型是空模型, 就返回Java中的 null

  • 如果模型实现了 AdapterTemplateModel 接口, 如果它是 T 的实例, 或者它是一个数字而且可以使用下面第三点描述的数字强制转换成 T, 那么 model.getAdaptedObject(T) 的结果会返回。 由BeansWrapper创建的所有方法是AdapterTemplateModel的实现, 所以由BeansWrapper为基本的Java对象创建的展开模型通常不如初始的Java对象。

  • 如果模型实现了已经废弃的 WrapperTemplateModel 接口, 如果它是 T 的实例, 或者它是一个数字而且可以使用下面第二点描述的数字强制转换成 T ,那么 model.getWrappedObject() 方法的结果会返回。

  • 如果 T 是 java.lang.String 类型, 那么如果模型实现了 TemplateScalarModel 接口,它的字符串值将会返回。 请注意,如果模型没有实现接口, 我们不能尝试使用String.valueOf(model)方法自动转换模型到String类型。 这里不得不使用内建函数?string明确地用字符串来处理非标量。

  • 如果 T 是原始的数字类型或者是可由 T 指定的 java.lang.Number 类型,还有模型实现了 TemplateNumberModel 接口, 如果它是 T 的实例或者是它的装箱类型 (如果 T 是原始类型),那么它的数字值会返回。 否则,如果 T 是一个Java内建的数字类型 (原始类型或是 java.lang.Number 的标准子类, 包括 BigInteger 和 BigDecimal), 类型 T 的一个新对象或是它的装箱类型会由数字模型的适当强制的值来生成。

  • 如果 T 是 boolean 值或 java.lang.Boolean 类型,模型实现了 TemplateBooleanModel 接口,那么布尔值将会返回。

  • 如果 T 是 java.util.Map 类型,模型实现了 TemplateHashModel 接口, 那么一个哈希表模型的特殊Map表示对象将会返回。

  • 如果 T 是 java.util.List 类型,模型实现了 TemplateSequenceModel 接口, 那么一个序列模型的特殊List表示对象将会返回。

  • 如果 T 是 java.util.Set 类型,模型实现了 TemplateCollectionModel 接口, 那么集合模型的一个特殊Set表示对象将会返回。

  • 如果 T 是 java.util.Collection 或 java.lang.Iterable 类型,模型实现了 TemplateCollectionModel 或 TemplateSequenceModel 接口, 那么集合或序列模型(各自地)一个特殊的Set或List表示对象将会返回。

  • 如果 T 是Java数组类型,模型实现了 TemplateSequenceModel 接口, 那么一个新的指定类型的数组将会创建, 它其中的元素使用数组的组件类型作为 T, 递归展开到数组中。

  • 如果 T 是 char 或 java.lang.Character 类型,模型实现了 TemplateScalarModel 接口, 它的字符串表示中包含精确的一个字符,那么一个 java.lang.Character 类型的值将会返回。

  • 如果 T 定义的是 java.util.Date 类型,模型实现了 TemplateDateModel 接口, 而且它的日期值是 T 的实例, 那么这个日期值将会返回。

  • 如果模型是数字模型,而且它的数字值是 T 的实例,那么数字值就会返回。 你可以得到一个实现了自定义接口的 java.lang.Number类型的自定义子类,也许T就是那个接口。(*)

  • 如果模型是日期类型,而且它的日期值是 T 的实例, 那么日期值将会返回。类似的考虑为(*)

  • 如果模型是标量类型,而且 T 可以从 java.lang.String 类型来定义, 那么字符串值将会返回。 这种情况涵盖T是java.lang.Object, java.lang.Comparable和java.io.Serializable类型。(**)

  • 如果模型是布尔类型,而且 T 可以从 java.lang.Boolean 类型来定义, 那么布尔值将会返回。 和(**)是相同的

  • 如果模型是哈希表类型,而且 T 可以从 freemarker.ext.beans.HashAdapter 类型来定义, 那么一个哈希表适配器将会返回。 和(**)是相同的

  • 如果模型是序列类型,而且 T 可以从 freemarker.ext.beans.SequenceAdapter 类型来定义, 那么一个序列适配器将会返回。 和(**)是相同的

  • 如果模型是集合类型,而且 T 可以从 freemarker.ext.beans.SetAdapter 类型来定义, 那么集合的set适配器将会返回。 和(**)是相同的

  • 如果模型是 T 的实例,那么模型本身将会返回。 这种情况涵盖方法明确地声明一个 FreeMarker 特定模型接口, 而且允许返回指令,当java.lang.Object被请求时允许返回方法和转换的模型

  • 意味着没有可能转换的异常被抛出。

4.4.9 访问静态方法

从 BeansWrapper.getStaticModels() 方法返回的 TemplateHashModel 可以用来创建哈希表模型来访问任意类的静态方法和字段。

BeansWrapper wrapper = BeansWrapper.getDefaultInstance();

TemplateHashModel staticModels = wrapper.getStaticModels();

TemplateHashModel fileStatics = (TemplateHashModel) staticModels.get("java.io.File")

之后就可以得到模板的哈希表模型,它会暴露所有 java.lang.System 类的静态方法和静态字段 (final类型和非final类型)作为哈希表的键。 设想你已经将之前的模型放到根root模型中了:

root.put("File", fileStatics);

从现在开始,你可以在模板中使用 ${File.SEPARATOR} 来插入文件分隔符,或者你可以列出所有文件系统中的根元素,通过:

<#list File.listRoots() as fileSystemRoot>...</#list>

当然,你必须小心这个模型所带来的潜在的安全问题。

你可以给模板作者完全的自由, 不管它们通过将静态方法的哈希表放到模板的根模型中, 来使用哪种类的静态方法,如用如下方式:

root.put("statics", BeansWrapper.getDefaultInstance().getStaticModels());

如果它被用作是以类名为键的哈希表, 这个对象暴露的只是任意类的静态方法。那么你可以在模板中使用如 ${statics["java.lang.System"].currentTimeMillis()} 这样的表达式。请注意,这样会有更多的安全隐患,比如, 如果方法暴露级别对 EXPOSE_ALL 是很弱的, 那么某些人可以使用这个模型调用 System.exit() 方法。

请注意,在上述的示例中,我们通常使用默认的 BeansWrapper 实例。这是一个方便使用的静态包装器实例, 你可以在很多情况下使用。特别是你想修改一些属性 (比如模型缓存,安全级别,或者是空模型对象表示)时, 你也可以自由地来创建自己的 BeansWrapper 实例, 然后用它们来代替默认包装器。

4.4.10 访问枚举类型

在JRE 1.5版本之后,从方法 BeansWrapper.getEnumModels() 返回的 TemplateHashModel 可以被用作创建访问枚举类型值的哈希表模型。 (试图在之前JRE中调用这个方法会导致 UnsupportedOperationException 异常。)

BeansWrapper wrapper = BeansWrapper.getDefaultInstance();

TemplateHashModel enumModels = wrapper.getEnumModels();

TemplateHashModel roundingModeEnums = (TemplateHashModel) enumModels.get("java.math.RoundingMode");

这样你就可以得到模板哈希表模型,它暴露了 java.math.RoundingMode 类所有枚举类型的值, 并把它们作为哈希表的键。设想你将之前的模型已经放入root模型中了:

root.put("RoundingMode", roundingModeEnums);

现在开始,你可以在模板中使用表达式 RoundingMode.UP 来引用枚举值 UP

你可以给模板作者完全的自由,不管它们使用哪种枚举类, 将枚举模型的哈希表放到模板的root模型中,可以这样来做:

root.put("enums", BeansWrapper.getDefaultInstance().getEnumModels());

如果它被用作是类名作为键的哈希表,这个对象暴露了任意的枚举类。 那么可以在模板中使用如 ${enums["java.math.RoundingMode"].UP} 的表达式。

被暴露的枚举值可以被用作是标量(它们会委派它们的 toString() 方法),也可以用在相同或不同的比较中。

请注意,在上述的例子中,我们通常使用默认的 BeansWrapper 实例。这是一个方便使用的静态包装器实例, 你可以在很多情况下使用。特别是你想修改一些属性 (比如模型缓存,安全级别,或者是空模型对象表示)时, 你也可以自由地来创建自己的 BeansWrapper 实例, 然后用它们来代替默认包装器。

4.5 日志

4.5.1 日志库选择

简而言之,在现代(比如2015年)的应用程序中, 记录日志推荐使用SLF4J API。 要让 FreeMarker 2.3.x. 使用SLF4J,在项目中加入依赖 org.slf4j:log4j-over-slf4j 即可, 要确保 log4j:log4j 不能存在。(从 FreeMarker 2.4.x 开始,尽管没有什么害处, 但也不再需要 log4j-over-slf4j 了。)

FreeMarker 整合了如下的日志包:SLF4JApache Commons LoggingLog4J 1.x,Avalon LogKit 和 java.util.logging默认情况下, FreeMarker(在2.3.x版本下)会按如下顺序来查找日志包, 而且会自动使用第一个发现的包: LOG4J(从2.3.22开始,如果正确安装了log4j-over-slf4j,则会使用SLF4J来代替), Apache Avalon LogKit, java.util.logging。 正如你所见,Log4j有最高的优先级。org.apache.log4j.Logger 类会检测Log4j的存在,那么也就是说,像log4j-over-slf4j 或 log4j-1.2-api,Log4j重定向也会有最高优先级。

在 FreeMarker 2.4 版本之前,因为向后兼容性的限制, SLF4J和Apache Commons Logging不会被自动搜索。但是如果你正确安装了 org.slf4j:log4j-over-slf4j(也就意味着, 在类路径下没有真实的Log4j,SLF4J有一个像 logback-classic 的支持实现),那么FreeMarker会直接使用SLF4J API来代替Log4j API (从FreeMarker 2.3.22版本开始)。

请注意,应用Log4j2日志有个相似的技巧:如果 org.apache.logging.log4j:log4j-1.2-api 可用, FreeMarker 2.3.x会使用它,因为它看起来就像Log4j, 但是所有的消息都会自动到Log4j2中。

如果自动检测没有给出你想要的结果,那么你可以设置系统属性 org.freemarker.loggerLibrary 来明确选择 (从2.3.22版本开始)一个日志库,比如:

java ... -Dorg.freemarker.loggerLibrary=SLF4J

系统属性支持的值有: SLF4J, CommonsLogging, JUL (即 java.util.logging), Avalon, auto (默认行为), none (关闭日志)。

请注意,为了可靠的运行,系统属性应该在JVM启动时(向上面那样)就该设置好, 而不是在Java代码之后。

推荐使用SLF4J,因为它在 FreeMarker 中运行的更好, 也是因为从 FreeMarker 2.4 版本开始它有自动检测的最高优先级。

4.5.2 日志分类

由FreeMarker产生的所有日志信息会被记录到名称由 freemarker.开头的日志记录器中。 现在被使用的记录器是:

日志分类名称目标
freemarker.beans记录Beans包装器模块的日志信息。
freemarker.cache记录模板加载和缓存相关的日志信息。
freemarker.runtime记录在模板执行期间的和特定分类无关的相关信息。 更重要的是,它会记录模板异常并在模板处理期间抛出 (但它却应该在现行的应用程序中禁用;稍后将会解释)。
freemarker.runtime.attempt记录在模板执行期间抛出的模板异常日志信息, 但是是开启DEBUG严重级别,并由 attempt/recover 指令捕捉。 请注意,该异常也会被记录到正常的日志记录器中 (比如freemarker.runtime)。
freemarker.servlet记录来自 FreemarkerServlet 类的消息
freemarker.jsp记录FreeMarker JSP 支持的消息

FreeMarker 会在模板执行期间使用 freemarker.runtime 记录异常,即便异常继续增加,最终由 Template.process 或 Environment.process 抛出。 (那些都是从应用程序或框架中调用模板时的API调用。) 良好的应用程序会记录它们抛出的异常,极少数情况下是处理它们而不去记录日志。 但是FreeMarker已经记录了异常,那么就会得到比期望的多一条日志记录。 要修复这个问题(从2.3.22版本开始),可以设置 log_template_exceptions (Configurable.setLogTemplateExceptions(boolean)) 为 false

4.6 在Servlet中使用FreeMarker

作为基础了解,在web应用程序范畴内使用 FreeMarker 和其它并没有什么不同; FreeMarker将它的输出写入传递给 Template.process 方法的 Writer 对象,它不关心 Writer 将输出写入控制台,文件或是 HttpServletResponse 的输出流。 FreeMarker 并不知道什么是servlet和web;它仅仅是使用模板文件来合并Java对象, 之后从它们中间生成输出文本。从这里可知,如何创建一个Web应用程序都随你的习惯来。

但是,你可能想在已经存在的Web应用框架中使用FreeMarker。 许多框架都是基于"Model 2"架构的,JSP页面来控制显示。 如果你使用了这样的框架(比如Apache Struts), 那么可以继续阅读本文。对于其他框架请参考它们的文档。

4.6.1 在"Model 2"中使用FreeMarker(重要)

许多框架依照HTTP请求转发给用户自定义的"action"类, 将数据作为属性放在 ServletContext, HttpSession 和 HttpServletRequest 对象中, 之后请求被框架派发到一个JSP页面中(视图层),使用属性传递过来的数据来生成HTML页面, 这样的策略通常就是所指的Model 2模型。

使用这样的框架,你就可以非常容易地用FTL文件来代替JSP文件。 但是,因为你的Servlet容器(Web应用程序服务器),不像JSP文件, 它可能并不知道如何处理FTL文件,那么就需要对Web应用程序进行一些额外的配置:

  1. 复制 freemarker.jar (从FreeMarker发布包的lib目录中) 到Web应用程序的 WEB-INF/lib 目录下。

  2. 将下面的部分添加到Web应用程序的 WEB-INF/web.xml 文件中 (调整部分内容是否需要):

  3. 比如,代替这个JSP页面 (注意它使用了Struts标签库来保存设计,而不是嵌入可怕的Java代码):

  4. 在 FreeMarker 中,<html:form action="/query">...</html:form> 仅仅被视为是静态文本,所以它会按照原本输出出来了,就像其他XML或HTML标记一样。 JSP标签也仅仅是FreeMarker的指令,没有什么特殊之处,所以你可以 使用FreeMarker语法 形式来调用它们,而不是JSP语法: <@html.form action="/query">...</@html.form>。 注意在FreeMarker语法中 不能像JSP那样在参数中使用 ${...}, 而且不能给参数值加引号。 所以这样是错误的:<#-- WRONG: --> <@my.jspTag color="${aVariable}" name="aStringLiteral" width="100" height=${a+b} />但下面这样是正确的:<#-- Good: --> <@my.jspTag color=aVariable name="aStringLiteral" width=100 height=a+b />

  5. 在这两个模板中,当你要引用 user 和 latestProduct 时,首先它会尝试去查找已经在模板中创建的同名变量 (比如 prod;如果你使用JSP:这是一个page范围内的属性)。 如果那样做不行,它会尝试在 HttpServletRequest 对象中查找那个名字的属性, 如果没有找到就在 HttpSession 中找,如果还没有找到那就在 ServletContext 中找。FTL按这种情况工作是因为 FreemarkerServlet 创建数据模型由上面提到的3个对象中的属性而来。 那也就是说,这种情况下根哈希表root不是 java.util.Map (正如本手册中的一些例子那样),而是 ServletContext+HttpSession+HttpServletRequest ;FreeMarker 在处理数据模型类型的时候非常灵活。所以如果你想将变量 "name" 放到数据模型中,那么你可以调用 servletRequest.setAttribute("name", "Fred");这是模型2的逻辑, 而 FreeMarker 将会适应它。

  6. FreemarkerServlet 也会在数据模型中放置3个哈希表, 这样你就可以直接访问3个对象中的属性了。这些哈希表变量是:Request, SessionApplication (和ServletContext对应)。它还会暴露另外一个名为 RequestParameters 的哈希表,这个哈希表提供访问HTTP请求中的参数。

  7. FreemarkerServlet 也有很多初始参数。 它可以被设置从任意路径来加载模板,从类路径下,或相对于Web应用程序的目录。 你可以设置模板使用的字符集。你还可以设置想使用的对象包装器等等。

  8. 通过子类别,FreemarkerServlet 易于定制特殊需要。 那就是说,如果你需要对所有模板添加一个额外的可用变量,使用servlet的子类, 覆盖 preTemplateProcess() 方法,在模板被执行前, 将你需要的额外数据放到模型中。或者在servlet的子类中,在 Configuration 中设置这些全局的变量作为 共享变量。

4.6.2 包含其它Web应用程序资源中的内容 (重要)

你可以使用由 FreemarkerServlet (2.3.15版本之后) 提供的客户化标签<@include_page path="..."/> 来包含另一个Web应用资源的内容到输出内容中;这对于整合JSP页面 (在同一Web服务器中生活在FreeMarker模板旁边) 的输出到FreeMarker模板的输出中非常有用。使用:

<@include_page path="path/to/some.jsp"/>

和在JSP中使用该标签是相同的:

<jsp:include page="path/to/some.jsp">

<@include_page ...> 不能和 <#include ...>搞混, 后者是为了包含FreeMarker模板而不会牵涉到Servlet容器。 使用 <#include ...> 包含的模板和包含它的模板共享模板处理状态, 比如数据模型和模板语言变量,而 <@include_page ...> 开始一个独立的HTTP请求处理。

一些Web应用框架为此提供它们自己的解决方案, 这种情况下你就可以使用它们来替代。 而一些Web应用框架不使用 FreemarkerServlet, 所以 include_page 是不可用的。

路径可以是相对的,也可以是绝对的。相对路径被解释成相对于当前HTTP请求 (一个可以触发模板执行的请求)的URL,而绝对路径在当前的servlet上下文 (当前的Web应用)中是绝对的。你不能从当前Web应用的外部包含页面。 注意你可以包含任意页面,而不仅仅是JSP页面; 我们仅仅使用以 .jsp 结尾的页面作为说明。

除了参数 path 之外,你也可以用布尔值 (当不指定时默认是true)指定一个名为 inherit_params 可选的参数来指定被包含的页面对当前的请求是否可见HTTP请求中的参数

最后,你可以指定一个名为 params 的可选参数, 来指定被包含页面可见的新请求参数。如果也传递继承的参数, 那么指定参数的值将会得到前缀名称相同的继承参数的值。params 的值必须是一个哈希表类型,它其中的每个值可以是字符串, 或者是字符串序列(如果你需要多值参数)。这里给出一个完整的示例:

<@include_page path="path/to/some.jsp" inherit_params=true params={"foo": "99", "bar": ["a", "b"]}/>

这会包含 path/to/some.jsp 页面, 传递它的所有的当前请求的参数,除了"foo"和"bar", 这两个会被分别设置为"99"和多值序列"a","b"。 如果原来请求中已经有这些参数的值了,那么新值会添加到原来存在的值中。 那就是说,如果"foo"有值"111"和"123",那么现在它会有"99","111","123"。

事实上使用 params 给参数传递非字符串值是可能的。这样的一个值首先会被转换为适合的Java对象 (数字,布尔值,日期等),之后调用它们Java对象的 toString() 方法来得到字符串值。最好不要依赖这种机制,作为替代, 明确参数值在模板级别不能转换成字符串类型之后, 在使用到它的地方可以使用内建函数 ?string 和 ?c

4.6.3 在FTL中使用自定义JSP标签

FreemarkerServlet 将一个哈希表类型的 JspTaglibs 放到数据模型中,就可以使用它来访问JSP标签库了。 自定义JSP标签库将被视为普通用户自定义指令来访问,自定义EL函数 (从 FreeMarker 2.3.22 版本开始)视为方法。例如,这个JSP文件:

JspTaglibs 不是 FreeMarker 的核心特性; 它只存在于通过 FreemarkerServlet 调用的模板。 这是因为JSP 标签/函数 假定一个servlet环境(FreeMarker不会), 加上一些Servlet概念被模仿成 FreemarkerServlet 创建的特定Freemarker数据模型。很多现代开发框架以纯净的方式使用FreeMarker, 而不是通过 FreemarkerServlet

因为自定义JSP标签是在JSP环境中来书写操作的,它们假设变量 (在JSP中常被指代"beans")被存储在4个范围中:page范围,request范围, session范围和application范围。FTL没有这样的表示法(4种范围),但是 FreemarkerServlet给自定义标签提供仿真的环境, 这样就可以维持JSP范围中的"beans"和FTL变量之间的对应关系。 对于自定义的JSP标签,请求request,会话session和应用application是和真实JSP相同的: javax.servlet.ServletContextHttpSession 和 ServletRequest 对象中的属性。从FTL的角度来看, 这三种范围都在数据模型中,这点前面已经解释了。page范围和FTL全局变量(参见global指令)是对应的。 那也就是,如果你使用 global 指令创建一个变量,通过仿真的JSP环境, 它会作为page范围变量对自定义标签可见。而且,如果一个JSP标签创建了一个新的page范围变量, 那么结果和用 global 指令创建的是相同的。 要注意在数据模型中的变量作为page范围的属性对JSP标签是不可见的,尽管它们在全局是可见的, 因为数据模型和请求,会话,应用范围是对应的,而不是page范围。

在JSP页面中,你可以对所有属性值加引号,这和参数类型是字符串, 布尔值或数字没有关系。但是因为在FTL模板中自定义标签可以被用户自定义FTL指令访问到, 你将不得不在自定义标签中使用FTL语法规则,而不是JSP语法。所以当你指定一个"属性"的值时, 那么在 = 的右边是一个 FTL 表达式因此, 你不能对布尔值和数字值的参数加引号 (比如:<@tiles.insert page="/layout.ftl" flush=true/>), 否则它们将被解释为字符串值,当FreeMarker试图传递值到期望非字符串值的自定义标记中时, 这就会引起类型不匹配错误。而且还要注意,这很自然,你可以使用任意FTL表达式作为属性的值, 比如变量,计算的结果值等。(比如:<@tiles.insert page=layoutName flush=foo && bar/>)

Servlet容器运行过程中,因为它实现了自身的轻量级JSP运行时环境, 它用到JSP标签库,而 FreeMarker 并不依赖于JSP支持。这是一个很小但值得注意的地方: 在它们的TLD文件中,开启 FreeMarker 的JSP运行时环境来分发事件到JSP标签库中注册时间监听器, 你应该将下面的内容添加到Web应用下的 WEB-INF/web.xml 文件中:

<listener> <listener-class>freemarker.ext.jsp.EventForwarding</listener-class> </listener>

请注意,尽管servlet容器没有本地的JSP支持,你也可以在 FreeMarker 中使用JSP标签库。 只是确保对JSP 1.2版本(或更新)的 javax.servlet.jsp.* 包在Web应用程序中可用就行。如果你的servlet容器只对JSP 1.1支持, 那么你不得不将下面六个类(比如你可以从Tomcat 5.x或Tomcat 4.x的jar包中提取)复制到Web应用的 WEB-INF/classes/...目录下: javax.servlet.jsp.tagext.IterationTag, javax.servlet.jsp.tagext.TryCatchFinally, javax.servlet.ServletContextListener, javax.servlet.ServletContextAttributeListener, javax.servlet.http.HttpSessionAttributeListener, javax.servlet.http.HttpSessionListener。但是要注意, 因为容器只支持JSP 1.1,通常是使用较早的Servlet 2.3之前的版本, 事件监听器可能就不支持,因此JSP 1.2标签库来注册事件监听器会正常工作。

在撰写本文档时,JSP已经升级到2.1了,许多特性也已经实现了, 除了JSP 2(也就是说JSP自定义标记在JSP语言中实现了)的"标签文件"特性。 标签文件需要被编译成Java类文件,在 FreeMarker 下才会有用。

JspTaglibs[uri] 会去找到URI指定的TLD,就像JSP的 @taglib 指令所做的。 它实现了JSP规范中所描述的TLD发现机制。这里可以阅读更多,但简而言之, 它会在 WEB-INF/web.xml taglib 元素中, 在 WEB-INF/**/*.tld 文件中,还有 WEB-INF/lib/*.{jar,zip}/META-INF/**/*.tld 文件中寻找TLD。 此外,当设置了 FreemarkerServlet 的初始化参数(从 2.3.22版本开始) MetaInfTldSources 和/或 ClasspathTlds, 即便是在WAR结构之外,它也会发现对于类加载器可见的TLD。参考 FreemarkerServlet 的Java API文档来获取更多描述。 它也可以从Java系统属性中来设置,当你想在Eclipse运行配置中来修改而不去修改 web.xml时,就可以随手完成;再强调一点,请参考 FreemarkerServlet API 文档。 FreemarkerServlet 也会识别 org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern servlet 上下文属性,并且将它中间的配置项添加到 MetaInfTldSources

4.6.4 在JSP页面中嵌入FTL

有一个标签库允许你将FTL片段放到JSP页面中。 嵌入的FTL片段可以访问JSP 的4种范围内的属性(Beans)。 你可以在 FreeMarker 发布包中找到一个可用的示例和这个标签库。

4.7 为FreeMarker配置安全策略

4.8 遗留的XML包装实现

遗留的XML包装已经废弃了。 FreeMarker 2.3 已经引入了对新的XML处理模型的支持。要支持它, 新的XML包装包已经引入了,就是 freemarker.ext.dom。 对于新用法,我们鼓励你使用。它会在XML处理指南中来说明。

freemarker.ext.xml.NodeListModel 类提供了来包装XML文档展示为结点树模板模型每个结点列表可以包含零个或多个XML结点 (文档类型,元素类型,文本类型,处理指令,注释,实体引用,CDATA段等)。 结点列表实现了下面模板的语义模型接口:

4.8.1 模板标量模型(TemplateScalarModel)

当使用一个标量时,结点列表将会呈现XML片段,表示其包含的结点。 这使得使用XML到XML转换模板很方便。

4.8.2  模板集合模型(TemplateCollectionModel)

当用 list 指令来使用一个集合时, 它会简单枚举它的结点。每个结点将会被当作一个新的单一结点组的结点列表返回。

4.8.3  模板序列模型(TemplateSequenceModel)

当被用作是序列时,它会返回第i个结点作为一个新的结点列表, 包含单独的被请求的结点。也就是说,要返回 <book> 元素的第三个 <chapter> 元素,你可以使用下面的代码 (结点索引是从零开始的):

<#assign thirdChapter = xmldoc.book.chapter[2]>

4.8.4 模板哈希表模型(TemplateHashModel)

当被用作是哈希表时,它基本上是用来遍历子结点。也就是说, 如果你有个名为 book 的结点列表, 并包装了一个有很多chapter的元素结点,那么 book.chapter 将会产生一个book元素的所有chapter元素的结点列表。@符号常被用来指代属性: book.@title 产生一个有单独属性的结点列表, 也就是book元素的title属性。

意识到下面这样的结果是很重要的,比如,如果 book 没有 chapter-s,那么 book.chapter 就是一个空序列,所以 xmldoc.book.chapter?? 就 不会 是 false,它会一直是 true!相似地, xmldoc.book.somethingTotallyNonsense?? 也不会是false。为了检查是否发现子结点,可以使用 xmldoc.book.chapter?size == 0

哈希表定义了一些"魔力键"。所有的这些键以下划线开头。 最值得注意的是_text,可以得到结点的文本内容: ${book.@title._text} 将会给模板交出属性的值。 相似地,_name将会取得元素或属性的名字。 * 或 _allChildren 返回所有结点列表元素中的直接子元素,而 @* 或 _allAttributes 返回结点列表中元素的所有属性。 还有很多这样的键;下面给出哈希表键的详细总结:

键名结果为
* 或 _children所有当前结点(非递归)的直接子元素。适用于元素和文档结点。
@* 或 _attributes当前结点的所有属性。仅适用于元素。
@attributeName当前结点的命名属性。适用于元素,声明和处理指令。 在声明中它支持属性 publicIdsystemId 和 elementName。在处理指令中,它支持属性 target 和 data,还有数据中以 name="value" 对出现的其他属性名。 对于声明和处理指令的属性结点是合成的,因此它们没有父结点。 要注意,@* 不能在声明或处理指令上进行操作。
_ancestor当前结点的所有祖先,直到根元素(递归)。 适用于类型和 _parent 相同的结点类型。
_ancestorOrSelf当前结点和它的所有祖先结点。 适用于和 _parent 相同的结点类型。
_cname当前结点(命名空间URI+本地名称)的标准名称, 每个结点(非递归)一个字符串值。适用于元素和属性。
_content当前结点的全部内容,包括子元素,文本, 实体引用和处理指令(非递归)。适用于元素和文档。
_descendant当前结点的所有递归的子孙元素。适用于文档和元素结点。
_descendantOrSelf当前结点和它的所有递归的子孙元素。适用于文档和元素结点。
_document当前结点所属的所有文档类型。适用于所有除文本的结点。
_doctype当前结点的声明。仅仅适用于文档类型结点。
_filterType这是一种按类型过滤的模板方法模型。当被调用时, 它会产生一个结点列表,仅仅包含它们当前结点, 这些结点的类型和传递给它们参数的一种类型相匹配。 你应该传递任意数量的字符串给这个方法, 其中包含来保持类型的名字。合法的类型名称是:"attribute", "cdata","comment","document","documentType", "element","entity","entityReference", "processingInstruction","text"。
_name当前结点的名称。每个结点(非递归)一个字符串值。 适用于元素和属性(返回它们的本地名称),实体, 处理指令(返回它的目标),声明(返回它的public ID)。
_nsprefix当前结点的命名空间前缀,每个结点(非递归)一个字符串值。 适用于元素和属性。
_nsuri当前结点的命名空间URI,每个结点(非递归)一个字符串值。 适用于元素和属性。
_parent当前结点的父结点。适用于元素,属性,注释,实体,处理指令。
_qname当前结点在 [namespacePrefix:]localName 形式的限定名,每个结点(非递归)一个字符串值。适用于元素和属性。
_registerNamespace(prefix, uri)注册一个对当前结点列表和从当前结点列表派生出的所有结点列表有指定前缀和URI的XML命名空间。 注册之后,你可以使用nodelist["prefix:localname"] 或 nodelist["@prefix:localname"] 语法来访问元素和属性, 它们的名字是命名空间范围内的。 注意命名空间的前缀需要不能和当前XML文档它自己使用的前缀相匹配, 因为命名空间纯粹是由URI来比较的。
_text当前结点的文本内容,每个结点(非递归)一个字符串值。 适用于元素,属性,注释,处理指令(返回它的数据)和CDATA段。 保留的XML字符('<'和'&')不能被转义。
_type返回描述结点类型的结点列表,每个结点包含一个字符串值。 可能的结点名称是:合法的结点名称是:"attribute","cdata","comment", "document","documentType","element","entity","entityReference", "processingInstruction","text"。如果结点类型是未知的,就返回"unknown"。
_unique当前结点的一个拷贝,仅仅保留每个结点第一次的出现,消除重复。 重复可以通过应用对树的向上遍历出现在结点列表中,如_parent, _ancestor_ancestorOrSelf 和 _document,也就是说,foo._children._parent 会返回一个结点列表,它包含foo中重复的结点,每个结点会包含出现的次数, 和它子结点数目相等。这些情况下,使用 foo._children._parent._unique 来消除重复。适用于所有结点类型。
其它键当前结点的子元素的名称和键相匹配。这允许以 book.chapter.title 这种风格语法进行方便的子元素遍历。 请注意,在技术上 nodeset.childname 和 nodeset("childname") 相同,但是两者写法都很短, 处理也很迅速。适用于文档和元素结点。

4.8.5 模板方法模型(TemplateMethodModel)

当被用作方法模型,它返回一个结点列表, 这个列表是处理结点列表中当前内容的XPath表达式的结果。 为了使这种特性能够工作,你必须将 Jaxen 类库放到类路径下。比如:

<#assign firstChapter=xmldoc("//chapter[first()]")>

4.8.6 命名空间处理

为了遍历有命名空间范围内名称的子元素这个目的, 你可以使用结点列表注册命名空间前缀。 你可以在Java代码中来做,调用:

public void registerNamespace(String prefix, String uri);

方法,或者在模板中使用

${nodelist._registerNamespace(prefix, uri)}

语法。从那里开始, 你可以在命名空间通过特定的URI来标记引用子元素,用这种语法

nodelist["prefix:localName"]

nodelist["@prefix:localName"]

和在 XPath 表达式中使用这些命名空间前缀一样。 命名空间使用一个结点列表来注册并传播到所有结点列表, 这些结点列表来自于原来的结点列表。要注意命名空间只可使用URI来进行匹配, 所以你可以在你的模板中安全地使用命名空间的前缀,这和在实际XML中的不同, 在模板和XML文档中,一个前缀只是一个对URI的本地别名。

4.9 和Ant一起使用FreeMarker

我们现在知道有两种"FreeMarker Ant tasks":

  • FreemarkerXmlTask:它来自于FreeMarker的发布包, 打包到 freemarker.jar 中。 这是使用FreeMarker模板转换XML文档的轻量级的,易于使用的Ant任务。 它的入口源文件(输入文件)是XML文件,和生成的输出文件对应, 这是通过单独模板实现的。也就是说,对于每个XML文件, 模板会被执行(在数据模型中的XML文档), 模板的输出会被写入到一个和原XML文件名相似名称的文件中。 因此,模板文件扮演了一个和XSLT样式表相似的角色,但它是FTL,而不是XSLT。

  • FMPP:这是一个重量级的,以很少的XML为中心, 第三方Ant任务(和独立的命令行工具)。 它主要的目的是用作为模板文件的源文件(输入文件)生成它们自己对应的输出文件, 但它也对以XML为源文件的 FreemarkerXmlTask 进行支持。 而且,相比于FreemarkerXmlTask,它还有额外的特性。 那么它的缺点是什么?它太复杂太一般化了,不容易掌握和使用。

  • 这一部分介绍了 FreemarkerXmlTask, 要了解FMPP更多的信息,可以访问它的主页:http://fmpp.sourceforge.net/。

  • 为了使用 FreemarkerXmlTask 首先必须在你的Ant构建文件中定义 freemarker.ext.ant.FreemarkerXmlTask,然后调用任务。 假设你想使用假定的"xml2html.ftl"模板转换一些XML文档到HTML, XML文档在目录"xml"中而HTML文档生成到目录"html"中,你应该这样来写:

  • 这个任务将会对每个XML文档调用模板。每个文档将会被解析成DOM树, 然后包装成FreeMarker结点变量。当模板开始执行时, 特殊变量 .node 被设置成XML文档结点的根root。

  • 请注意,如果你正使用遗留的(FreeMarker 2.2.x 和以前版本)XML适配器实现, 也同样可以进行,而且XML树的根结点被放置在数据模型中, 作为变量 document。它包含了遗留的 freemarker.ext.xml.NodeListModel 类的实例。

  • 请注意,所有通过构建文件定义的属性将会作为名为"properties"的哈希表模型来用。 一些其他方法也会可用;对模板中什么样的可用变量的详细描述, 还有什么样的属性可以被任务接受,参见 freemarker.ext.ant.FreemarkerXmlTask 的JavaDoc文档。

4.10 Jython包装器

freemarker.ext.jython 包包含了启用任意Jython对象的模型, 并被用作是TemplateModel。在一个基础的示例中, 你可以使用如下调用:

public TemplateModel wrap(Object obj);

freemarker.ext.jython.JythonWrapper 类的方法。 这个方法会包装传递的对象,包装成合适的 TemplateModel。 下面是一个对返回对象包装器的属性的总结。为了下面的讨论, 我们假设在模板模型根中,对对象 obj 调用 JythonWrapper 后模型名为 model

4.10.1 模板哈希表模型功能(TemplateHashModel functionality)

PyDictionary 和 PyStringMap 将会被包装成一个哈希表模型。 键的查找映射到 __finditem__ 方法;如果一个项没有被找到, 那么就返回一个为 None 的模型。

4.10.2 模板标量模型功能(TemplateScalarModel functionality)

每一个python对象会实现 TemplateScalarModel 接口, 其中的 getAsString() 方法会委派给 toString() 方法。

4.10.3 模板布尔值模型功能(TemplateBooleanModel functionality)

每一个python对象会实现 TemplateBooleanModel 接口, 其中的 getAsBoolean() 方法会指派给 __nonzero__() 方法, 符合Python语义的true/false。

4.10.4 模板数字模型功能(TemplateNumberModel functionality)

PyIntegerPyLong 和 PyFloat 对象的模型包装器实现了 TemplateNumberModel 接口,其中的 getAsNumber() 方法返回 __tojava__(java.lang.Number.class)

4.10.5 模板序列模型功能(TemplateSequenceModel functionality)

对所有扩展了 PySequence 的类的模型包装器会实现 TemplateSequenceModel 接口, 因此它们中的元素可以通过使用 model[i] 语法形式的序列来访问, 这会指派给__finditem__(i)。你也可以使用内建函数 model?size 查询数组的长度或者list的大小, 它会指派给 __len__()

最后

以上就是鳗鱼画笔为你收集整理的20200426-27 关于Freemarker中的入门,数据模型,配置,其他 知识梳理1.入门2.数据模型3.配置的全部内容,希望文章能够帮你解决20200426-27 关于Freemarker中的入门,数据模型,配置,其他 知识梳理1.入门2.数据模型3.配置所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(40)

评论列表共有 0 条评论

立即
投稿
返回
顶部