Bean wrapper

Page Contents

Note:

建议不要再直接使用BeansWrapper。请使用其子类DefaultObjectWrapper,尽管要确保其incompatibleImprovements属性至少为 2.3.22. DefaultObjectWrapper提供更清晰的数据模型(较少混淆的多类型 FTL 值),并且通常更快。 在此处查看有关 DefaultObjectWrapper 的更多信息...

freemarker.ext.beans.BeansWrapper是最初添加到 FreeMarker 的object wrapper,因此可以将任意 POJO-s(普通的旧 Java 对象)包装到TemplateModel接口中。从那时起,它已成为正常的处理方式,实际上DefaultObjectWrapper本身是BeansWrapper的扩展。因此,这里描述的所有内容也都适用于DefaultObjectWrapper,除了DefaultObjectWrapper将使用freemarker.template.SimpleXxx类包装StringNumberDatearrayCollection(如List),MapBooleanIterator对象以及具有freemarker.ext.dom.NodeModel(freemarker.ext.dom.NodeModel(__)的 W3C DOM 节点) ,因此上述规则不适用于这些规则。

这些情况下,您将要使用BeansWrapper而不是DefaultObjectWrapper

  • 在模板执行期间,应允许修改模型的Collection -s 和Map -s。 (DefaultObjectWrapper防止了这种情况,因为它会在包装时创建集合的副本,并且副本将是只读的.)

  • 如果将arrayCollectionMap对象的标识传递到模板中的包装对象的方法时,必须保留它们的标识。也就是说,如果这些方法必须获得与之前包装的对象完全相同的对象。

  • 如果前面列出的类(StringMapList ... etc)的 Java API 对于模板是可见的。即使使用BeansWrapper,它们在默认情况下也不可见,但是可以通过设置曝光水平来实现(请参阅下文)。请注意,这通常是一种不良做法;尝试使用the built-ins(例如foo?sizefoo?upper_casefoo?replace('_', '-') ...等)代替 Java API。

以下是BeansWrapper创建的TemplateModel -s 的摘要。为了进行下面的讨论,假定在包装之前将对象称为obj,在包装之后将对象称为model

TemplateHashModel functionality

每个对象都将包装在TemplateHashModel中,该TemplateHashModel将公开对象的 JavaBeans 属性和方法。这样,您可以在模板中使用model.foo来调用obj.getFoo()obj.isFoo()方法。 (请注意,公共字段不是直接可见的;您必须为它们编写一个 getter 方法.)公共方法也可以通过哈希模型作为模板方法模型进行检索,因此可以使用model.doBar()调用object.doBar()。关于方法模型功能的更多讨论。

如果无法将请求的键 Map 到 Bean 属性或方法,则框架将尝试找到所谓的“通用 get 方法”,即具有签名 public any-return-type get(String)或 public any-return-type get(Object)的方法,并使用请求的键调用该方法。请注意,这允许方便地访问java.util.Map和类似类中的 Map-只要 Map 的键是String,并且某些属性或方法名不会遮盖该 Map 即可。 (有一种避免 shade 的解决方案,请 continue 阅读.)另外请注意,java.util.ResourceBundle对象的模型使用getObject(String)作为通用的 get 方法。

如果您在BeansWrapper实例上调用setExposeFields(true),它将还将类的公共非静态字段公开为哈希键和值。即如果fooBar类的公共非静态字段,并且bar是包装Bar实例的模板变量,则bar.foo表达式将求值为bar对象的字段foo的值。该类的所有超类中的公共字段也都公开。

关于安全性

默认情况下,您将无法访问几种认为不安全的方法。例如,您不能使用同步方法(waitnotifynotifyAll),线程和线程组 Management 方法(stopsuspendresumesetDaemonsetPriority),反射(Field setXxx方法,Method.invokeConstructor.newInstanceClass.newInstanceClass.getClassLoader等)和SystemRuntime类(execexithaltload等)中的各种危险方法。 BeansWrapper具有多个安全级别(称为“方法公开级别”),默认名称EXPOSE_SAFE可能适用于大多数应用程序。有一个称为EXPOSE_ALL的无防护级别,它甚至可以调用上述不安全的方法,而有一个严格的EXPOSE_PROPERTIES_ONLY级别将仅公开 bean 属性的获取方法。最后,有一个名为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内置查询数组的长度或列表的大小。

同样,每个采用单个参数的方法(通过intlongfloatdoublejava.lang.Objectjava.lang.Numberjava.lang.Integer)都可以通过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):更快,因为没有属性和方法名称查找。它没有 shade;最后,它适用于非字符串键,因为参数与普通方法调用一样被解包。实际上,Map上的model(key)等于model.get(key),只是写入时间较短。

java.util.ResourceBundle的模型还实现TemplateMethodModelEx作为资源访问和消息格式的便捷方式。对 bundle 的单参数调用将检索名称与 unwrapped 参数的toString()值相对应的资源。对 bundle 的多参数调用还将检索名称与 unwrapped 参数的toString()值相对应的资源,但是它将使用它作为格式模式,并使用第二个及后续参数的 unwrapped 值将其传递给java.text.MessageFormat作为格式参数。 MessageFormat对象将使用产生它们的包的语言环境进行初始化。

Unwrapping rules

从模板调用 Java 方法时,需要将其参数从模板模型转换回 Java 对象。假设目标类型(方法的形式参数的声明类型)表示为T,则按以下 Sequences 尝试以下规则:

  • 如果该模型是此包装器的空模型,则返回 Java null

  • 如果模型实现AdapterTemplateModel,则返回model.getAdaptedObject(T)的结果(如果它是T的实例或它是一个数字),并且可以使用数字强制转换为T,如下面的三个项目符号所述。 BeansWrapper 创建的所有模型本身都是 AdapterTemplateModel 实现,因此解包由 BeansWrapper 为基础 Java 对象创建的模型始终会产生原始 Java 对象。

  • 如果模型实现了已弃用的WrapperTemplateModel,则返回model.getWrappedObject()的结果(如果它是T的实例或它是一个数字,并且可以使用数字强制转换为T,如下面的两个项目符号所述)。

  • 如果Tjava.lang.String,那么如果模型实现TemplateScalarModel,则返回其字符串值。请注意,如果模型未实现 TemplateScalarModel,则我们不会尝试使用 String.valueOf(model)自动将模型转换为字符串。您必须显式使用内置的?string 来将非标量作为字符串传递。

  • 如果T是原始数字类型,或者java.lang.Number可从T分配,并且模型实现TemplateNumberModel,则如果它是T的实例或其装箱类型(如果T是原始类型),则返回其数字值。否则,如果T是内置 Java 数字类型(原始类型或java.lang.Number的标准子类,包括BigIntegerBigDecimal),则将使用数字模型的适当强制值创建T类的新对象或其装箱类型。

  • 如果Tbooleanjava.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的实例,则返回其日期值。

  • 如果 model 是数字模型,并且其数值是T的实例,则返回该数值。您可以具有实现自定义接口的 java.lang.Number 的自定义子类,T 可能是该接口。 (*)

  • 如果 model 是日期模型,并且其日期值为T的实例,则返回日期值。与(*)类似的考虑

  • 如果 model 是标量模型,并且可以从java.lang.String分配T,则返回字符串值。这涵盖了 T 为 java.lang.Object,java.lang.Comparable 和 java.io.Serializable(**)的情况

  • 如果 model 是 Boolean 模型,并且可以从java.lang.Boolean分配T,则返回布尔值。和...一样 (**)

  • 如果模型是哈希模型,并且可以从freemarker.ext.beans.HashAdapter分配T,则返回哈希适配器。和...一样 (**)

  • 如果 model 是序列模型,并且T可从freemarker.ext.beans.SequenceAdapter分配,则返回序列适配器。和...一样 (**)

  • 如果 model 是集合模型,并且可以从freemarker.ext.beans.SetAdapter分配T,则返回集合的设置适配器。和...一样 (**)

  • 如果模型是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类的所有静态方法和静态字段(最终的和非最终的)公开为哈希键。假设您将先前的模型放在根模型中:

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实例并改用它们,尤其是当您想要修改其某些 Feature(例如模型缓存,安全级别或空模型表示形式)时。

Accessing enums

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.put("RoundingMode", roundingModeEnums);

从现在开始,您可以使用RoundingMode.UP作为表达式来引用模板中的UP枚举值。

您甚至可以通过以下方式为模板作者提供完全的自由:使用以下方法将枚举模型哈希放入模板根模型中:

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

如果此对象用作以类名作为键的哈希值,则此对象公开任何枚举类。然后,您可以在模板中使用诸如${enums["java.math.RoundingMode"].UP}之类的表达式。

暴露的枚举值可以用作标量(它们将委托给它们的toString()方法),也可以用于相等性和不相等性比较中。

请注意,在以上示例中,我们始终使用默认的BeansWrapper实例。这是一个方便的静态包装器实例,您可以在大多数情况下使用。您还可以自由创建自己的BeansWrapper实例并改用它们,尤其是当您想要修改其某些 Feature(例如模型缓存,安全级别或空模型表示形式)时。