1. JSP 和 FreeMarker ?
  2. 为什么 FreeMarker 对 null 和不存在的变量很敏感,如何来处理它?
  3. 为什么 FreeMarker 打印奇怪的数字格式 (比如 1,000,000 或 1 000 000 而不是 1000000)?
  4. 为什么 FreeMarker 会不好打印的小数/分组分隔符号 (比如3.14而不是3,14) ?
  5. 为什么当我想用如 ${aBoolean} 格式打印布尔值时, FreeMarker 会抛出错误,又如何来修正呢 ?
  6. FreeMarker 没有找到我的模板 (TemplateNotFoundExceptionFileNotFoundException, "Template not found" 错误消息)
  7. 文档中编写了关于特性 X, 但是好像 FreeMarker 并不知道它,或者行为和文档中的不同, 或者据称已经修复的bug仍然存在。
  8. FreeMarker标签中的 <> 混淆了编辑器或XML处理器,应该怎么做 ?
  9. 什么是合法的变量名 ?
  10. 如何使用包含负号(-),冒号 (:),点(.),或其它特殊字符的 变量名(宏名,参数名) ?
  11. 为什么当我尝试使用 X JSP 自定义标签时, 得到了 "java.lang.IllegalArgumentException: argument type mismatch" ?
  12. 如何像 jsp:include 一样的方式引入其它的资源 ?
  13. 如何给普通Java-method/TemplateMethodModelEx/TemplateTransformModel/TemplateDirectiveModel 的实现传递普通 java.lang.*/java.util.* 对象 ?
  14. 为什么在 myMap[myKey] 表达式中不能使用非字符串的键?那现在应该怎么做 ?
  15. 当使用 ?keys/?values 遍历Map(哈希表)的内容时,得到了混合真正map条目的 java.util.Map 的方法。当然,只是想获取map的条目。
  16. 在 FreeMarker 模板中如何修改序列(list)和哈希表(maps) ?
  17. 关于 null 在 FreeMarker 模板语言是什么样的?
  18. 我该怎么在表达式(作为另外一个指令参数)中使用指令(宏)的输出 ?
  19. 在输出中为什么用"?"来代替字符 X
  20. 在模板执行完成后,怎么在模板中获取计算过的值 ?
  21. How to assign to (or #import into) a dynamically constructed variable name (like to name that's stored in another variable)?
  22. Can I allow users to upload templates and what are the security implications?
  23. How to implement a function or macro in Java Language instead of in the template language?
  24. In my Servlet based application, how do I show a nice error page instead of a stack trace when error occurs during template processing?
  25. I'm using a visual HTML editor that mangles template tags. Will you change the template language syntax to accommodate my editor?
1.  JSP 和 FreeMarker ?

我们比较 FreeMarker 和 JSP 2.0 + JSTL 的组合。

FreeMarker 的优点:

  • FreeMarker 不绑定Servlet,网络/Web环境;它仅仅是通过合并模板和Java对象 (数据模型)来生成文本输出的类库。你可以在任意地方任意时间来执行模板; 不需要HTTP的请求转发或类似的手段,也不需要Servlet环境。 出于这些特点你可以轻松的将它整合到任何系统中去。

  • 更简洁的语法。看下这个JSP(假设 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>):

    <c:if test="${t}">
      True
    </c:if>
    
    <c:choose>
      <c:when test="${n == 123}">
          Do this
      </c:when>
      <c:otherwise>
          Do that
      </c:otherwise>
    </c:choose>
    
    <c:forEach var="i" items="${ls}">
    - ${i}
    </c:forEach>

    相等的 FTL:

    <#if t>
      True
    </#if>
    
    <#if n == 123>
      Do this
    <#else>
      Do that
    </#if>
    
    <#list ls as i>
    - ${i}
    </#list>
  • 在模板中没有servlet特定的范围和其它高级技术 (除非,当然,你可以故意地将它们放入数据模型中)。 一开始就是为MVC设计的,它仅仅专注于展示。

  • 可以从任意位置加载模板;从类路径下,从数据库中等。

  • 默认情况下,数字和日期格式是本地化敏感的。 因为我们对用户输出,你所做的仅仅是书写 ${x}, 而不是 <fmt:formatNumber value="${x}" />

  • 易于定义特设的宏和函数。

  • 隐藏错误并假装它不存在。丢失的变量和 null 也不会默认视为 0/false/空字符串, 但会引发错误。在这里参考更多内容...

  • "对象包装"。允许你在模板中以自定义,面向表现的方式来展示对象。 (比如:参看这里, 来看看使用这种技术时W3C的DOM结点是如何通过模板展现出来的。)

  • 宏和函数仅仅是变量,所以它们可以很容易的作为参数值来传递, 放置到数据模型中等,就像其它任意值。

  • 第一次访问一个页面时几乎察觉不到的延迟 (或在它改变之后),因为没有更高级的编译发生。

FreeMarker 的缺点:

  • 不是一种标准。很少的工具和IDE来集成它,少数的开发者知道它, 很少的工业化的支持。(然而,使用合适的设置, 大部分JSP标签库可以在 FreeMarker 模板中运行,除非它们基于 .tag 文件。)

  • 除了一些视觉上的相似性,它的语法不同于HTML/XML语法规则, 这会使得新用户感到混乱(这就是简洁的价值所在)。JSP也不遵循它, 只是接近。

  • 因为宏和函数仅仅是变量,不正确的指令, 参数名和丢失的必须变量仅仅在运行时会被检测到。

  • 不能和JSF一起使用。(这在技术上可行,但是没有人来实现它)

如果你认为可以用 FreeMarker 在应用程序或遗留的仅支持JSP的框架中来代替JSP, 你可以阅读这部分内容:程序开发指南/其它/在Servlet中使用FreeMarker/在"Model 2"中使用FreeMarker

2.  为什么 FreeMarker 对 null 和不存在的变量很敏感,如何来处理它?

概括一下这点是关于什么的:默认情况下,FreeMarker 将试图访问一个不存在的变量或 null 值 (这两点是一样的)视为一个错误, 这会中断模板的执行。

首先,你应该理解敏感的原因。很多脚本语言和模板语言都能容忍不存在的变量 (还有 null),通常它们将这些变量视为空字符串和/或0, 还有逻辑false。这些行为主要有以下几点问题:

  • 它潜在隐藏了一些可能偶然发生的错误,就像变量名中的一个错字, 或者当模板编写者引用程序员没有放到数据模型中的变量时, 或程序员使用了一个不同的名字。人们是容易犯下这种偶然的错误的, 而计算机则不会,所以失去这些机会,模板引擎可以显示这些错误则是一个很糟糕的运行方式。 尽管你很小心地检查了开发期间模板输出的内容,那也很容易就忽略了如 <#if hasWarnigs>print warnings here...</#if> 这样的错误,可能永远不会打印警告信息, 因为你已经搞乱了变量名(注意到了吗?)。也考虑一下后期的维护,当你以后修改你的应用时, 你可能不会每次都重新来仔细检查模板(很多应用程序都有数以百计的模板)。 单元测试也不会很好的覆盖到web页面内容(如果设置了它们...); 它们最多只会检查web页面中的某些手动设置模式,所以它们通常会光滑地通过, 但那些修改实际是有bug的。如果页面执行失败,那么测试人员将会注意, 单元测试也会注意(比如整个页面失败),在生产环境中,维护人员会注意 (假设一些人员检查错误日志)。

  • 做了危险的假设。脚本语言或模板引擎对应用程序领域是一无所知的, 所以当它决定一些它不知道是 0/false 值时,这是一个相当不负责而且很武断的事情。 仅仅因为它不知道你当前银行账户的余额,我们能说是0么? 仅仅因为不知道一个病人是否对青霉素过敏,我们就能说他/她不对青霉素过敏? 想一想这样错误的暗示信息。展示一个错误提示页面通常要比直接显示错误信息好很多, 导致用户端的错误决定。

这种情况下(不面对这种问题),不对其敏感那就是隐藏错误假装它不存在了, 这样很多用户会觉得方便,但是我们仍然相信在大多数情况下严格对待这个问题, 从长远考虑会节省你更多时间并提高软件的质量。

另外一方面,我们意识到你有更好的原因在有些地方不想 FreeMarker 对错误敏感, 那么对于这种情况的解决方案如下:

  • 通常数据模型中含有 null 或可选变量。 这种情况下使用 这些操作符。 如果你使用它们很频繁的话,那么就要重新考虑一下你的数据模型了, 因为过分依赖它们并不是那些难以使用的详细模板的结果, 但是会增加隐藏错误和打印任意不正确输出(原因前面已经说过了)的可能性。

  • 在一些应用程序中,你也许想显示不完整/损坏的页面,而不是错误页面。 这种情况下,你可以 使用另一种错误控制器 而不是默认的。自定义的错误控制器可以略过有问题的部分,或者在那儿显示错误指示, 而不会中止整个页面的呈现。请注意,尽管错误控制器没有给出变量任意的默认值, 显示危急信息的页面也可能会好过显示错误页面。

  • 如果页面包含的信息不是至关重要的(比如一些侧栏), 另外一个你可能感兴趣的特性是 attempt/recover 指令

3.  为什么 FreeMarker 打印奇怪的数字格式 (比如 1,000,000 或 1 000 000 而不是 1000000)?

FreeMarker 使用Java平台的本地化敏感的数字格式信息。 默认的本地化数字格式可能是分组或其他不想要的格式。 为了避免这种情况,你不得不使用 FreeMarker 设置 中的 number_format 来重写Java平台建议的数字格式,比如:

cfg.setNumberFormat("0.######");  // now it will print 1000000
// where cfg is a freemarker.template.Configuration object

请注意,人们通常在没有分组分隔符时阅读大数是有些困难的。 所以通常建议保留分隔符,而在对"计算机"处理时的情况(分组分隔符会使其混乱), 就要使用 c 内建函数 了。比如:

<a href="/shop/productdetails?id=${product.id?c}">Details...</a>

对于计算机,你需要 ?c,而根据本地化设置, 小数分隔符还是要担心。

4.  为什么 FreeMarker 会不好打印的小数/分组分隔符号 (比如3.14而不是3,14) ?

不同的国家使用不同的小数/分组分隔符号。 如果你看到不正确的符号,那么可能你的本地化设置不太合适。 设置Java虚拟机的默认本地化或使用 FreeMarker 设置 中的 locale 来重写默认本地化。比如:

cfg.setLocale(java.util.Locale.ITALY);
// where cfg is a freemarker.template.Configuration object

然而有时你想输出一个数字,这个数字不是对用户的, 而是对"计算机"的(比如你想在CSS中打印大小), 不管页面的本地化(语言)是怎么样的,这种情况你必须使用点来作为小数分隔符。 这样就可以使用 c 内建函数,比如:

font-size: ${fontSize?c}pt;
5.  为什么当我想用如 ${aBoolean} 格式打印布尔值时, FreeMarker 会抛出错误,又如何来修正呢 ?

不像是数字,布尔值没有通用的可接受的格式, 在相同页面中也没有一个通用的格式。就像当你在HTML页面中展示一件产品是可洗的, 你可能不会想给访问者看"Washable:true",而是"Washable:yes"。 所以我们强制模板作者(由 ${washable} 引起错误)去探索用户的感知, 在确定的地方布尔值应该来显示成什么。通常我们格式化布尔值的做法是: ${washable?string("yes", "no")}${caching?string("Enabled", "Disabled")}${heating?string("on", "off")}等。

但有两种情形这里是无用的:

  • 当打印布尔值来生成计算机语言输出,那么就想要 true/false,使用 ${someBoolean?c}。 (这至少需要 FreeMarker 2.3.20 版本。在那之前,通用的做法是编写 ${someBoolean?string}, 但这是很危险的,因为它的输出基于当前的布尔值格式设置,默认的是 "true"/"false"。)

  • 当对很多布尔值进行同一方式的格式化时。这种情形可以设置 boolean_format 设置项 (Configuration.setBooleanFormat) 来影响, 从 FreeMarker 2.3.20 版本开始,你可以仅仅编写 ${someBoolean}。 (请注意,这对 true/false 无效 - 你还必须在那儿使用 ?c。)

6.  FreeMarker 没有找到我的模板 (TemplateNotFoundExceptionFileNotFoundException, "Template not found" 错误消息)

首先,你应该知道 FreeMarker 是不从文件系统路径直接加载模板的。 它使用一个简单虚拟的文件系统可以读取非文件系统资源 (在jar内部的模板,从数据库表中读取模板等...)。虚拟文件由配置设置项来决定, Configuration.setTemplateLoader(TemplateLoader)。 即便你使用的 TemplateLoader 映射到了文件系统, 它会有一个包含所有模板的基路径,那就是你不能伸到的虚拟文件系统的根 (也就是说,绝对路径仍然是相对于虚拟文件系统的)。

解决问题的小窍门:

  • 如果你是配置 FreeMarker 的人,请确认你设置了合适的 TemplateLoader

  • 否则,请看未找到模板的错误消息是否包含所使用的 TemplateLoader 的描述。如果没有, 那么你使用的是老版本的 FreeMarker,那么请更新版本。得到 FileNotFoundException 而不是 TemplateNotFoundException 也是版本太老, 所以你不会得到更多的错误消息。(如果 TemplateLoader 在错误消息中是形如 foo.SomeTemplateLoader@64f6106c 这样的内容, 而没有显示相关的参数,你可以请作者定义一个更好的 toString()。)

  • 常犯的错误是对基于Servlet的web应用程序使用了 FileTemplateLoader 而不是 WebappTemplateLoader。 它会在一种环境中可用, 但是不会作用于在另一种,因为Servlet规范没有承诺资源可以作为普通文本来访问, 甚至当 war 文件被提取时。

  • 要知道当你从其它模板中包含/引入模板时,如果没有以 / 来开始模板名称,那么它就会被解释为相对于包含模板的路径。 错误消息会包含全(分解后的)名,所以应该注意这里。

  • 检查你是否正在使用 \ (反斜杠) 来代替 / (斜杠)。 (FreeMarker 2.3.22 和之后的版本会在错误消息中警告这点。)

  • 作为最后的补救办法,对 freemarker.cache 类别开启debug级别的日志(在你使用的日志框架中),然后来看还会有什么。

7.  文档中编写了关于特性 X, 但是好像 FreeMarker 并不知道它,或者行为和文档中的不同, 或者据称已经修复的bug仍然存在。

你确定你正在使用的文档和正在使用的 FreeMarker 版本号相同? 特别要注意,在线文档是对最新稳定的FreeMarker发布版。你可能使用的是老版本; 请更新它。

你确定Java类加载器发现了你期望使用的相同版本的 freemarker.jar?也许 freemarker.jar 是老版本的。要检查这点,尝试使用 ${.version} 在模板中打印版本号。(如果以"Unknown built-in variable: version" 错误消息结束, 那么你使用的是相当相当老的版本。)

如果你怀疑该问题是有多个 freemarker.jar, 典型的罪魁祸首就是某些模块有Maven或Ivy依赖使用了老的 freemarker group ID, 而不是更为现代的 org.freemarker group ID。因为不同的group ID, 不会被Maven或Ivy视为构件冲突,而是把两个版本都引入。这种情况下, 不得不去掉 freemarker 依赖。

如果你认为文档或 FreeMarker 有错误,请在bug跟踪器或邮件列表中中报告。 谢谢你!

8.  FreeMarker标签中的 <> 混淆了编辑器或XML处理器,应该怎么做 ?

从 FreeMarker 2.3.4 版本开始,你可以使用 [] 来代替 <>。要获取更多细节, 阅读这里...

关于在变量名中使用的字符和变量名的长度,FreeMarker 没有限制, 但是为了你的方便,在选择变量名时最好是简单变量引用表达式(参见 这里)。 如果你不得不选择一个非常极端的变量名,那也不是一个问题:参加这里

10.  如何使用包含负号(-),冒号 (:),点(.),或其它特殊字符的 变量名(宏名,参数名) ?

如果你的变量名很奇怪,比如 "foo-bar", 当你编写如 ${foo-bar} 的形式时, FreeMarker 会曲解你想要的东西。在这种确定的情况下, 它会相信你想从 foo 中减去 bar 的值, 这个FAQ例子解释了如何控制这样的情况。

首先,应该清理这些句法问题。关于变量名中使用的字符和变量名的长度, FreeMarker没有限制。

如果特殊字符是负号 (-, UCS 0x2D) 或点 (., UCS 0x2E) 或冒号 (:, UCS 0x3A)中的一种, 那么你所要做的就是在这些字符前面放置反斜杠(\), 比如在 foo\-bar (从 FreeMarker 2.3.22 版本开始)。 之后 FreeMarker 就会知道你不是想要相同符号的操作符。 在你指定未被引号表示的标识符时,这都起作用,比如对宏和函数名称, 参数名称,所有种类的变量引用。(请注意,这些转义仅在标识符中起作用, 而不是在字符串中。)

当特殊字符不是负号,点,或冒号中的一种时,那就很微妙了。 我们来看看有问题的变量,名称为 "a+b"。那么:

  • 如果你像读取变量:如果它是子变量或其它,可以编写 something["a+b"] (请记住, something.xsomething["x"]) 是相等的。如果它是顶级变量, 它们可以通过特殊哈希变量来访问,.vars, 所以你可以编写 .vars["a+b"]。很自然地, 这个技巧对宏和函数调用有有效: <@.vars["a+b"]/>.vars["a+b"](1, 2)

  • 如果你想创建或修改变量:所有允许你来创建或修改变量的指令 (比如 assignlocalglobalmacrofunction,等等)允许对目的变量名的引用。 比如, <#assign foo = 1><#assign "foo" = 1> 是相同的。所以你可以编写如 <#assign "a+b" = 1><#macro "a+b">

  • 不幸的是,你不能使用这样的变量名(包含不是 -.: 的特殊字符)来作为宏参数名。

11.  为什么当我尝试使用 X JSP 自定义标签时, 得到了 "java.lang.IllegalArgumentException: argument type mismatch" ?

首先,请更新 FreeMarker,因为 2.3.22 和之后的版本给出了更多有用的错误消息, 会给出该问题更好的答案。不管怎样,原因如下。在JSP页面,你对所有参数(属性)值使用引号, 如果参数的类型是字符串或布尔值或数字,它不会起作用。 但是因为自定义标签在FTL模板中是作为普通用户自定义FTL指令来访问的, 你不得不在自定义标签内使用FTL语法规则,而不是JSP规则。因此,根据FTL规则, 必须不能对布尔值和数字参数值使用引号,否则它们会被解释成字符串值, 当 FreeMarker 尝试传递这些值给自定义标签,而它们需要非字符串值时, 这会引起类型不匹配错误。

比如,Struts Tiles的 insert 标签参数 flush 是布尔值。在JSP中,正确的语法是:

<tiles:insert page="/layout.jsp" flush="true"/>
...

但是在FTL中,你应该编写:

<@tiles.insert page="/layout.ftl" flush=true/>
...

而且,出于相似的原因,这是错误的:

<tiles:insert page="/layout.jsp" flush="${needFlushing}"/>
...

你应该编写:

<tiles:insert page="/layout.jsp" flush=needFlushing/>
...

(不是 flush=${needFlushing}!)

12.  如何像 jsp:include 一样的方式引入其它的资源 ?

不是使用 <#include ...>, 那仅仅是包含另外一个 FreeMarker 模板而不涉及Servlet容器。

因为你要的包含方法是和Servlet相关的, 而纯 FreeMarker 是不知道Servlet和HTTP的存在, 那是Web应用框架来决定你是否可以这样做和如何来做。 比如,在Struts2中,你可以这么来做:

<@s.include value="/WEB-INF/just-an-example.jspf" />

如果Web应用框架对 FreeMarker 的支持是基于 freemarker.ext.servlet.FreemarkerServlet 的, 那么你可以这样来做(从 FreeMarker 2.3.15 版本之后):

<@include_page path="/WEB-INF/just-an-example.jspf" />

但是如果Web应用框架提供它自己的解决方案, 那么你就可以参考,毕竟它可能会做一些特殊的处理。

更多关于 include_page 的信息,可以 阅读这里...

13.  如何给普通Java-method/TemplateMethodModelEx/TemplateTransformModel/TemplateDirectiveModel 的实现传递普通 java.lang.*/java.util.* 对象 ?

不幸的是,对于这个问题没有简单的通用解决方案。 问题在于 FreeMarker 的对象包装是很灵活的,当从模板中访问变量时是很棒的, 但是会使得在Java端解包时变成一个棘手的问题。比如, 很可能将一个非 java.util.Map 对象包装称为 TemplateHashModel(FTL哈希表变量)。 但是它就不能被解包成 java.util.Map, 因为没有包装过的 java.util.Map

所以该怎么做呢?基本上有下面两种情况:

  • 对于展示目的(比如一种"工具"用来帮助 FreeMarker 模板) 指令和方法应该声明它们的形式参数为 TemplateModel 类型和它的更确切的子接口类型。毕竟,对象包装是对于表面转换数据模型, 并服务于展示层的,而这些方法是展示层的一部分。如果你仍然需要普通Java类型, 那么可以你可以转向当前 ObjectWrapperObjectWrapperAndUnwrapper 接口 (可以使用 Environment.getObjectWrapper())。

  • 和展示任务(比如,对于业务逻辑层)不相关的方法应该被实现成普通的Java方法, 而且不能使用任何 FreeMarker 特定的类,因为根据MVC范例, 它们必须独立于展示技术(FreeMarker)。如果这样的方法是从模板中调用的,那么 对象包装 的责任就是要保证参数转换到合适的类型。如果你使用了 DefaultObjectWrapperBeansWrapper, 那么这就会自动发生。对于 DefaultObjectWrapper,如果你 设置它的 incompatibleImprovements 为 2.3.22, 该机制运行得更好。

14.  为什么在 myMap[myKey] 表达式中不能使用非字符串的键?那现在应该怎么做 ?

FreeMarker模板语言(FTL)的 "哈希表" 类型和Java的 Map 是不同的。FTL的哈希表也是一个关联数组, 但是它仅仅使用字符串的键。这是因为它是为子变量而引入的 (比如 user.password 中的 password, 它和 user["password"] 是相同的),而变量名是字符串。

所以,在FTL有支持非字符串键的类型之前,你还是不得不转向Java的 Map API。你可以这么来做: myMap?api.get(nonStringKey)。然而,对于运行 ?api,你可能需要配置一下 FreeMarker; 在这里参考更多...

请注意,Java的 Map对键的确切类非常专注, 所以对于在模板中计算的数字类型的键,你不得不将它们转换成合适的Java类型, 否则其中的项就不能被发现。比如,如果你在Map中使用 Integer 类型的键,那么你应该编写 ${myMap.get(numKey?int)}。 这是由FTL的有意简化的仅有单独数字类型的类型系统导致的非常丑陋的写法, 而Java区分很多数字类型。请注意,当键值直接从数据模型 (也就是说,你不用在模板中使用算数运算来改变它的值)中获取时是不需要转换的, 包含当它是方法返回值的情况,而且在包装之前要是合适的类, 因为这样解包的结果将会是原始的类型。

15.  当使用 ?keys/?values 遍历Map(哈希表)的内容时,得到了混合真正map条目的 java.util.Map 的方法。当然,只是想获取map的条目。

当然是使用了 BeansWrapper 或者你自己的对象包装器, 或者是它的自定义子类,而它的 simpleMapWrapper 属性将会置成 false。不幸的是,这是默认(出于向下兼容的考虑)的情况, 所以在你创建对象包装器的地方,你不得不明确地设置它为 true。 而且,至少从 2.3.22 版本开始,应用程序应该使用 DefaultObjectWrapper (将 它的 incompatibleImprovements 设置为 2.3.22 - 这很重要), 那就不会有这个问题。

16.  在 FreeMarker 模板中如何修改序列(list)和哈希表(maps) ?

首先,你也许不想修改序列/哈希表,仅仅是连接(增加)它们中的两个或多个, 这就会生成一个新的序列/哈希表,而不是修改已经存在的那个。这种情况下使用 序列连接哈希表连接符。而且,你也可以使用 子序列操作符 来代替移除序列项。 然而,要注意性能的影响:这些操作很快,但是这些哈希表/序列都是后续操作的结果 (也就是说,当你使用操作的结果作为另外一个操作的输入等情况时), 而这些结果的读取是比较慢的。

现在,如果你仍然想修改序列/哈希表,那么继续阅读...

FreeMarker 模板语言并不支持序列/哈希表的修改。它是用来展示已经计算好的东西的, 而不是用来计算数据的。要保持模板简洁。但是不要放弃,下面你会看到一些建议和技巧。

如果你能在数据模型构建器的程序和模板之间分离这些工作是最好的, 那么模板就不需要来改变序列/哈希表。也许你想重新构思一下你的数据模型了,你会明白这是可能的。 但是,很少有对一些复杂但都是和纯展示相关的算法进行需要修改序列/哈希表的这种情况。 它很少发生,所以要三思那些计算(或它们其中的部分)是属于数据模型领域的而不是展示领域的。 我们假设它们确实是输入展示领域的。比如,你想以一些非常精妙的方式展示一个关键词的索引, 这些算法需要你来创建,还有编写一些序列变量。那么你应该做这样的一些事情 (糟糕的情况包含糟糕的方案):

<#assign caculatedResults =
    'com.example.foo.SmartKeywordIndexHelper'?new().calculate(keywords)>
<#-- some simple algorithms comes here, like: -->
<ul>
  <#list caculatedResults as kw>
    <li><a href="${kw.link}">${kw.word}</a>
  </#list>
</ul>

也就是说,你从模板中去除了展示任务中的复杂部分, 而把它们放到了Java代码中。请注意,它不会影响数据模型, 所以展示层仍然会和其它的应用逻辑相分离。 当然这种处理问题的缺陷就是模板设计者会需要Java程序员的帮助, 但是对于复杂的算法这可能也是需要的。

现在,如果你仍然坚持说你需要直接使用 FreeMarker 模板来改变序列/哈希表, 这里有两种解决方案,但是请阅读它们之后的警告:

  • 你可以通过编写 TemplateMethodModelExTemplateDirectiveModel 的实现类来修改特定类型的序列/哈希表。 仅仅只是特定的类型,因为 TemplateSequenceModelTemplateHashModel 没有用来修改的方法, 所以你需要序列或哈希表来实现一些额外的方法。这个解决方案的一个示例可以在FMPP (FMPP 是 FreeMarker-based text file PreProcessor,即基于FreeMarker的文本文件与处理器, 用于生成文本文件。可以参考FMPP项目的主页获取更多信息http://fmpp.sourceforge.net ,译者注) 中看到。它允许你这样来进行操作(pp 存储由FMPP为模板提供的服务):

    <#assign a = pp.newWritableSequence()>
    <@pp.add seq=a value="red" />

    pp.add 指令仅仅作用于由 pp.newWritableSequence() 方法创建的序列。 因此,模板设计者不能修改一个来自于数据模型的序列。

  • 如果你使用了定制的包装器(你可以使用 <@myList.append foo />),那么序列可以有一些方法/指令。 (而且,如果你使用来配置它,那么它就会暴露出公有的方法, 你可以对变量来使用作用于 java.util.Mapjava.util.List 对象的Java API。就像Apache的Velocity一样。)

但是要小心,这些解决方案有一个问题:序列连接序列切分 操作符 (比如 seq[5..10]) 和 ?reverse 不会复制原来的序列,仅仅只是包装了一下(为了效率),所以, 如果源序列后期(一种不正常的混叠效应)改变了,那么结果序列将也会改变。 相同的问题也存在于 哈希表连接 的结果;它只是包装了两个哈希表,所以,如果你之前修改了要添加的哈希表, 那么结果哈希表将会神奇地改变。作为一种变通方式,在你执行了上述有问题的操作之后, 要保证你没有修改作为输入的对象,或者没有使用由上述两点 (比如,在FMPP中,你可以这样来做:<#assign b = pp.newWritableSequence(a[5..10])><#assign c = pp.newWritableHash(hashA + hashB)>)描述的解决方案提供的方法创建结果的拷贝。 当然这很容易丢失,所以再次重申,宁可创建数据模型而不用去修改集合, 也不要使用上面展示的显示层的任务助手类。

17.  关于 null 在 FreeMarker 模板语言是什么样的?

FreeMarker 模板语言并不知道Java语言中的 null。 它也没有关键字 null,而且它也不能测试变量是否是 null。当在技术上面对 null 时, 那么它会将其视作是不存在的变量。比如,如果 x 在数据模型中是 null,而且不会被呈现出来,那么 ${x!'missing'} 将会打印出 "missing",你无法辨别其中的不同。 而且,比如你想测试是否Java代码中的一个方法返回了 null, 仅仅像 <#if foo.bar()??> 这样来写即可。

你也许对这后面实现的理由感兴趣,出于展示层的观点,null 和不存在的东西通常是一样的。这二者之间的不同仅仅是技术上的细节, 是实现细节的结果而不是应用逻辑。你也不能将 null 和其它东西来比较(不像Java语言);在模板中,null 和其它东西来比较是没有意义的,因为模板语言不进行标识比较 (当你想比较两个对象时,像Java中的 == 操作符), 但更常见的是内容的比较(像Java语言中的 Object.equals(Object); 它也不会对 null 起作用)。 而 FreeMarker 如何来别变一些具体的东西和不存在的或未知的东西来比较? 或两个不存在(未知的)东西是相等的?当然这些问题无法来回答。

这个不了解 null 的方法至少有一个问题。 当你从模板中调用Java代码的方法时,你也许想传递 null 值作为参数(因为方法是设计用于Java语言的,那里是有 null 这个概念的)。 这种情况下你可以利用 FreeMarker 的一个bug(在我们提供一个传递 null 值给方法正确的方案前,这个bug我们是不会修复的):如果你指定一个不存在的值作为参数, 那么它不会引发错误,但是 null 就会被传递给这个方法。就像 foo.bar(nullArg) 将会使用 null 作为参数调用 bar 方法,假设没有名为"nullArg"的参数存在。

18.  我该怎么在表达式(作为另外一个指令参数)中使用指令(宏)的输出 ?

使用 assignlocal 指令捕捉输出到变量中。比如:

<#assign capturedOutput><@outputSomething /></#assign>
<@otherDirective someParam=capturedOutput />
19.  在输出中为什么用"?"来代替字符 X

这是因为你想打印的字符不能用输出流的 字符集 (编码) 来表现,所以Java平台 (而不是FreeMarker)用问号来代替了会有问题的字符。通常情况,对于输出和模板 (使用模板对象的 getEncoding() 方法)应该使用相同的字符集, 或者是更安全的,你通常对输出应该使用UTF-8字符集。 对输出流使用的字符集不是由FreeMarker决定的,而是你决定的,当你创建 Writer 对象时,传递了模板的 process 方法。

示例:这里在Servlet中使用了UTF-8字符集:

...
resp.setContentType("text/html; charset=utf-8");
Writer out = resp.getWriter();
...
t.process(root, out);
...

请注意,问号(或其他替代符号)可能在 FreeMarker 环境之外产生, 这种情况上面的做法都没有用了。 比如一种糟糕的/错误配置的数据库连接或者JDBC驱动可能带来已经替代过的字符文本。 HTML形式是另外一种编码问题的潜在来源。 在很多地方打印字符串中字符的数字编码是个很好的想法, 首先来看看问题是在哪里发生的。

你可以阅读有关字符集和 FreeMarker 的更多信息:在这里...

20.  在模板执行完成后,怎么在模板中获取计算过的值 ?

首先,要确定你的应用程序设计得很好:模板应该展示数据, 而不是来计算数据。如果你仍确定你想这么做,请继续阅读...

当你使用 <#assign x = "foo">时, 那么你不会真正修改数据模型(因为它是只读的,参考: 程序开发指南/其它/多线程),但是在处理 (参考 程序开发指南/其它/变量,范围)运行时环境 (参考environment)创建 x 变量。这个问题就是当 Template.process 返回时,运行时环境将会被丢弃,因为它是为单独 Template.process 调用创建的:

// internally an Environment will be created, and then discarded
myTemplate.process(root, out);

要阻止这个,你可以做下面的事情,是和上面相同的, 除了你有机会返回在模板中创建的变量:

Environment env = myTemplate.createProcessingEnvironment(root, out);
env.process();  // process the template
TemplateModel x = env.getVariable("x");  // get variable x
21.  How to assign to (or #import into) a dynamically constructed variable name (like to name that's stored in another variable)?

If you really can't avoid doing that (you should, as it's confusing), you can solve that with constructing the appropriate FTL source code dynamically in a string, then using the interpret built-in. For example, if you want to assign to the variable whose name is stored in the varName variable:

<@"<#assign ${varName}='example'>"?interpret />
22.  Can I allow users to upload templates and what are the security implications?

In general you shouldn't allow that, unless those users are system administrators or other trusted personnel. Consider templates as part of the source code just like *.java files are. If you still want to allow users to upload templates, here are what to consider:

  • Denial-of-Service (DoS) attacks: It's trivial to create templates that run practically forever (with a loop), or exhaust memory (by concatenating to a string in a loop). FreeMarker can't enforce CPU or memory usage limits, so this is something that has no solution on the FreeMarker-level.

  • Data-model and wrapping (Configuration.setObjectWrapper): The data-model might gives access to the public Java API of some objects that you have put into the data-model. By default, for objects that aren't instances of a the bunch of specially handler types (String, Number, Boolean, Date, Map, List, array, and a few others), their public Java API will be exposed. To avoid that, you have to construct the data-model so that it only exposes the things that are really necessary for the template. For that, you may want to use SimpleObjectWrapper (via Configuration.setObjectWrapper or the object_wrapper setting) and then create the data-model purely from Map-s, List-s, Array-s, String-s, Number-s, Boolean-s and Date-s. Or, you can implement your own extremely restrictive ObjectWrapper, which for example could expose your POJO-s safely.

  • Template-loader (Configuration.setTemplateLoader): Templates may load other templates by name (by path), like <#include "../secret.txt">. To avoid loading sensitive data, you have to use a TemplateLoader that double-checks that the file to load is something that should be exposed. FreeMarker tries to prevent the loading of files outside the template root directory regardless of template loader, but depending on the underlying storage mechanism, exploits may exist that FreeMarker can't consider (like, just as an example, ~ jumps to the current user's home directory). Note that freemarker.cache.FileTemplateLoader checks the canonical paths, so that's maybe a good candidate for this task, yet, adding a file extension check (file must be *.ftl) is maybe a good idea.

  • The new built-in (Configuration.setNewBuiltinClassResolver, Environment.setNewBuiltinClassResolver): It's used in templates like "com.example.SomeClass"?new(), and is important for FTL libraries that are partially implemented in Java, but shouldn't be needed in normal templates. While new will not instantiate classes that are not TemplateModel-s, FreeMarker contains a TemplateModel class that can be used to create arbitrary Java objects. Other "dangerous" TemplateModel-s can exist in you class-path. Plus, even if a class doesn't implement TemplateModel, its static initialization will be run. To avoid these, you should use a TemplateClassResolver that restricts the accessible classes (possibly based on which template asks for them), such as TemplateClassResolver.ALLOWS_NOTHING_RESOLVER.

23.  How to implement a function or macro in Java Language instead of in the template language?

It's not possible (yet), but something very similar is possible if you write a class that implements freemarker.template.TemplateMethodModelEx or freemarker.template.TemplateDirectiveModel respectively, and then where you were write <#function my ...>...</#function> or <#macro my ...>...</#macro> you write <#assign my = "your.package.YourClass "?new()> instead. Note that using the assign directive for this works because functions (and methods) and macros are just plain variables in FreeMarker. (For the same reason you could also put TemplateMethodModelEx or TemplateDirectiveModel instances into the data-model before calling the template, or into the shared variable map (see: freemarker.template.Configuration.setSharedVariable(String, TemplateModel)) when you initialize the application.)

24.  In my Servlet based application, how do I show a nice error page instead of a stack trace when error occurs during template processing?

First of all, use RETHROW_HANDLER instead of the default DEBUG_HANDLER (for more information about template exception handlers read this...). Now FreeMarker will not print anything to the output when an error occurs, so the control is in your hands. After you have caught the exception of Template.process(...) basically you can follow two strategies:

  • Call httpResp.isCommitted(), and if that returns false, then you call httpResp.reset() and print a ``nice error page'' for the visitor. If the return value was true, then try to finish the page be printing something that makes clear for the visitor that the page generation was abruptly interrupted because of an error on the Web server. You may have to print a lot of redundant HTML end-tags and set colors and font size to ensure that the error message will be actually readable in the browser window (check the source code of the HTML_DEBUG_HANDLER in src\freemarker\template\TemplateException.java to see an example).

  • Use full page buffering. This means that the Writer doesn't send the output to the client progressively, but buffers the whole page in the memory. Since you provide the Writer instance for the Template.process(...) method, this is your responsibility, FreeMarker has nothing to do with it. For example, you may use a StringWriter, and if Template.process(...) returns by throwing an exception, then ignore the content accumulated by the StringWriter, and send an error page instead, otherwise you print the content of StringWriter to the output. With this method you surely don't have to deal with partially sent pages, but it can have negative performance implications depending on the characteristic of the pages (for example, the user will experience more response delay for a long page that is generated slowly, also the server will consume more RAM). Note that using a StringWriter is surely not the most efficient solution, as it often reallocates its buffer as the accumulated content grows.

25.  I'm using a visual HTML editor that mangles template tags. Will you change the template language syntax to accommodate my editor?

We won't change the standard version, because a lot of templates depend on it.

Our view is that the editors that break template code are themselves broken. A good editor should ignore, not mangle, what it doesn't understand.

You maybe interested in that starting from FreeMarker 2.3.4 you can use [ and ] instead of < and >. For more details read this...