Bean的包装

优质
小牛编辑
130浏览
2023-12-01

直接使用 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 类包装的StringNumberDatearrayCollection(如List), MapBooleanIterator对象,会用 freemarker.ext.dom.NodeModel 来包装W3C的DOM结点(更多关于包装的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..<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, 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 可以用来创建哈希表模型来访问任意类的静态方法和字段。

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 实例, 然后用它们来代替默认包装器。