对象包装

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

对象包装器是实现了 freemarker.template.ObjectWrapper 接口的类。它的目标是实现Java对象(应用程序中特定类等,比如 StringMapList 实例)和FTL类型系统之间的映射。换句话说, 它指定了模板如何在数据模型(包含从模板中调用的Java方法的返回值)中发现Java对象。 对象包装器作为插件放入 Configuration 中,可以使用 object_wrapper 属性设置 (或者使用Configuration.setObjectWrapper)。

从技术角度来说,FTL类型系统由之前介绍过的 TemplateModel 子接口 (TemplateScalarModelTemplateHashModeTemplateSequenceModel等)来表示。要映射Java对象到FTL类型系统中, 对象包装器的 TemplateModel wrap(java.lang.Object obj) 方法会被调用。

有时FreeMarker需要撤回映射,此时 对象包装器ObjectWrapperObject unwrap(TemplateModel) 方法就被调用了 (或其他的变化,请参考API文档来获取详细内容)。最后的操作是在 ObjectWrapperAndUnwrapper 中,它是 ObjectWrapper 的子接口。很多实际的包装器会实现 ObjectWrapperAndUnwrapper 接口。

我们来看一下包装Java对象并包含其他对象 (比如 MapList,数组, 或者有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),当使用 SimpleHashput 方法来填充(这点很重要,它不会复制已经填充并存在的 Map)。这会加快顶层数据模型变量的访问速度。

默认对象包装器

object_wrapper Configuration 的默认设置是 freemarker.template.DefaultObjectWrapper 实例。除非有特别的需求,那么建议使用这个对象包装器,或者是自定义的 DefaultObjectWrapper 的子类。

它会识别大部分基本的Java类型,比如 StringNumberBooleanDateList (通常还有全部的 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 使用单例。

  • DefaultObjectWrapperincompatibleImprovements 属性, 这在 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。 如果使用 .propertiesFreemarkerServlet 初始化参数,就会像 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 实现来适配 TuppleTempateSequenceMoldel 接口:

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)