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

学习笔记,选自freeMarker中文文档,译自 Email: ddekany at users.sourceforge.net

1.变量、范围

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

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

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

  1. 在 Environment 中:

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

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

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

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

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

  3. Configuration 对象存储的共享变量集合中。

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

2.字符集问题

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

输入的字符集

  当 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 的软件故意将设置输出内容的字符集和模板字符集设置成相同的)。

输出的字符集

注意: 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 信息:

1 Writer w = new OutputStreamWriter(out, outputCharset);
2 Environment env = template.createProcessingEnvironment(dataModel, w);
3 env.setOutputEncoding(outputCharset);
4 env.process();

3.多线程

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

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

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

4.Bean的包装

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

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

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

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

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

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

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

模板哈希表模型功能(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 变量的值。所有这个类的超类中公有变量都会被暴露出来。

说一点安全性

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

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

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

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

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

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

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

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

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

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

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

  一个对象的所有方法作为 TemplateMethodModelEx 对象的表述, 它们在对象模型的方法名中使用哈希表的键来访问。当使用 model.method(arg1, arg2, ...) 来调用方法时,形参被作为模板模型传递给方法。 方法首先不会包装它们,后面我们会说到解包的详细内容。 这些不被包装的参数之后被实际方法来调用。以防止方法被重载, 许多特定的方法将会被选择使用相同的规则,也就是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 对象将会使用它们原本的本地化资源包来初始化。

解包规则

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

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

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

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

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

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

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

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

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

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

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

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

  • 如果 Tcharjava.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被请求时允许返回方法和转换的模型。

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

访问静态方法

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

1 BeansWrapper wrapper = BeansWrapper.getDefaultInstance();
2 TemplateHashModel staticModels = wrapper.getStaticModels();
3 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 实例, 然后用它们来代替默认包装器。

访问枚举类型

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

1 BeansWrapper wrapper = BeansWrapper.getDefaultInstance();
2 TemplateHashModel enumModels = wrapper.getEnumModels();
3 TemplateHashModel roundingModeEnums =
4     (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 实例, 然后用它们来代替默认包装器。

5.日志

日志库选择

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

  如果你对这些细节好奇,或者不能使用SLF4J,那么就继续阅读吧...

  默认情况下, 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-slf4jlog4j-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

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

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

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

日志分类

  由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.processEnvironment.process 抛出。 (那些都是从应用程序或框架中调用模板时的API调用。) 良好的应用程序会记录它们抛出的异常,极少数情况下是处理它们而不去记录日志。 但是FreeMarker已经记录了异常,那么就会得到比期望的多一条日志记录。 要修复这个问题(从2.3.22版本开始),可以设置 log_template_exceptions (Configurable.setLogTemplateExceptions(boolean)) 为 false

6.在Servlet中使用freeMarker

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

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

在"Model 2"中使用FreeMarker

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

freeMarker(九)——程序开发指南补充知识_第1张图片

 

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

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

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

 1 <servlet>
 2   <servlet-name>freemarkerservlet-name>
 3   <servlet-class>freemarker.ext.servlet.FreemarkerServletservlet-class>
 4     
 5   
 6   <init-param>
 7     <param-name>TemplatePathparam-name>
 8     <param-value>/param-value>
 9   init-param>
10   <init-param>
11     <param-name>NoCacheparam-name>
12     <param-value>trueparam-value>
13   init-param>
14   <init-param>
15     <param-name>ContentTypeparam-name>
16     <param-value>text/html; charset=UTF-8param-value> 
17   init-param>
18     
19   
20   <init-param>
21     <param-name>incompatible_improvementsparam-name>
22     <param-value>2.3.22param-value>
23     
25   init-param>
26   <init-param>
27     <param-name>template_exception_handlerparam-name>
28     
29     <param-value>rethrowparam-value>
30   init-param>
31   <init-param>
32     <param-name>template_update_delayparam-name>
33     
34     <param-value>0param-value>
35   init-param>
36   <init-param>
37     <param-name>default_encodingparam-name>
38     
39     <param-value>UTF-8param-value>
40   init-param>
41   <init-param>
42     <param-name>localeparam-name>
43     
44     <param-value>en_USparam-value>
45   init-param>
46   <init-param>
47     <param-name>number_formatparam-name>
48     <param-value>0.##########param-value>
49   init-param>
50 
51   <load-on-startup>1load-on-startup>
52 servlet>
53 
54 <servlet-mapping>
55   <servlet-name>freemarkerservlet-name>
56   <url-pattern>*.ftlurl-pattern>
57 servlet-mapping>
58 
59 ...
60 
61 
66 <security-constraint>
67   <web-resource-collection>
68     <web-resource-name>FreeMarker MVC Viewsweb-resource-name>
69     <url-pattern>*.ftlurl-pattern>
70   web-resource-collection>
71   <auth-constraint>
72     
73   auth-constraint>
74 security-constraint>

  在这之后,你可以像使用JSP(*.jsp) 文件那样使用FTL文件(*.ftl)了。 (当然你可以选择除 ftl 之外的扩展名;这只是惯例)

注意:它是怎么工作的?让我们先来看看JSP是怎么工作的。 许多servlet容器处理JSP时使用一个映射为 *.jsp 的servlet请求URL格式。这样servlet就会接收所有URL是以 .jsp 结尾的请求,查找请求URL地址中的JSP文件, 内部编译后生成 Servlet,然后调用生成好的serlvet来生成页面。 这里为URL类型是 *.ftl 映射的 FreemarkerServlet 也是相同功能,只是FTL文件不会编译成 Servlet,而是给 Template 对象, 之后 Template 对象的 process 方法就会被调用来生成页面。

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

 1 <%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
 2 <%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
 3 
 4 <html>
 5 <head><title>Acmee Products Internationaltitle>
 6 <body>
 7   <h1>Hello <bean:write name="user"/>!h1>
 8   <p>These are our latest offers:
 9   <ul>
10     <logic:iterate name="latestProducts" id="prod">
11       <li><bean:write name="prod" property="name"/>
12         for <bean:write name="prod" property="price"/> Credits.
13     logic:iterate>
14   ul>
15 body>
16 html>

  你可以使用这个FTL文件(使用 ftl 扩展名而不是 jsp):

 1 <html>
 2 <head><title>Acmee Products Internationaltitle>
 3 <body>
 4   <h1>Hello ${user}!h1>
 5   <p>These are our latest offers:
 6   <ul>
 7     <#list latestProducts as prod>
 8       <li>${prod.name} for ${prod.price} Credits.
 9     #list>
10   ul>
11 body>
12 html>
警告:在 FreeMarker 中, ... 仅仅被视为是静态文本,所以它会按照原本输出出来了,就像其他XML或HTML标记一样。 JSP标签也仅仅是FreeMarker的指令,没有什么特殊之处,所以你可以 使用FreeMarker语法 形式来调用它们,而不是JSP语法: <@html.form action="/query">...。 注意在FreeMarker语法中 不能像JSP那样在参数中使用 ${...}, 而且 不能给参数值加引号。 所以这样是 错误的
1 <#-- WRONG: -->
2 <@my.jspTag color="${aVariable}" name="aStringLiteral"
3             width="100" height=${a+b} />

  但下面这样是正确的:

1 <#-- Good: -->
2 <@my.jspTag color=aVariable name="aStringLiteral"
3             width=100 height=a+b />

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

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

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

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

  要获取更多信息,可以阅读该类的Java API文档。

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

在FTL中使用自定义JSP标签

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

 1 <%@ page contentType="text/html;charset=ISO-8859-2" language="java"%>
 2 <%@ taglib prefix="e" uri="/WEB-INF/example.tld" %>
 3 <%@ taglib prefix="oe" uri="/WEB-INF/other-example.tld" %>
 4 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
 5 <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
 6 
 7 <%-- Custom JSP tags and functions: --%>
 8 
 9 <e:someTag numParam="123" boolParam="true" strParam="Example" anotherParam="${someVar}">
10   ...
11 e:someTag>
12 
13 <oe:otherTag />
14 
15 ${e:someELFunction(1, 2)}
16 
17 
18 <%-- JSTL: --%>
19 
20 <c:if test="${foo}">
21   Do this
22 c:if>
23 
24 <c:choose>
25   <c:when test="${x == 1}">
26       Do this
27   c:when>
28   <c:otherwise>
29       Do that
30   c:otherwise>
31 c:choose>
32 
33 <c:forEach var="person" items="${persons}">
34   ${person.name}
35 c:forEach>
36 
37 ${fn:trim(bar)}

  基本一致的FTL是:

 1 <#assign e=JspTaglibs["/WEB-INF/example.tld"]>
 2 <#assign oe=JspTaglibs["/WEB-INF/other-example.tld"]>
 3 
 4 <#-- Custom JSP tags and functions: --#>
 5 
 6 <@e.someTag numParam=123 boolParam=true strParam="Example" anotherParam=someVar>
 7   ...
 8 @e.someTag>
 9 
10 <@oe.otherTag />
11 
12 ${e.someELFunction(1, 2)}
13 
14 
15 <#-- JSTL - Instead, use native FTL constructs: -->
16 
17 <#if foo>
18   Do this
19 #if>
20 
21 <#if x == 1>
22   Do this
23 <#else>
24   Do that
25 #if>
26 
27 <#list persons as person>
28   ${person.name}
29 #list>
30 
31 ${bar?trim}
注意:

1.参数值没有使用引号,而且 "${...}" 和JSP中使用的一样。 后面会详细解释。

2. 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.ServletContextHttpSessionServletRequest 对象中的属性。从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/>)

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

<listener>
  <listener-class>freemarker.ext.jsp.EventForwardinglistener-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.IterationTagjavax.servlet.jsp.tagext.TryCatchFinallyjavax.servlet.ServletContextListenerjavax.servlet.ServletContextAttributeListenerjavax.servlet.http.HttpSessionAttributeListenerjavax.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

在JSP页面中嵌入FTL

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

7.为freeMarker配置安全策略

  当 FreeMarker 运行在装有安全管理器的Java虚拟机中时, 你不得不再授与一些权限,确保运行良好。最值得注意的是, 你需要为对 freemarker.jar 的安全策略文件添加这些条目:

1 grant codeBase "file:/path/to/freemarker.jar" 
2 {
3   permission java.util.PropertyPermission "file.encoding", "read";
4   permission java.util.PropertyPermission "freemarker.*", "read";
5 }

  另外,如果从一个目录中加载模板, 你还需要给 FreeMarker 授权来从那个目录下读取文件, 使用如下的授权:

grant codeBase "file:/path/to/freemarker.jar" 
{
  ...
  permission java.io.FilePermission "/path/to/templates/-", "read";
}

  最终,如果你使用默认的模板加载机制,也就是从当前文件夹下加载模板, 那么需要指定这些授权内容:(请注意,表达式 ${user.dir} 将会在运行时被策略解释器处理,几乎它就是一个 FreeMarker 模板)

1 grant codeBase "file:/path/to/freemarker.jar" 
2 {
3   ...
4   permission java.util.PropertyPermission "user.dir", "read";
5   permission java.io.FilePermission "${user.dir}/-", "read";
6 }

  很自然地,如果你在Windows下运行, 使用两个反斜杠来代替一个斜杠来分隔路径中的目录间隔。

8.遗留的XML包装实现

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

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

模板标量模型(TemplateScalarModel)

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

模板集合模型(TemplateCollectionModel)

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

模板序列模型(TemplateSequenceModel)

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

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

模板哈希表模型(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 当前结点的命名属性。适用于元素,声明和处理指令。 在声明中它支持属性 publicIdsystemIdelementName。在处理指令中,它支持属性 targetdata,还有数据中以 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.childnamenodeset("childname") 相同,但是两者写法都很短, 处理也很迅速。适用于文档和元素结点。

模板方法模型(TemplateMethodModel)

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

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

命名空间处理

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

public void registerNamespace(String prefix, String uri);

  方法,或者在模板中使用

${nodelist._registerNamespace(prefix, uri)}

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

nodelist["prefix:localName"]

  和

nodelist["@prefix:localName"]

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

 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"中,你应该这样来写:

1 <taskdef name="freemarker" classname="freemarker.ext.ant.FreemarkerXmlTask">
2   <classpath>
3     <pathelement location="freemarker.jar" />
4   classpath>
5 taskdef>
6 <mkdir dir="html" />
7 <freemarker basedir="xml" destdir="html" includes="**/*.xml" template="xml2html.ftl" />

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

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

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

10.Jython包装器

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

public TemplateModel wrap(Object obj);

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

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

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

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

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

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

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

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

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

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

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

 

译自 Email: ddekany at users.sourceforge.net

转载于:https://www.cnblogs.com/fx-blog/p/6293229.html

你可能感兴趣的:(freeMarker(九)——程序开发指南补充知识)