对象包装
对象包装器是实现了 freemarker.template.ObjectWrapper
接口的类。它的目标是实现Java对象(应用程序中特定类等,比如 String
,
Map
,List
实例)和FTL类型系统之间的映射。换句话说,
它指定了模板如何在数据模型(包含从模板中调用的Java方法的返回值)中发现Java对象。
对象包装器作为插件放入 Configuration
中,可以使用
object_wrapper
属性设置
(或者使用Configuration.setObjectWrapper
)。
从技术角度来说,FTL类型系统由之前介绍过的 TemplateModel
子接口
(TemplateScalarModel
,TemplateHashMode
,
TemplateSequenceModel
等)来表示。要映射Java对象到FTL类型系统中,
对象包装器的 TemplateModel wrap(java.lang.Object obj)
方法会被调用。
有时FreeMarker需要撤回映射,此时 对象包装器ObjectWrapper
的 Object unwrap(TemplateModel)
方法就被调用了
(或其他的变化,请参考API文档来获取详细内容)。最后的操作是在
ObjectWrapperAndUnwrapper
中,它是
ObjectWrapper
的子接口。很多实际的包装器会实现
ObjectWrapperAndUnwrapper
接口。
我们来看一下包装Java对象并包含其他对象
(比如 Map
,List
,数组,
或者有JavaBean属性的对象)是如何进行的。可以这么说,对象包装器将
Object[]
数组包装成 TemplateSquenceModel
接口的一些实现。当FreeMarker需要FTL序列中项的时候,它会调用
TemplateSquenceModel.get(int index)
方法。该方法的返回值是
TemplateModel
,也就是说,TemplateSquenceModel
实现不仅仅可以从给定的数组序列获取 对象
,
也可以负责在返回它之前包装该值。为了解决这个问题,典型的
TemplateSquenceModel
实现将会存储它创建的
ObjectWrapper
,之后再调用该 ObjectWrapper
来包装包含的值。相同的逻辑代表了 TemplateHashModel
或其他的
TemplateModel
,它是其它 TemplateModel
的容器。
因此,通常不论值的层次结构有多深,所有值都会被同一个 ObjectWrapper
包装。(要创建 TemplateModel
的实现类,请遵循这个原则,可以使用
freemarker.template.WrappingTemplateModel
作为基类。)
数据模型本身(root变量)是 TemplateHashModel
。
在 Template.process
中指定的root对象将会被在
object_wrapper
配置中设置的对象包装器所包装,并产生一个
TemplateHashModel
。从此,被包含值的包装遵循之前描述的逻辑
(比如,容器负责包装它的子实例)。
行为良好的对象包装器都会绕过已经实现 TemplateModel
接口的对象。如果将已经实现 TemplateModel
的对象放到数据模型中
(或者从模板中调用的Java方法返回这个对象),那么就可以避免实际的对象包装。
当特别是通过模板访问创建的值时,通常会这么做。因此,要避免更多上面对象包装的性能问题,
但也可以精确控制模板可以看到的内容(不是基于当前对象包装器的映射策略)。
常见的应用程序使用该手法是使用 freemarker.template.SimpleHash
作为数据模型的根root(而不是Map
),当使用 SimpleHash
的 put
方法来填充(这点很重要,它不会复制已经填充并存在的
Map
)。这会加快顶层数据模型变量的访问速度。
默认对象包装器
object_wrapper
Configuration
的默认设置是 freemarker.template.DefaultObjectWrapper
实例。除非有特别的需求,那么建议使用这个对象包装器,或者是自定义的
DefaultObjectWrapper
的子类。
它会识别大部分基本的Java类型,比如 String
,
Number
,Boolean
,
Date
,List
(通常还有全部的
java.util.Collection
类型),
数组,Map
等。并把它们自然地包装成匹配
TemplateModel
接口的对象。它也会使用
freemarker.ext.dom.NodeModel
来包装W3C DOM结点,
所以可以很方便地处理XML, 在XML章节会有描述)。
对于Jython对象,会代理到 freemarker.ext.jython.JythonWrapper
上。
而对于其它所有对象,则会调用 BeansWrapper.wrap
(超类的方法),
暴露出对象的JavaBean属性作为哈希表项
(比如FTL中的 myObj.foo
会在后面调用 getFoo()
),
也会暴露出对象(比如FTL中的 myObj.bar(1, 2)
就会调用方法)
的公有方法(JavaBean action)。(关于对象包装器的更多信息,请参阅
该章节。)
关于 DefaultObjectWrapper
更多值得注意的细节:
-
不用经常使用它的构造方法,而是使用
DefaultObjectWrapperBuilder
来创建它。 这就允许 FreeMarker 使用单例。 -
DefaultObjectWrapper
有incompatibleImprovements
属性, 这在 2.3.22 或更高版本中是极力推荐的(参看该效果的 API文档)。如何来设置:-
如果已经在 2.3.22 或更高版本的
Configuration
中设置了incompatible_improvements
选项, 而没有设置object_wrapper
选项(那么它就保留默认值), 我们就什么都做不了了,因为它已经使用了同等incompatibleImprovements
属性值的DefaultObjectWrapper
单例。 -
另外也可以在
Configuration
中独立设置incompatibleImprovements
。基于如何创建/设置ObjectWrapper
,可以通过这样完成 (假设想要incompatibleImprovements
2.3.22):-
如果使用了构建器API:
... = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_22).build()
-
或者使用构造方法:
... = new DefaultObjectWrapper(Configuration.VERSION_2_3_22)
-
或者使用
object_wrapper
属性 (*.properties
文件或java.util.Properties
对象):object_wrapper=DefaultObjectWrapper(2.3.21)
-
或者通过
FreemarkerServlet
配置object_wrapper
和在web.xml
中的init-param
属性来配置:<init-param> <param-name>object_wrapper</param-name> <param-value>DefaultObjectWrapper(2.3.21)</param-value> </init-param>
-
-
-
在新的或测试覆盖良好的项目中,也建议设置
forceLegacyNonListCollections
属性为false
。 如果使用.properties
或FreemarkerServlet
初始化参数,就会像DefaultObjectWrapper(2.3.22, forceLegacyNonListCollections=false)
, 同时,使用Java API可以在DefaultObjectWrapperBuilder
对象调用build()
之前调用setForceLegacyNonListCollections(false)
。 -
自定义
DefaultObjectWrapper
的最常用方法是覆盖handleUnknownType
方法。
自定义对象包装示例
我们假定有一个应用程序特定的类,像下面这样:
package com.example.myapp; public class Tupple<E1, E2> { public Tupple(E1 e1, E2 e2) { ... } public E1 getE1() { ... } public E2 getE2() { ... } }
若想让模板将它视作长度为2的序列,那么就可以这么来调用
someTupple[1]
,
<#list someTupple
...>
, 或者
someTupple?size
。需要创建一个
TemplateSequenceModel
实现来适配
Tupple
到
TempateSequenceMoldel
接口:
package com.example.myapp.freemarker; ... public class TuppleAdapter extends WrappingTemplateModel implements TemplateSequenceModel, AdapterTemplateModel { private final Tupple<?, ?> tupple; public TuppleAdapter(Tupple<?, ?> tupple, ObjectWrapper ow) { super(ow); // coming from WrappingTemplateModel this.tupple = tupple; } @Override // coming from TemplateSequenceModel public int size() throws TemplateModelException { return 2; } @Override // coming from TemplateSequenceModel public TemplateModel get(int index) throws TemplateModelException { switch (index) { case 0: return wrap(tupple.getE1()); case 1: return wrap(tupple.getE2()); default: return null; } } @Override // coming from AdapterTemplateModel public Object getAdaptedObject(Class hint) { return tupple; } }
关于类和接口:
-
TemplateSequenceModel
: 这就是为什么模板将它视为序列 -
WrappingTemplateModel
: 只是一个方便使用的类,用于TemplateModel
对象进行自身包装。通常仅对包含其它对象的对象需要。 参考上面的wrap(...)
调用。 -
AdapterTemplateModel
: 表明模板模型适配一个已经存在的对象到TemplateModel
接口, 那么去掉包装就会给出原有对象。
最后,我们告诉 FreeMarker 用 TuppleAdapter
(或者,可以在将它们传递到FreeMarker之前手动包装它们)
包装 Tupple
。那样的话,首先创建一个自定义的对象包装器:
package com.example.myapp.freemarker; ... public class MyAppObjectWrapper extends DefaultObjectWrapper { public MyAppObjectWrapper(Version incompatibleImprovements) { super(incompatibleImprovements); } @Override protected TemplateModel handleUnknownType(final Object obj) throws TemplateModelException { if (obj instanceof Tupple) { return new TuppleAdapter((Tupple<?, ?>) obj, this); } return super.handleUnknownType(obj); } }
那么当配置 FreeMarker (关于配置,参考这里...) 将我们的对象包装器插在:
// Where you initialize the cfg *singleton* (happens just once in the application life-cycle): cfg = new Configuration(Configuration.VERSION_2_3_22); ... cfg.setObjectWrapper(new MyAppObjectWrapper(cfg.getIncompatibleImprovements()));
或者使用 java.util.Properties
来代替配置
FreeMarker (也就是 .properties
文件):
object_wrapper=com.example.myapp.freemarker.MyAppObjectWrapper(2.3.22)