错误控制

可能的异常

关于 FreeMarker 发生的异常,可以分为如下几类:

  • 当配置 FreeMarker 时发生异常:典型地情况,就是在应用程序初始化时, 仅仅配置了一次 FreeMarker。在这个过程中,异常就会发生, 从 FreeMarker 的API中,我们可以很清楚的看到这一点...

  • 当加载和解析模板时发生异常:调用了 Configuration.getTemplate(...) 方法, FreeMarker就要把模板文件加载到内存中然后来解析它 (除非模板已经在 Configuration 对象中被 缓存 了)。 在这期间,有两种异常可能发生:

    • 因模板文件没有找到而发生的 IOException 异常, 或在读取文件时发生其他的I/O问题。比如没有读取文件的权限,或者是磁盘错误。 这些错误的发出者是 TemplateLoader 对象,可以将它作为插件设置到 Configuration对象中。(为了正确起见:这里所说的”文件”, 是简化形式。例如,模板也可以存储在关系型数据库的表中。这是 TemplateLoader所要做的事。)

    • 根据FTL语言的规则,模板文件发生语法错误时会导致 freemarker.core.ParseException异常。当获得 Template对象 (Configuration.getTemplate(...))时, 这种错误就会发生,而不是当执行 (Template.process(...))模板的时候。 这种异常是 IOException 的一个子类。

  • 当执行(处理)模板时发生的异常,也就是当调用了 Template.process(...) 方法时会发生的两种异常:

    • 当试图写入输出对象时发生错误而导致的 IOException 异常。

    • 当执行模板时发生的其它问题而导致的 freemarker.template.TemplatException 异常。 比如,一个频繁发生的错误,就是当模板引用一个不存在的变量。 默认情况下,当 TemplatException 异常发生时, FreeMarker会用普通文本格式在输出中打印出FTL的错误信息和堆栈跟踪信息, 然后再次抛出 TemplatException 异常而中止模板的执行, 就可以捕捉到 Template.process(...) 方法抛出的异常了。而这种行为是可以定制的。FreeMarker也会经常写 TemplatException 异常的 日志

根据TemplateException来自定义处理方式

TemplateException 异常在模板处理期间的抛出是由 freemarker.template.TemplateExceptionHandler 对象控制的,这个对象可以使用 setTemplateExceptionHandler(...) 方法配置到 Configuration 对象中。 TemplateExceptionHandler 对象只包含一个方法:

void handleTemplateException(TemplateException te, Environment env, Writer out)
        throws TemplateException;

无论 TemplateException 异常什么时候发生,这个方法都会被调用。 异常处理是传递的 te 参数控制的, 模板处理的运行时(Runtime,译者注)环境可以访问 env 变量, 处理器可以使用 out 变量来打印输出信息。 如果方法抛出异常(通常是重复抛出 te),那么模板的执行就会中止, 而且 Template.process(...) 方法也会抛出同样的异常。如果 handleTemplateException 对象不抛出异常,那么模板将会继续执行,就好像什么也没有发生过一样, 但是引发异常的语句将会被跳过(后面会详细说)。 当然,控制器仍然可以在输出中打印错误提示信息。

任何一种情况下,当 TemplateExceptionHandler 被调用前, FreeMarker 将会记录异常日志

我们用实例来看一下,当错误控制器不抛出异常时, FreeMarker是如何跳过出错''语句''的。假设我们已经使用了如下模板异常控制器:

class MyTemplateExceptionHandler implements TemplateExceptionHandler {
    public void handleTemplateException(TemplateException te, Environment env, java.io.Writer out)
            throws TemplateException {
        try {
            out.write("[ERROR: " + te.getMessage() + "]");
        } catch (IOException e) {
            throw new TemplateException("Failed to print error message. Cause: " + e, env);
        }
    }
}

...

cfg.setTemplateExceptionHandler(new MyTemplateExceptionHandler());

如果错误发生在非FTL标记(没有被包含在 <#...><@...>之间)的插值中, 那么整个插值将会被跳过。那么下面这个模板 (假设 badVar 在数据模型中不存在):

a${badVar}b

如果我们使用了 MyTemplateExceptionHandler,就会打印:

a[ERROR: Expression badVar is undefined on line 1, column 4 in test.ftl.]b

而下面这个模板也会打印相同信息(除了报错的列数位置会不同...):

a${"moo" + badVar}b

因为像这样来写时,只要插值内发生任何错误,整个插值都会被跳过。

如果错误发生在指令调用中参数的计算时,或者是指令参数列表发生问题时, 或在<@exp ...>中计算 exp时发生错误,或者 exp不是用户自定义的指令, 那么整个指令调用都会被跳过。例如:

a<#if badVar>Foo</#if>b

将会输出:

a[ERROR: Expression badVar is undefined on line 1, column 7 in test.ftl.]b

请注意,错误发生在 if 指令的开始标签 (<#if badVar>)中,但是整个指令的调用都被跳过了。 从逻辑上说,嵌套的内容(Foo)也被跳过了, 因为嵌套的内容是受被包含的指令(if)控制(打印)的。

下面这个的输出也是相同的(除了报错的列数会不同...):

a<#if "foo${badVar}" == "foobar">Foo</#if>b

因为,正如这样来写,在参数处理时发生任何一个错误, 整个指令的调用都将会被跳过。

如果错误发生在已经开始执行的指令之后,那么指令调用将不会被跳过。 也就是说,如果在嵌套的内容中发生任何错误:

a
<#if true>
  Foo
  ${badVar}
  Bar
</#if>
c

或者在一个宏定义体内:

a
<@test />
b
<#macro test>
  Foo
  ${badVar}
  Bar
</#macro>

那么输出将会是:

a
  Foo
  [ERROR: Expression badVar is undefined on line 4, column 5 in test.ftl.]
  Bar
c

FreeMarker 本身带有这些预先编写的错误控制器:

  • TemplateExceptionHandler.DEBUG_HANDLER: 打印堆栈跟踪信息(包括FTL错误信息和FTL堆栈跟踪信息)和重新抛出的异常。 这是默认的异常控制器(也就是说,在所有新的 Configuration 对象中,它是初始化好的)。

  • TemplateExceptionHandler.HTML_DEBUG_HANDLER: 和 DEBUG_HANDLER 相同,但是它可以格式化堆栈跟踪信息, 那么就可以在Web浏览器中来阅读错误信息。 当你在制作HTML页面时,建议使用它而不是 DEBUG_HANDLER

  • TemplateExceptionHandler.IGNORE_HANDLER: 简单地压制所有异常(但是要记住,FreeMarker 仍然会写日志)。 它对处理异常没有任何作用,也不会重新抛出异常。

  • TemplateExceptionHandler.RETHROW_HANDLER: 简单重新抛出所有异常而不会做其它的事情。 这个控制器对Web应用程序(假设你在发生异常之后不想继续执行模板)来说非常好, 因为它在生成的页面发生错误的情况下,给了你很多对Web应用程序的控制权 (因为FreeMarker不向输出中打印任何关于该错误的信息)。 要获得更多在Web应用程序中处理错误的信息,可以 参见FAQ

在模板中明确地处理错误

尽管它和 FreeMarker 的配置(本章的主题)无关,但是为了说明的完整性, 在这里提及一下,你可以在模板中直接控制错误。 通常这不是一个好习惯(尽量保持模板简单,技术含量不要太高),但有时仍然需要: