你的位置:首页 > Java教程

[Java教程]Freemarker 程序开发


Freemarker 程序开发

 

现在web开发中,多使用freemarker 来描述页面。通常会使用的macro来定义各种组件,从而达到UI组件的复用。结合使用其它的指定,可快速的描述一个html页面。那么能否将freemarker用在其它地方呢?又是如何将freemarker用到其它地方呢。本篇文章就来介绍一下freemarker的用法。

 

         首先回答第一个问题,是肯定能够用到其它地方的。

有关第二个问题,则在看完下面的内容后,答案自明。

 

Freemarker 程序开发

  • 1、入门
  • 2、数据模型
    • 2.1 基本内容
    • 2.2 标量类型
    • 2.3 容器类型
    • 2.4 方法
    • 2.5 指令
    • 2.6 对象包装
  • 3、配置
    • 共享变量
    • 配置设置
    • 模板加载
      • 模板加载器
      • 模板缓存

 

 

1、入门:

 

首先,你应该创建一个 freemarker.template.Configuration 实例, 然后调整它的设置。Configuration 实例是存储 FreeMarker 应用级设置的核心部分。同时,它也处理创建和 缓存 预解析模板(比如 Template 对象)的工作。

也许你只在应用(可能是servlet)生命周期的开始执行一次

一个Configuration创建工厂:

 

package com.fjn.helper.frameworkex.freemarker;

 

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

 

import freemarker.template.Configuration;

import freemarker.template.TemplateExceptionHandler;

 

public class ConfigurationFactory {

    private Configuration config;

    private static final Logger logger = LoggerFactory.getLogger(ConfigurationFactory.class);

    private Object initLock = new Object();

 

    public Configuration getConfig() {

       init();

       return config;

    }

 

    private void init() {

       if (config != null) {

           return;

       }

       synchronized (initLock) {

           if (config != null) {

              return;

           }

           Configuration config = new Configuration();

           try {

              // File rootClassPath = new

              // File(this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI());

              // config.setDirectoryForTemplateLoading(new File(rootClassPath,

              // "/ftls"));

              config.setClassForTemplateLoading(ConfigurationFactory.class, "/ftls");

 

              config.setDefaultEncoding("UTF-8");

              config.setDateFormat("yyyy-MM-dd");

              config.setNumberFormat("#.##");

              config.setClassicCompatible(true);

               config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);

              config.setTemplateUpdateDelay(10);

 

              this.config = config;

           } catch (Exception ex) {

              logger.error(ex.getMessage(), ex);

           }

       }

    }

}

 

 

在简单的示例中你可以使用 java.lang 和 java.util 包中的类, 还有用户自定义的Java Bean来构建数据对象:

  • 使用 java.lang.String 来构建字符串。
  • 使用 java.lang.Number 来派生数字类型。
  • 使用 java.lang.Boolean 来构建布尔值。
  • 使用 java.util.List 或Java数组来构建序列。
  • 使用 java.util.Map 来构建哈希表。
  • 使用自定义的bean类来构建哈希表,bean中的项和bean的属性对应。比如, product 的 price 属性 (getProperty())可以通过product.price 获取。(bean的action也可以通过这种方式拿到;

 

package com.fjn.helper.frameworkex.freemarker;

 

public class Product {

    private String url;

    private String name;

   

    public String getUrl() {

       return url;

    }

    public void setUrl(String url) {

       this.url = url;

    }

    public String getName() {

       return name;

    }

    public void setName(String name) {

       this.name = name;

    }

}

 

 

 

package com.fjn.helper.frameworkex.freemarker;

 

import java.io.IOException;

import java.io.OutputStreamWriter;

import java.io.Writer;

import java.util.HashMap;

import java.util.Map;

 

import org.junit.Test;

 

import freemarker.template.Template;

import freemarker.template.TemplateException;

 

public class ProductTest {

         private ConfigurationFactory factory = new ConfigurationFactory();

 

         @Test

         public void test() throws TemplateException, IOException {

                   // Create the root hash

                   Map<String, Object> root = new HashMap<>();

                   // Put string ``user'' into the root

                   root.put("user", "Big Joe");

                   // Create the hash for ``latestProduct''

                   Map<String, Object> latest = new HashMap<>();

                   // and put it into the root

                   root.put("latestProduct", latest);

                   // put ``url'' and ``name'' into latest

                   latest.put("url", "products/greenmouse.html");

                   latest.put("name", "green mouse");

 

                   // get a template

                   Template template = factory.getConfig().getTemplate("product.ftl");

 

                   Writer out = new OutputStreamWriter(System.out);

                   template.process(root, out);

                   out.flush();

                   System.out.println("over......");

         }

}

 

Java I/O 相关注意事项:基于 out 对象,必须保证 out.close() 最后被调用。当 out 对象被打开并将模板的输出写入文件时,这是很电影的做法。其它时候, 比如典型的Web应用程序,那就 不能 关闭 out 对象。

 

 

总结一下使用freemarker开发程序的流程:

1、创建一个configuration ,并做一些配置。

2、开发模板文件(.ftl)

3、为每一个模板指定相应的数据模型(map)。

4、使用configuration取得模板并进行处理生成最终的文件。

 

 

2、数据模型

 

2.1 基本内容:

在 入门 章节中, 我们已经知道如何使用基本的Java类(MapString,等)来构建数据模型了。在内部,模板中可用的变量都是实现了freemarker.template.TemplateModel 接口的Java对象。 但在数据模型中,可以使用基本的Java集合类作为变量,因为这些变量会在内部被替换为适当的 TemplateModel 类型。这种功能特性被称作是 对象包装。对象包装功能可以透明地把 任何 类型的对象转换为实现了TemplateModel 接口类型的实例。这就使得下面的转换成为可能,如在模板中把 java.sql.ResultSet 转换为序列变量, 把javax.servlet.ServletRequest 对象转换成包含请求属性的哈希表变量, 甚至可以遍历对象包装器 实现(可能是自定义的实现); 这将在后面讨论。 现在的要点是想从模板访问任何对象,它们早晚都要转换为实现了 TemplateModel 接口的对象。那么首先你应该熟悉来写 TemplateModel 接口的实现类。

有一个粗略的 freemarker.template.TemplateModel 子接口对应每种基本变量类型(TemplateHashModel 对应哈希表,TemplateSequenceModel 对应序列, TemplateNumberModel 对应数字等等)。比如,想为模板使用 java.sql.ResultSet 变量作为一个序列,那么就需要编写一个 TemplateSequenceModel 的实现类,这个类要能够读取 java.sql.ResultSet 中的内容。我们常这么说,使用 TemplateModel 的实现类 包装 了 java.sql.ResultSet,基本上只是封装 java.sql.ResultSet 来提供使用普通的TemplateSequenceModel 接口访问它。请注意一个类可以实现多个 TemplateModel 接口;这就是为什么FTL变量可以有多种类型 (参看模板开发指南/数值,类型/基本内容)

请注意,这些接口的一个细小的实现是和 freemarker.template 包一起提供的。 例如,将一个 String 转换成FTL的字符串变量, 可以使用 SimpleScalar,将 java.util.Map 转换成FTL的哈希表变量,可以使用 SimpleHash 等等。

如果想尝试自己的 TemplateModel 实现, 一个简单的方式是创建它的实例,然后将这个实例放入数据模型中 (也就是把它 放到 哈希表的根root上)。 对象包装器将会给模板提供它的原状,因为它已经实现了 TemplateModel 接口,所以没有转换(包装)的需要。 (这个技巧当你不想用对象包装器来包装(转换)某些对象时仍然有用。)

 

2.2 标量类型:

有4种类型的标量:

  • 布尔值
  • 数字
  • 字符串
  • 日期类型(子类型: 日期(没有时间部分),时间或者日期-时间)

每一种标量类型都是 TemplateTypeModel 接口的实现,这里的 Type 就是类型的名称。这些接口只定义了一个方法: typegetAsType();。 它返回变量的Java类型(boolean, Number, String 和 Date 各自代表的值)。

Note:

由于历史遗留的原因,字符串标量的接口是 TemplateScalarModel,而不是TemplateStringModel。 (因为早期的 FreeMarker 字符串就是标量。)

这些接口的一个细小的实现和 SimpleType 类名在 freemarker.template 包中是可用的。 但是却没有 SimpleBooleanModel 类型;为了代表布尔值, 可以使用 TemplateBooleanModel.TRUE 和 TemplateBooleanModel.FALSE 来单独使用。

Note:

由于历史遗留的原因,字符串标量的实现类是 SimpleScalar,而不是 SimpleString

在FTL中标量是一成不变的。当在模板中设置变量的值时, 使用其他的实例来替换 TemplateTypeModel 实例时, 是不用改变原来实例中存储的值的。

2.2.1 "日期" 类型的难点

对于日期类型来说,有一些难题,因为Java API通常不区别 java.util.Date 只存储日期部分(April 4, 2003), 时间部分(10:19:18 PM),或两者都存(April 4, 2003 10:19:18 PM)。 为了用本文正确显示值(或者进行其它确定的操作),FreeMarker必须知道 java.util.Date 的哪个部分存储了有意义上的信息, 哪部分没有被使用(通常是标记为0的)。不幸的是, 通常该信息只是当值从数据库中取得时可用, 因为大多数数据库有独立的日期,时间和日期-时间(又叫做时间戳)类型, java.sql 有3个对应的 java.util.Date 子类和它们相匹配。

TemplateDateModel 接口有两个方法:分别是 java.util.Date getAsDate() 和 int getDateType()。 该接口典型的实现是存储一个 java.util.Date 对象, 加上一个整数来辨别子类型。这个整数的值也必须是 TemplateDateModel 接口中的常量之一:DATETIME, DATETIME 和 UNKNOWN

关于 UNKNOWNjava.lang 和 java.util 下的类通常被自动转换成 TemplateModel 的实现类,就是所谓的 对象包装器ObjectWrapper(请参考之前的对象包装介绍)。 如果对象包装器要包装 java.util.Date 类, 它不是 java.sql 日期类的实例,那就不能决定子类型是什么, 所以使用 UNKNOWN。之后,如果模板需要使用这个变量, 而且操作也需要子类型,那就会停止执行并抛出错误。为了避免这种情况的发生, 对于那些可能有问题的变量,模板开发人员必须明确地指定子类型,使用内建函数 date, time 或 datetime (比如lastUpdated?datetime)。请注意, 如果和格式化参数一起使用内建函数 string, 比如foo?string("MM/dd/yyyy"),那么 FreeMarker 就不必知道子类型了。

 

2.3 容器类型:

容器包括哈希表,序列和集合三种类型。

2.3.1哈希表

哈希表是实现了 TemplateHashModel 接口的Java对象。TemplateHashModel 有两个方法: TemplateModel get(String key),这个方法根据给定的名称返回子变量, boolean isEmpty(),这个方法表明哈希表是否含有子变量。 get 方法当在给定的名称没有找到子变量时返回null。

TemplateHashModelEx 接口扩展了 TemplateHashModel。它增加了更多的方法,使得可以使用内建函数 values 和 keys 来枚举哈希表中的子变量。

经常使用的实现类是 SimpleHash,该类实现了 TemplateHashModelEx 接口。从内部来说,它使用一个 java.util.Hash 类型的对象存储子变量。 SimpleHash 类的方法可以添加和移除子变量。 这些方法应该用来在变量被创建之后直接初始化。

在FTL中,容器是一成不变的。那就是说你不能添加,替换和移除容器中的子变量。

2.3.2序列

序列是实现了 TemplateSequenceModel 接口的Java对象。它包含两个方法:TemplateModel get(int index) 和 int size()

经常使用的实现类是 SimpleSequence。该类内部使用一个 java.util.List 类型的对象存储它的子变量。 SimpleSequence 有添加子元素的方法。 在序列创建之后应该使用这些方法来填充序列。

2.3.3集合

集合是实现了 TemplateCollectionModel 接口的Java对象。这个接口定义了一个方法: TemplateModelIterator iterator()TemplateModelIterator 接口和 java.util.Iterator 相似,但是它返回 TemplateModels 而不是 Object, 而且它能抛出TemplateModelException 异常。

通常使用的实现类是 SimpleCollection

 

 

 

 

2.4方法

 

方法变量在存于实现了 TemplateMethodModel 接口的模板中。这个接口包含一个方法: TemplateModel exec(java.util.List arguments)。 当使用 方法调用表达式 调用方法时,exec 方法将会被调用。 形参将会包含FTL方法调用形参的值。exec 方法的返回值给出了FTL方法调用表达式的返回值。

TemplateMethodModelEx 接口扩展了 TemplateMethodModel 接口。它没有添加任何新方法。 事实上这个对象实现这个 标记 接口是给FTL引擎暗示, 形式参数应该直接以 TemplateModel 的形式放进 java.util.List。否则将会以 String 形式放入list。

出于这些很明显的原因,这些接口没有默认的实现。

例如:下面这个方法,返回第一个字符串在第二个字符串第一次出现时的索引位置, 如果第二个字符串中不包含第一个字符串,则返回-1:

public class IndexOfMethod implements TemplateMethodModel {

   

    public TemplateModel exec(List args) throws TemplateModelException {

        if (args.size() != 2) {

            throw new TemplateModelException("Wrong arguments");

        }

        return new SimpleNumber(

            ((String) args.get(1)).indexOf((String) args.get(0)));

    }

}

如果将一个实例放入根数据模型中,像这样:

root.put("indexOf", new IndexOfMethod());

那么就可以在模板中调用:

<#assign x = "something">

${indexOf("met", x)}

${indexOf("foo", x)}

将会输出:

2

-1

 

 

2.5 指令

 

Java程序员可以使用 TemplateDirectiveModel 接口在Java代码中实现自定义指令。详情可以参加API文档。

Note:

TemplateDirectiveModel 在 FreeMarker 2.3.11 版本时才加入, 来代替快被废弃的TemplateTransformModel

示例 1

我们要实现一个指令, 这个指令可以将在它开始标签和结束标签之内的字符都转换为大写形式。 就像这个模板:

foo
<@upper>
  bar
  <#-- All kind of FTL is allowed here -->
  <#list ["red", "green", "blue"] as color>
    ${color}
  </#list>
  baaz
</@upper>
wombat

将会输出:

foo
  BAR
    RED
    GREEN
    BLUE
  BAAZ
wombat

下面是指令类的源代码:

package com.example;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
 
import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
 
/**
 *  FreeMarker user-defined directive that progressively transforms
 *  the output of its nested content to upper-case.
 *  
 *  <p><b>Directive info</b></p>
 * 
 *  <p>Directive parameters: None
 *  <p>Loop variables: None
 *  <p>Directive nested content: Yes
 */
public class UpperDirective implements TemplateDirectiveModel {
    
    public void execute(Environment env,
            Map params, TemplateModel[] loopVars,
            TemplateDirectiveBody body)
            throws TemplateException, IOException {
        // Check if no parameters were given:
        if (!params.isEmpty()) {
            throw new TemplateModelException(
                    "This directive doesn't allow parameters.");
        }
        if (loopVars.length != 0) {
                throw new TemplateModelException(
                    "This directive doesn't allow loop variables.");
        }
        
        // If there is non-empty nested content:
        if (body != null) {
            // Executes the nested body. Same as <#nested> in FTL, except
            // that we use our own writer instead of the current output writer.
            body.render(new UpperCaseFilterWriter(env.getOut()));
        } else {
            throw new RuntimeException("missing body");
        }
    }
    
    /**
     * A {@link Writer} that transforms the character stream to upper case
     * and forwards it to another {@link Writer}.
     */ 
    private static class UpperCaseFilterWriter extends Writer {
       
        private final Writer out;
           
        UpperCaseFilterWriter (Writer out) {
            this.out = out;
        }
 
        public void write(char[] cbuf, int off, int len)
                throws IOException {
            char[] transformedCbuf = new char[len];
            for (int i = 0; i < len; i++) {
                transformedCbuf[i] = Character.toUpperCase(cbuf[i + off]);
            }
            out.write(transformedCbuf);
        }
 
        public void flush() throws IOException {
            out.flush();
        }
 
        public void close() throws IOException {
            out.close();
        }
    }
 
}

现在我们需要创建这个类的实例, 然后让这个指令在模板中可以通过名称"upper"来访问 (或者是其它我们想用的名字)。一个可行的方案是把这个指令放到数据模型中:

root.put("upper", new com.example.UpperDirective());

但更好的做法是将常用的指令作为 共享变量 放到 Configuration 中。

当然也可以使用 内建函数new 将指令放到一个FTL库(宏的集合,就像在模板中, 使用 include 或 import )中:

<#-- Maybe you have directives that you have implemented in FTL -->
<#macro something>
  ...
</#macro>
 
<#-- Now you can't use <#macro upper>, but instead you can: -->
<#assign upper = "com.example.UpperDirective"?new()>

 

 

 

 

2.6  对象包装

 

对象包装器是实现了 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需要撤回映射,此时 对象包装器ObjectWrapper 的 Object 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),当使用 SimpleHash 的 put 方法来填充(这点很重要,它不会复制已经填充并存在的 Map)。这会加快顶层数据模型变量的访问速度。

默认对象包装器

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

它会识别大部分基本的Java类型,比如 String, NumberBoolean, DateList (通常还有全部的 java.util.Collection 类型), 数组,Map等。并把它们自然地包装成匹配 TemplateModel 接口的对象。它也会使用 freemarker.ext.dom.NodeModel 来包装W3C DOM结点, 所以可以很方便地处理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:
      • 或者使用构造方法:
      • 或者使用 object_wrapper 属性 (*.properties 文件或 java.util.Properties 对象):
      • 或者通过 FreemarkerServlet 配置 object_wrapper 和在 web. 中的 init-param 属性来配置:
  • 在新的或测试覆盖良好的项目中,也建议设置 forceLegacyNonListCollections 属性为 false。 如果使用 .properties 或FreemarkerServlet 初始化参数,就会像 DefaultObjectWrapper(2.3.22, forceLegacyNonListCollections=false), 同时,使用Java API可以在 DefaultObjectWrapperBuilder 对象调用 build() 之前调用setForceLegacyNonListCollections(false)
  • 自定义 DefaultObjectWrapper 的最常用方法是覆盖 handleUnknownType 方法。
... = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_22).build()
... = new DefaultObjectWrapper(Configuration.VERSION_2_3_22)
object_wrapper=DefaultObjectWrapper(2.3.21)
§  <init-param>
§      <param-name>object_wrapper</param-name>
§      <param-value>DefaultObjectWrapper(2.3.21)</param-value>
</init-param>

自定义对象包装示例

我们假定有一个应用程序特定的类,像下面这样:

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)

 

 

3、配置

 

3.1 共享变量

 

Shared variables (共享变量)是为所有模板定义的变量。可以使用 setSharedVariable 方法向配置中添加共享变量:

Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);

...

cfg.setSharedVariable("warp", new WarpDirective());

cfg.setSharedVariable("company", "Foo Inc.");

在所有使用这个配置的模板中,名为 wrap 的用户自定义指令和一个名为 company 的字符串将会在数据模型的根root上可见, 那就不用在根哈希表上一次又一次的添加它们。在传递给 Template.process 的 根root对象里的变量将会隐藏同名的共享变量。

Warning!

如果配置对象在多线程环境中使用,不要使用 TemplateModel 实现类来作为共享变量, 因为它是不是线程安全的! 这也是基于Servlet应用程序的典型情形。

出于向后兼容的特性,共享变量的集合初始化时 (就是对于新的 Configuration 实例来说)不能为空。 它包含下列用户自定义指令(用户自定义指令使用时需要用 @ 来代替#):

名称

capture_output

freemarker.template.utility.CaptureOutput

compress

freemarker.template.utility.StandardCompress

html_escape

freemarker.template.utility.HtmlEscape

normalize_newlines

freemarker.template.utility.NormalizeNewlines

freemarker.template.utility.

 

 

 

 

3.2 配置设置

Settings(配置设置) 是影响FreeMarker行为的已被命名的值。配置设置有很多, 例如:locale,number_format,default_encoding, template_exception_handler。可以参考 Configuration.setSetting(...)的Java API 文档 来查看配置设置的完整列表。

配置设置存储在 Configuration 实例中,可以在 Template 实例中被覆盖。比如,在配置中给 locale 设置为 "en_US", 那么使用该配置的所有模板中的 locale 都使用 "en_US", 除非在模板中locale被明确地设置成其它不同的值(参见 localization)。 因此,在Configuration 中的值充当默认值, 这些值在每个模板中也可以被覆盖。在 Configuration 或 Template 实例中的值也可以在单独调用Template.process 方法后被覆盖。 对于每个调用了 freemarker.core.Environment 对象的值在内部创建时就持有模板执行的运行时环境,也包括了那个级别被覆盖了的设置信息。 在模板执行时,那里存储的值也可以被改变,所以模板本身也可以设置配置信息, 比如在输出中途来变换 locale 设置。

配置信息可以被想象成3层(Configuration, Template,Environment), 最高层包含特定的值,它为设置信息提供最有效的值。 比如(设置信息A到F仅仅是为这个示例而构想的):

 

Setting A

Setting B

Setting C

Setting D

Setting E

Setting F

Layer 3: Environment

1

-

-

1

-

-

Layer 2: Template

2

2

-

-

2

-

Layer 1: Configuration

3

3

3

3

-

-

配置信息的有效值为:A=1,B=2,C=3,D=1,E=2。 而F的设置则是 null,或者在你获取它的时候将抛出异常。

我们看看如何准确设置配置信息:

  • Configuration 层: 原则上设置配置信息时使用 Configuration 对象的setter方法,例如:
  • Configuration myCfg = new Configuration(Configuration.VERSION_2_3_23);
  • myCfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);

myCfg.setDefaultEncoding("UTF-8");

在真正使用 Configuration 对象 (通常在初始化应用程序时)之前来配置它,后面必须将其视为只读的对象。

在实践中,比如很多Web应用框架中,就应该使用这种框架特定的配置方式来进行配置, 比如使用成对的 String 来配置(像在.properties 属性配置文件中那样)。 在这种情况下,框架的作者大多数使用 Configuration 对象的 setSetting(String name, String value) 方法。 这可以参考setSetting的API文档 部分来获取可用的设置名和参数的格式的信息。 而在Spring框架中,我们可以这样进行:

<bean id="freemarkerConfig"

    >

  <property name="freemarkerSettings">

    <props>

      <prop key="incompatible_improvements">2.3.23</prop>

      <prop key="template_exception_handler">rethrow</prop>

      <prop key="default_encoding">UTF-8</prop>

    </props>

  </property>

</bean>

请注意,这种形式的配置( String 键-值对) 和直接使用 Configuration 的API相比, 很不幸地被限制了。

  • Template 层:对于被请求的本地化信息,模板的 locale 设置由 Configuration.getTemplate(...) 来设置。 否则,就不能在这里进行设置,除非想控制 Template 对象来代替 freemarker.cache.TemplateCache,这样的话, 应该在 Template 对象第一次被使用前就设置配置信息, 然后就将 Template 对象视为是只读的。
  • Environment 层:这里有两种配置方法:
    • 使用Java API:使用 Environment 对象的setter方法。当然想要在模板执行之前来做,然后当调用myTemplate.process(...) 时会遇到问题, 因为在内部创建 Environment 对象后立即就执行模板了, 导致没有机会来进行设置。这个问题的解决可以用下面两个步骤进行:
  • Environment env = myTemplate.createProcessingEnvironment(root, out);
  • env.setLocale(java.util.Locale.ITALY);
  • env.setNumberFormat("0.####");

env.process();  // process the template

    • 在模板中(通常这被认为是不好的做法)直接使用 setting 指令,例如:
  • <#setting locale="it_IT">

<#setting number_format="0.####">

  • 在这层,当什么时候改变配置信息,是没有限制的。

要知道 FreeMarker 支持什么样的配置信息还有它们的意义, 可以先看看FreeMarker Java API文档中的下面这部分内容:

  • 在三层中 freemarker.core.Configurable 的setter方法来配置。
  • 只在 Configuration 层可用的 freemarker.template.Configuration 的setter方法来配置。
  • 在三层中可用 String 键-值对书写的 freemarker.core.Configurable.setSetting(String,String) 配置。
  • 只在 Configuration 层中可用 String 键-值对书写的 freemarker.template.Configuration.setSetting(String, String) 配置。

 

3.3 模板加载

 

模板加载器

模板加载器是加载基于抽象模板路径下,比如 "index.ftl" 或 "products/catalog.ftl" 的原生文本数据对象。 这由具体的模板加载器对象来确定它们取得请求数据时使用了什么样的数据来源 (文件夹中的文件,数据等等)。当调用 cfg.getTemplate (这里的 cfg 就是Configuration 实例)时, FreeMarker询问模板加载器是否已经为 cfg 建立返回给定模板路径的文本,之后 FreeMarker 解析文本生成模板。

内建模板加载器

在 Configuration 中可以使用下面的方法来方便建立三种模板加载。 (每种方法都会在其内部新建一个模板加载器对象,然后创建Configuration 实例来使用它。)

void setDirectoryForTemplateLoading(File dir);

void setClassForTemplateLoading(Class cl, String prefix);

void setServletContextForTemplateLoading(Object servletContext, String path);

上述的第一种方法在磁盘的文件系统上设置了一个明确的目录, 它确定了从哪里加载模板。不要说可能,File 参数肯定是一个存在的目录。否则,将会抛出异常。

第二种调用方法使用了一个 Class 类型的参数和一个前缀。这是让你来指定什么时候通过相同的机制来加载模板, 不过是用Java的ClassLoader 来加载类。 这就意味着传入的class参数会被 Class.getResource() 用来调用方法来找到模板。参数 prefix 是给模板的名称来加前缀的。在实际运行的环境中, 类加载机制是首选用来加载模板的方法,通常情况下,从类路径下加载文件的这种机制, 要比从文件系统的特定目录位置加载安全而且简单。在最终的应用程序中, 所有代码都使用 .jar 文件打包也是不错的, 这样用户就可以直接执行包含所有资源的 .jar 文件了。

第三种调用方式需要Web应用的上下文和一个基路径作为参数, 这个基路径是Web应用根路径(WEB-INF目录的上级目录)的相对路径。 那么加载器将会从Web应用目录开始加载模板。尽管加载方法对没有打包的 .war 文件起作用,因为它使用了ServletContext.getResource() 方法来访问模板, 注意这里我们指的是“目录”。如果忽略了第二个参数(或使用了""), 那么就可以混合存储静态文件(.html.jpg等) 和 .ftl 文件,只是 .ftl 文件可以被送到客户端执行。 当然必须在 WEB-INF/web. 中配置一个Servlet来处理URI格式为 *.ftl 的用户请求,否则客户端无法获取到模板, 因此你将会看到Web服务器给出的秘密提示内容。在站点中不能使用空路径,这是一个问题, 你应该在 WEB-INF 目录下的某个位置存储模板文件, 这样模板源文件就不会偶然地被执行到,这种机制对servlet应用程序来加载模板来说, 是非常好用的方式,而且模板可以自动更新而不需重启Web应用程序, 但是对于类加载机制,这样就行不通了。

从多个位置加载模板

如果需要从多个位置加载模板,那就不得不为每个位置都实例化模板加载器对象, 将它们包装到一个称为 MultiTemplateLoader 的特殊模板加载器, 最终将这个加载器传递给 Configuration 对象的 setTemplateLoader(TemplateLoader loader)方法。 下面给出一个使用类加载器从两个不同位置加载模板的示例:

import freemarker.cache.*; // template loaders live in this package
 
...
 
FileTemplateLoader ftl1 = new FileTemplateLoader(new File("/tmp/templates"));
FileTemplateLoader ftl2 = new FileTemplateLoader(new File("/usr/data/templates"));
ClassTemplateLoader ctl = new ClassTemplateLoader(getClass(), "");
TemplateLoader[] loaders = new TemplateLoader[] { ftl1, ftl2, ctl };
MultiTemplateLoader mtl = new MultiTemplateLoader(loaders);
 
cfg.setTemplateLoader(mtl);

现在,FreeMarker将会尝试从 /tmp/templates 目录加载模板,如果在这个目录下没有发现请求的模板,它就会继续尝试从/usr/data/templates 目录下加载,如果还是没有发现请求的模板, 那么它就会使用类加载器来加载模板。

从其他资源加载模板

如果内建的类加载器都不适合使用,那么就需要来编写自己的类加载器了, 这个类需要实现 freemarker.cache.TemplateLoader 接口, 然后将它传递给 Configuration 对象的 setTemplateLoader(TemplateLoader loader)方法。 可以阅读API JavaDoc文档获取更多信息。

如果模板需要通过URL访问其他模板,那么就不需要实现 TemplateLoader 接口了,可以选择子接口freemarker.cache.URLTemplateLoader 来替代, 只需实现 URL getURL(String templateName) 方法即可。

模板名称(模板路径)

解析模板的名称(也就是模板路径)是由模板解析器来决定的。 但是要和其它对路径的格式要求很严格的组件一起使用。通常来说, 强烈建议模板加载器使用URL风格的路径。 在URL路径(或在UN*X路径)中符号有其它含义时,那么路径中不要使用 /(路径分隔符)字符,. (同目录符号)和..(父目录符号)。字符 *(星号)是被保留的, 它用于FreeMarker的 "模板获取" 特性。

://(或者使用 template_name_format 配置设置到 DEFAULT_2_4_0: (冒号) 字符)是被保留用来指定体系部分的,和URI中的相似。比如 someModule://foo/bar.ftl 使用 someModule,或者假定 DEFAULT_2_4_0 格式,classpath:foo/bar.ftl 使用 classpath 体系。解释体系部分完全由 TemplateLoader 决定。 (FreeMarker核心仅仅知道体系的想法,否则它不能正常处理相对模板名称。)

FreeMarker通常在将路径传递到 TemplateLoader 之前把它们正常化,所以路径中不会包含 /../ 这样的内容, 路径会相对于虚构的模板根路径(也就是它们不会以 / 开头)。 其中也不会包含 *,因为模板获取发生在很早的阶段。 此外,将 template_name_format 设置为DEFAULT_2_4_0,多个连续的 / 将会被处理成单独的 / (除非它们是 :// 模式分隔符的一部分)。

请注意,不管主机运行的操作系统是什么, FreeMarker 模板加载时经常使用斜线(而不是反斜线)。

模板缓存

FreeMarker 是会缓存模板的(假设使用 Configuration 对象的方法来创建 Template 对象)。这就是说当调用 getTemplate方法时,FreeMarker不但返回了 Template 对象,而且还会将它存储在缓存中, 当下一次再以相同(或相等)路径调用 getTemplate 方法时, 那么它只返回缓存的 Template 实例, 而不会再次加载和解析模板文件了。

如果更改了模板文件,当下次调用模板时,FreeMarker 将会自动重新载入和解析模板。 然而,要检查模板文件是否改变内容了是需要时间的,有一个 Configuration 级别的设置被称作"更新延迟",它可以用来配置这个时间。 这个时间就是从上次对某个模板检查更新后,FreeMarker再次检查模板所要间隔的时间。 其默认值是5秒。如果想要看到模板立即更新的效果,那么就要把它设置为0。 要注意某些模板加载器也许在模板更新时可能会有问题。 例如,典型的例子就是在基于类加载器的模板加载器就不会注意到模板文件内容的改变。

当调用了 getTemplate 方法时, 与此同时FreeMarker意识到这个模板文件已经被移除了,所以这个模板也会从缓存中移除。 如果Java虚拟机认为会有内存溢出时,默认情况它会从缓存中移除任意模板。 此外,你还可以使用 Configuration 对象的 clearTemplateCache 方法手动清空缓存。

何时将一个被缓存了的模板清除的实际应用策略是由配置的属性 cache_storage 来确定的,通过这个属性可以配置任何 CacheStorage 的实现。对于大多数用户来说, 使用 freemarker.cache.MruCacheStorage 就足够了。 这个缓存存储实现了二级最近使用的缓存。在第一级缓存中, 组件都被强烈引用到特定的最大数目(引用次数最多的组件不会被Java虚拟机抛弃, 而引用次数很少的组件则相反)。当超过最大数量时, 最近最少使用的组件将被送至二级缓存中,在那里它们被很少引用, 直到达到另一个最大的数目。引用强度的大小可以由构造方法来指定。 例如,设置强烈部分为20,轻微部分为250:

cfg.setCacheStorage(new freemarker.cache.MruCacheStorage(20, 250))

或者,使用 MruCacheStorage 缓存, 它是默认的缓存存储实现:

cfg.setSetting(Configuration.CACHE_STORAGE_KEY, "strong:20, soft:250");

当创建了一个新的 Configuration 对象时, 它使用一个 strongSizeLimit 值为0的 MruCacheStorage 缓存来初始化,softSizeLimit 的值是 Integer.MAX_VALUE (也就是在实际中,是无限大的)。但是使用非0的 strongSizeLimit 对于高负载的服务器来说也许是一个更好的策略,对于少量引用的组件来说, 如果资源消耗已经很高的话,Java虚拟机往往会引发更高的资源消耗, 因为它不断从缓存中抛出经常使用的模板,这些模板还不得不再次加载和解析。

 

 

 

中文手册下载:

https://sourceforge.net/projects/freemarker/files/chinese-manual/