Bean的包装
直接使用 BeansWrapper
是不推荐的。
而可以使用它的子类 DefaultObjectWrapper
来代替,要确保它的 incompatibleImprovements
属性至少是2.3.22。DefaultObjectWrapper
给出了干净的数据模型(很少的容易混淆的多类型的FTL值)而且通常速度很快。关于更多
DefaultObjectWrapper 可以参看这里...
freemarker.ext.beans.BeansWrapper
是一个对象包装器,
最初加到FreeMarker中是为了将任意的POJO(Plan Old Java Objects,普通的Java对象)
包装成 TemplateModel
接口类型。
这样它就可以以正常的方式来进行处理,事实上 DefaultObjectWrapper
本身是 BeansWrapper
的扩展类。
这里描述的所有东西对 DefaultObjectWrapper
都是适用的,
除了 DefaultObjectWrapper
会用到
freemarker.template.SimpleXxx
类包装的String
,Number
,
Date
,array
,
Collection
(如List
),
Map
,Boolean
和
Iterator
对象,会用 freemarker.ext.dom.NodeModel
来包装W3C的DOM结点(更多关于包装的W3C DOM 可以参考这里...),
所以上述这些描述的规则不适用。
当出现下面这些情况时,
你会想使用 BeansWrapper
包装器来代替
DefaultObjectWrapper
:
-
在模板执行期间,数据模型中的
Collection
和Map
应该被允许修改。 (DefaultObjectWrapper
会阻止这样做, 因为当它包装对象时创建了数据集合的拷贝,而这些拷贝都是只读的。) -
如果
array
,Collection
和Map
对象的标识符当在模板中被传递到被包装对象的方法时, 必须被保留下来。也就是说,那些方法必须得到之前包装好的同类对象。 -
如果在之前列出的Java API中的类(如
String
,Map
,List
等)应该在模板中可见。 还有BeansWrapper
,默认情况下它们是不可见的, 但是可以设置获取的可见程度(后面将会介绍)。请注意这是一个不好的实践, 尽量去使用 内建函数 (如foo?size
,foo?upper_case
,foo?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
变量的值。所有这个类的超类中公有变量都会被暴露出来。
说一点安全性
默认情况下,一些方法不能访问,它们被认为是模板中不安全的。
比如,不能使用同步方法(wait
,notify
,
notifyAll
),线程和线程组的管理方法(stop
,
suspend
,resume
,setDaemon
,
setPriority
),反射相关方法(Field
,
setXxx
方法,
Method.invoke
,Constructor.newInstance
,
Class.newInstance
,Class.getClassLoader
等),
System
和 Runtime
类中各种有危险性的方法
(exec
,exit
,halt
,
load
等)。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
的方法(indexOf
,
substring
等),尽管它们中很多都有内部的FreeMarker相同的实现,
最好使用它们(s?index_of(n)
,s[start..<end]
等)。
模板数字模型功能(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
, double
,java.lang.Object
,
java.lang.Number
,java.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()
方法的结果会返回。 -
如果
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被请求时允许返回方法和转换的模型。 -
意味着没有可能转换的异常被抛出。
访问静态方法
从 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
实例,
然后用它们来代替默认包装器。
访问枚举类型
在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
实例,
然后用它们来代替默认包装器。