1、基本内容
配置就是在对象中存储常用(应用级别)的设置和定义某些想在所有模板中可用的变量。它们也会处理Template实例的创建和缓存操作。配置对象是freemarker.template.Configuration的实例,可以通过构造方法来创建它。一个应用程序通常只使用一个共享的Configuration实例。
配置对象通过Template的方法来使用,特别是通过process方法。每个实例都有一个确切的而且和它相关的Configuration实例,这个实例由Template的构造方法分配给Template实例。可以指定一个Configuration实例作为它的参数。通常情况下,使用Configuration.getTemplate(而不是直接调用Template的构造方法)来获得Template实例,这种情况下,相关的Configuration实例就会是getTemplate方法被调用时的那个实例。
2、共享变量
Shared variables共享变量是为所有模板所定义的变量。可以使用setSharedVariable方法向配置实例中添加共享变量:
Configuration cfg = new Configuration();
...
cfg.setSahredVariable("wrap", new WrapDirective());
// 使用 ObjectWrapper.DEFAULT_WRAPPER
cfg.setSharedVariable("company", "Foo Inc.");
在所有使用这个配置的模板中,名为wrap的用户自定义指令和一个名为company的字符串将会在数据模型的根上可见,那就不用在根哈希表上一次又一次的添加它们。在传递给Template.process根对象里的变量将会隐藏同名的共享变量。
警告!
如果配置对象在多线程环境中使用,不要使用TemplateModel实现类来作为共享变量,因为它是线程不安全的。这也是基于Servlet的Web站点的典型情况。
出于向后兼容的特性,共享变量的集合初始化时(就是对于新的Configuration实例来说)不能为空。它包含下列用户自定义指令(用户自定义指令使用时需要用@来代替#):
3、配置信息
Settings配置信息是影响FreeMarker行为的已经被命名的值。配置信息的项有:locale,number_format。
配置信息信息存储在Configuration实例中,可以在Template实例中被覆盖。例如在配置对象中给locale设置为"en_US",那么locale在所有模板中都使用"en_US"的配置,除非在模板中locale被明确地设置成其它不同的(参见本地化设置)值。因此,在Configuration中的值充当默认值,这些值在每个模板中也可以被覆盖。在Configuration和Template实例中的值也可以在单独调用Template.process方法后被覆盖。对于每个调用了freemarker.core.Environment对象的值在内部创建时就持有模板执行的运行时环境,也包括了那个级别被覆盖了的设置信息。在模板执行时,那里存储的值也可以被改变,所以模板本身也可以设置配置信息,比如在输出中途来变换locale设置。
配置信息可以被想象成3层(Configuration,Template,Environment),最高层包含特定的值,它为设置信息提供最有效的值。比如(设置信息A到F仅仅是为这个示例而构想的)
配置信息的有效值为:A=1,B=2,C=3,D=1,E=2。而F的设置则是null,或者在你获取它的时候将抛出异常。
让我们看看如何准确设置配置信息:
Configuration层:原则上设置配置信息时使用Configuration对象的setter方法,例如:
Configuration myCfg = new Configuration();
myCfg.setLocale(java.util.Locale.ITALY);
myCfg.setNumberFormat("0.####");
在真正使用Configuration对象(通常在初始化应用程序时)之前来配置它,后面必须将其视为只读的对象。
在实践中,比如很多Web应用框架中,就应该使用这种框架特定的配置方式来进行配置,比如使用成对的字符串来配置(像在.properties属性配置文件中那样)。在这种情况下,框架的作者大多数使用Configuration对象的setSetting(String name, String value)方法。而在Spring框架中,我们可以这样进行:
Environment env = myTemplate.createProcessingEnvironment(root, out);
env.setLocale(java.util.Locale.ITALY);
env.setNumberFormat("0.####");
env.process(); // 处理模板
在模板中直接使用指令,例如:
<#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)配置。
4、模板加载
模板加载器:模板加载器是加载基于抽象模板路径下,比如"index.ftl"或"products/catalog.ftl"的原生文本数据对象。这由具体的模板加载器对象来确定它们取得请求数据时使用了什么样的数据来源(文件夹中的文件,数据等等)。当调用cfg.getTemplate(这里的cfg就是Configuration实例)时,FreeMarker询问模板加载器是否已经为cfg建立返回给定模板路径的文本,之后FreeMarker解析文本生成模板。
4.1、内建模板加载器
在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.xml中配置一个Servlet来处理URI格式为*.ftl的用户请求,否则客户端无法获取到模板,因此你将会看到Web服务器给出的秘密提示内容。在站点中不能使用空路径,这将成为一个问题,你应该在WEB-INF目录下的某个位置存储模板文件,这样模板源文件就不会偶然地被执行到,这种机制对servlet应用程序来加载模板来说,是非常好用的方式,而且模板可以自动更新而不需重启Web应用程序,但是对于类加载机制,这样就行不通了。
4.2、从多个位置加载模板
如果需要从多个位置加载模板,那就不得不为每个位置都实例化模板加载器对象,将它们包装到一个被称为MultiTemplateLoader的特殊模板加载器,最终将这个加载器传递给Configuration对象的setTemplateLoader(TemplateLoader loader)方法。下面给出一个使用类加载器从两个不同位置加载模板的示例:
import freemarker.cache.*; // 模板加载器在这个包下
...
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目录下加载,如果还是没有发现请求的模板,那么它就会使用类加载器来加载模板。
4.3、从其他资源加载模板
如果内建的类加载器都不适合使用,那么就需要来编写自己的类加载器了,这个类需要实现freemarker.cache.TemplateLoader接口,然后将它传递给Configuration对象的setTemplateLoader(TemplateLoader loader)方法。
如果你的模板需要通过URL访问其他模板,那么就不需要实现TemplateLoader接口了,可以选择子接口freemarker.cache.URLTemplateLoader来替代,只需实现URL getURL(String templateName)方法即可。
4.4、模板路径
解析模板的路径是由模板解析器来决定的。但是要和其它对路径的格式要求很严格的组件一起工作。通常来说,强烈建议模板加载器使用URL风格的路径。在URL路径(或在UN*X路径)中有其它含义时,那么路径中不要使用/,./,../和:// 。字符*和?是被保留的。而且,模板加载器也不想模板以/开始;FreeMarker从来不会使用这样的路径来调用模板加载器。FreeMarker再将路径传递给模板加载器之前通常会将路径进行正常化操作,所以相对于假想的模板根目录,路径中不会含有/../这些东西。
注意FreeMarker模板加载时经常使用斜线(而不是反斜线),不管运行的主机操作系统是什么。
4.5、模板缓存
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对象时,它使用一个maxStrongSize值为0的MruCacheStorage缓存来初始化,maxSoftSize的值是Integer.MAX_VALUE(也就是说在实际中,是无限大的)。但是使用非0的maxStrongSize对于高负载的服务器来说也许是一个更好的策略,对于少量引用的组件来说,如果资源消耗已经很高的话,Java虚拟机往往会引发更高的资源消耗,因为它不断从缓存中抛出经常使用的模板,这些模板还不得不再次加载和解析。
5、错误控制
5.1、可能的异常
关于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异常的日志。
5.2、根据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应用程序的控制权
5.3、在模板中明确地处理错误
尽管它和FreeMarker的配置(本章的主题)无关,你可以在模板中直接控制错误。通常这不是一个好习惯(尽量保持模板简单,技术含量不要太高),但有时仍然需要:
控制不存在/为空的变量:模板开发指南/模板/表达式/处理不存在的值部分。
在发生障碍的“porlets”中留存下来,可阅读的部分:参考手册/指令参考/尝试恢复部分。
6、其他方面
6.1、变量
当模板在访问变量时发生了什么事情,还有变量是如何存储的。
当调用Template.process方法时,它会在方法内部创建一个Environment对象,在process返回之前一直使用。Environment对象存储模板执行时的运行状态信息。除了这些,它还存储由模板中指令,如assign,macro,local或global创建的变量。它从来不会改变你传递给process的数据模型对象,也不会创建或替换存储在配置中的共享变量。
当你想要读取一个变量时,FreeMarker将会以这种顺序来查找,直到发现了完全匹配的的变量名称才会停下来:
1)在Environment对象中:
(1)、如果在循环中,在循环变量的集合中。循环变量是由(如list指令)来创建的。
(2)、如果在宏中,在宏的局部变量集合中。局部变量可以由local指令创建。而且,宏的参数也是局部变量。
(3)、在当前的命名空间中。可以使用assign指令将变量放到一个命名空间中。
(4)、在由global指令创建的变量集合中。FTL将它们视为数据模型的普通成员变量一样来控制它们。也就是说,它们在所有的命名空间中都可见,你也可以像访问一个数据模型中的数据一样来访问它们。
2)在传递给process方法的数据模型对象中。
3)在Configuration对象存储的共享变量集合中。
在实际操作中,来自模板设计者的观点是这6种情况应该只有4种,因为从那种观点来看,后面3种(由global创建的变量,真实的数据模型对象,共享变量)共同构成了全局变量的集合。
要注意在FTL中从一个确定的层面获取确定的变量是可以的。
6.2、字符集问题
像其它大多数的Java应用程序一样,FreeMarker使用“UNICODE 文本”(UTF-16)来工作。不过,也有必须处理字符集的情况,因为它不得不和外界交换数据,这就会使用到很多字符集。
1)输入的字符集
当FreeMarker要加载模板文件(或没有解析的文本文件)时,那就必须要知道文件使用的字符集,因为文件的存储是原生的字节数组形式。可以使用配置项encoding来确定字符集。这个配置项只在FreeMarker使用Configuration对象的getTemplate方法加载模板(解析过的或没有解析过的)时起作用。要注意include指令在内部也使用了这个方法,所以encoding的值对一个已经加载的模板(如果这个模板包含include指令的调用)来说很重要。
encoding配置的getter和setter方法在第一个(配置)层面很特殊。getter方法猜想返回值是基于Locale(本地化,译者注)传递的参数;它在地图区域编码表(称为编码地图)中查询编码,如果没有找到该区域,就返回默认编码。可以使用配置对象的setEncoding(Locale locale, String encoding)方法来填充编码表;编码表初始化时是空的。默认的初始编码是系统属性file.encoding的值,但是可以通过setDefaultEncoding方法来设置一个不同的默认值。
你也可以在模板层或运行环境层(当指定编码值作为getTemplate方法的参数时,应该在模板层覆盖encoding设置)直接给定值来覆盖encoding的设置。如果不覆盖它,那么locale设置的有效值将会是configuration.getEncoding(Locale)方法的返回值。
而且,代替这种基于字符集猜测的机制,你也可以在模板文件中使用ftl指令来指定特定的字符集。
也许你想知道为模板选择什么样的字符集更合适一些。这主要是依赖于你用来创建和修改模板的工具(如文本编辑器)。原则上,使用UTF-8字符集是最好的,但是在2004年时只有很少的一部分工具支持UTF-8,其它都不支持这种字符集。所以那种情况下就要使用本土语言中使用最广泛的字符集,这也许是你工作环境中使用的默认字符集(比如中国大陆主要使用GBK/GB2312字符集,中国台湾和香港地区主要使用BIG5字符集,译者注)。
要注意模板使用的字符集和模板生成的输出内容的字符集是独立的(除非包含FreeMarker的软件故意将设置输出内容的字符集和模板字符集设置成相同的)。
2)输出的字符集
注意:
output_encoding设置/参数和内建函数url从FreeMarker 2.3.1版本开始才可以使用,而在2.3以前的版本中是不存在的。
原则上,FreeMarker不处理输出内容的字符集问题,因为FreeMarker将输出内容都写入了java.io.Writer对象中。而Writer对象是由封装了FreeMarker(比如Web应用框架)的软件生成的,那么输出内容的字符集就是由封装软件来控制的。而FreeMarker有一个称为output_encoding(开始于FreeMarker 2.3.1版本之后)的设置。封装软件应该使用这个设置(Writer对象使用的字符集)来通知FreeMarker在输出中(否则FreeMarker不能找到它)使用哪种字符集。有一些新的特性,如内建函数url,特殊变量output_encoding也利用这个信息。因此,如果封装软件没有设置字符集这个信息,那么FreeMarker需要知道输出字符集的特性就不能被利用了。
如果你使用FreeMarker来编写软件,你也许想知道在输出内容中到底选择了哪种字符集。当然这取决于运行FreeMarker输出内容的计算机本身,但是如果用户对这个问题可以变通,那么通用的实践是使用模板文件的字符集作为输出的字符集,或者使用UTF-8。通常使用UTF-8是最佳的实践,因为任意的文本可能来自数据模型,那就可能包含不能被模板字符集所编码的字符。
如果使用了Template.createProcessingEnvironment(...)和Environment.process(...)方法来代替Template.process(...)方法,FreeMarker的设置可以对任意独立执行的模板进行。因此,你可以对每个独立执行的模板设置output_encoding信息:
Writer w = new OutputStreamWriter(out, outputCharset);
Environment env = template.createProcessingEnvironment(dataModel, w);
env.setOutputEncoding(outputCharset);
env.process();
7、多线程
在多线程运行环境中,Configuration实例,Template实例和数据模型应该是永远不能改变(只读)的对象。也就是说,创建和初始化它们(如使用set...方法)之后,就不能再修改它们了(比如不能再次调用set...方法)。这就允许我们在多线程环境中避免代价很大的同步锁问题。要小心Template实例;当你使用Configuration.getTemplate方法获得它的一个实例时,也许得到的是从模板缓存中缓存的实例,这些实例都已经被其他线程使用了,所以不要调用它们的set...方法(当然调用process方法还是不错的)。
如果你只从同一个线程中访问所有对象,那么上面所述的限制将不会起作用。
使用FTL来修改数据模型对象或者共享变量是不太可能的,除非将方法(或其他对象)放到数据模型中来做。我们不鼓励你编写修改数据模型对象或共享变量的方法。多试试使用存储在环境对象(这个对象是为独立的Template.process调用而创建的,用来存储模板处理的运行状态)中的变量,所以最好不要修改那些由多线程使用的数据。
8、Bean的包装
freemarker.ext.beans.BeansWrapper是一个对象包装器,最初加到FreeMarker中是为了将任意的POJO(Plan Old Java Objects,普通的Java对象)包装成TemplateModel接口类型。这样它就可以以正常的方式来进行处理,事实上DefaultObjectWrapper本身是BeansWrapper的扩展类。这里描述的所有东西对DefaultObjectWrapper都是适用的,除了DefaultObjectWrapper会用到freemarker.template.SimpleXxx类包装的String,Number,Date,array,Collection(如List),Map,Boolean和Iterator对象,会用 freemarker.ext.dom.NodeModel来包装W3C的DOM节点,所以上述这些描述的规则不适用。
当出现下面这些情况时,你会想使用BeansWrapper包装器来代替DefaultObjectWrapper:
在模板执行期间,数据模型中的Collection和Map应该被允许修改。(DefaultObjectWrapper会阻止这样做,因为当它包装对象时创建了数据集合的拷贝,而这些拷贝都是只读的。)
如果array,Collection和Map对象的标识符当在模板中被传递到被包装对象的方法时,必须被保留下来。也就是说,那些方法必须得到之前包装好的同类对象。
如果在之前列出的Java API中的类(如String,Map,List等),应该在模板中可见。还有,默认情况下它们是不可见的,但是可以设置获取的可见程度。要注意这不是一个好的实践,尽量去使用内建函数(如foo?size,foo?upper,foo?replace('_', '-')等)来代替Java API的使用。
下面是对BeansWrapper创建的TemplateModel对象进行的总结。为了后续的讨论,这里我们假设在包装之前对象都称为obj,而包装的后称为model。
8.1、TemplateHashModel functionality 模板哈希表模型
所有的对象都将被包装成TemplateHashModel类型,进而可以获取出JavaBean对象中的属性和方法。这样,就可以在模板中使用model.foo的形式来调用obj.getFoo()方法或obj.isFoo()方法了。(要注意公有的属性直接是不可见的,必须为它们编写getter方法才行)公有方法通过哈希表模型来取得,就像模板方法模型那样,因此可以使用model.doBar()来调用object.doBar()。下面我们来更多讨论一下方法模型功能。
如果请求的键值不能映射到一个bean的属性或方法时,那么框架将试图定位到“通用的get方法”,这个方法的签名是public any-return-type get(String)或public any-return-type get(Object),使用请求键值来调用它们。这样就使得访问java.util.Map和其他类似类型的键值对非常便利。只要map的键是String 类型的,属性和方法名可以在映射中查到。(有一种解决方法可以用来避免在映射中遮挡名称)要注意java.util.ResourceBundle对象的方法使用getObject(String) 方法作为通用的get方法。
如果在BeansWrapper实例中调用了setExposeFields(true)方法,那么
它仍然会暴露出类的公有的,非静态的变量,用它们作为哈希表的键和值。即如果foo是类Bar的一个公有的,非静态的变量,而bar是一个包装了Bar实例模板变量,那么表达式bar.foo的值将会作为bar对象中foo变量的值。所有这个类的超类中公有变量都会被暴露出来
8.2、一点安全性
默认情况下,不能访问模板制作时认为是不安全的一些方法。比如,不能使用同步方法(wait,notify,notifyAll),线程和线程组的管理方法(stop,suspend,resume,setDaemon,setPriority),反射相关方法(Field setXxx,Method.invoke,Constructor.newInstance,Class.newInstance,Class.getClassLoader等),System和Runtime类中各种有危险性的方法(exec,exit,halt,load等)。BeansWrapper也有一些安全级别(被称作“方法暴露的级别”),默认的级别被称作为EXPOSE_SAFE,它可能对大多数应用程序来说是适用的。没有安全保证的级别称作是EXPOSE_ALL,它允许你调用上述的不安全的方法。一个严格的级别是EXPOSE_PROPERTIES_ONLY,它只会暴露出bean属性的getters方法。最后,一个称作是EXPOSE_NOTHING的级别,它不会暴露任何属性和方法。这种情况下,你可以通过哈希表模型接口访问的那些数据只是map和资源包中的项,还有,可以从通用get(Object) 方法和get(String)方法调用返回的对象,所提供的受影响的对象就有这样的方法。
8.3、TemplateScalarModel functionality 模板标量模型
对于java.lang.String对象的模型会实现TemplateScalarModel接口,这个接口中的getAsString()方法简单代替了toString()方法。要注意把String对象包装到Bean包装器中,要提供比它们作为标量时更多的功能:因为哈希表接口描述了上述所需功能,那么包装String的模型也会提供访问所有String的方法(indexOf,substring等)。
8.4、TemplateNumberModel functionality 模板数字模型
对于是java.lang.Number的实例对象的模型包装器,它们实现了TemplateNumberModel接口,接口中的getAsNumber()方法返回被包装的数字对象。要注意把Number对象包装到Bean包装器中,要提供比它们作为数字时更多的功能:因为哈希表接口描述了上述所需功能,那么包装Number的模型也会提供访问所有Number的方法。
8.5、TemplateCollectionModel functionality 模板集合模型
对于本地的Java数组和其他所有实现了java.util.Collection接口的类的模型包装器,都实现了TemplateCollectionModel接口,因此也增强了使用list指令的附加功能。
8.6、TemplateSequenceModel functionality 模板序列模型
对于本地的Java数组和其他所有实现了java.util.List接口的类的模型包装器,都实现了TemplateSequenceModel接口,这样,它们之中的元素就可以使用model[i]这样的语法通过索引来访问了。你也可以使用内建函数model?size来查询数组的长度和列表的大小。
而且,所有的方法都可指定的一个单独的参数,从java.lang.Integer(即int, long, float, double,java.lang.Object,java.lang.Number,java.lang.Integer)中通过反射方法调用,这些类也实现了这个接口。这就意味着你可以通过很方便的方式来访问被索引的bean属性:model.foo[i]将会翻译为obj.getFoo(i)。
8.7、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):因为没有属性和方法名的查找,速度会很快;不容易被隐藏;最终对非字符串的键也能正常工作,因为参数没有被包装,只是被普通的方法调用。实际上,Map中的model(key)和model.get(key)是相等的,只是写起来很短。
java.util.ResourceBundle类的模型也实现了TemplateMethodModelEx接口,作为一种访问资源和信息格式化的方便形式。对资源包的单参数调用,将会取回名称和未包装参数的toString()方法返回值一致的资源。对资源包的多参数调用的情况和单参数一样,但是它会将参数作为格式化的模式传递给java.text.MessageFormat,在第二个和后面的作为格式化的参数中使用未包装的值。MessageFormat对象将会使用它们本来的本地化资源包来初始化。
8.8、解包规则
当从模板中调用Java方法时,它的参数需要从模板模型转换回Java对象。假设目标类型(方法常规参数被声明的类型)是用来T代表的,下面的规则将会按下述的顺序进行依次尝试:
●对包装器来说,如果模型是空模型,就返回Java中的null。
●如果模型实现了AdapterTemplateModel接口,如果它是T的实例,或者它是一个数字而且可以使用数字强制转换成T,那么model.getAdaptedObject(T)的结果会返回。由BeansWrapper创建的所有方法是AdapterTemplateModel的实现,所以由BeansWrapper为基本的Java对象创建的展开模型通常不如原本的Java对象。
● 如果模型实现了已经废弃的WrapperTemplateModel接口,如果它是T的实例,或者它是一个数字而且可以使用数字强制转换成T ,那么model.getWrappedObject()方法的结果会返回。
● 如果T是java.lang.String类型,那么如果模型实现了TemplateScalarModel接口,它的字符串值将会返回。注意如果模型没有实现接口,我们不能尝试使用String.valueOf(model)方法自动转换模型到String类型。这里不得不使用内建函数?string明确地用字符串来处理非标量。
● 如果T是原始的数字类型或者是可由T指定的java.lang.Number类型,还有模型实现了TemplateNumberModel接口,如果它是T的实例或者是它的装箱类型(如果T是原始类型), 那么它的数字值会返回。否则,如果T是一个Java内建的数字类型(原始类型或是java.lang.Number的标准子类,包括BigInteger和BigDecimal),类型T的一个新对象或是它的装箱类型会由数字模型的适当强制的值来生成。
● 如果T是boolean值或java.lang.Boolean类型,模型实现了TemplateHashModel接口,那么布尔值将会返回。
● 如果T是java.util.Map类型,模型实现了TemplateHashModel接口,那么一个哈希表模型的特殊Map表示对象将会返回。
● 如果T是java.util.List类型,模型实现了TemplateSequenceModel接口,那么一个序列模型的特殊List表示对象将会返回。
● 如果T是java.util.Set类型,模型实现了TemplateCollectionModel接口,那么集合模型的一个特殊Set表示对象将会返回。
● 如果T是java.util.Collection或java.lang.Iterable类型,模型实现了TemplateCollectionModel或TemplateSequenceModel接口,那么集合或序列模型(各自地)一个特殊的Set或List表示对象将会返回。
● 如果T是Java数组类型,模型实现了TemplateSequenceModel接口,那么一个新的指定类型的数组将会创建,它其中的元素使用数组的组件类型作为T,递归展开到数组中。
● 如果T是char或者java.lang.Character类型,模型实现了TemplateScalarModel接口,它的字符串表示中包含精确的一个字符,那
么一个java.lang.Character类型的值将会返回。
● 如果T定义的是java.util.Date类型,模型实现了TemplateDateModel接口,而且它的日期值是T的实例,那么这个日期值将会返回。
● 如果模型是数字模型,而且它的数字值是T的实例,那么数字值就会返回。你可以得到一个实现了自定义接口的java.lang.Number类型的自定义子类,也许T就是那个接口。
● 如果模型是日期类型,而且它的日期值是T的实例,那么日期值将会返回。类似的考虑为*。
● 如果模型是标量类型,而且T可以从java.lang.String类型来定义,那么字符串值将会返回。这种情况涵盖T是java.lang.Object, java.lang.Comparable和java.io.Serializable类型。
● 如果模型是布尔类型,而且T可以从java.lang.Boolean类型来定义,那么布尔值将会返回。和**是相同的。
● 如果模型是哈希表类型,而且T可以从freemarker.ext.beans.HashAdapter类型来定义,那么一个哈希表适配器将会返回。和**是形同的。
● 如果模型是序列类型,而且T可以从freemarker.ext.beans.SequenceAdapter类型来定义,那么一个序列适配器将会返回。和**是形同的。
● 如果模型是集合类型,而且T可以从freemarker.ext.beans.SetAdapter类型来定义,那么集合的set适配器将会返回。和**是形同的。
● 如果模型是T的实例,那么模型本身将会返回。这种情况涵盖方法明确地声明一个FreeMarker特定模型接口,而且允许返回指令,当java.lang.Object被请求时允许返回方法和转换的模型。
● 意味着没有可能转换的异常被抛出。
8.9、访问静态方法
从BeansWrapper.getStaticModels()方法返回的TemplateHashModel对象可以用来创建哈希表模型来访问静态方法和任意类型的字段。
BeansWrapper wrapper = BeansWrapper.getDefaultInstance();
TemplateHashModel staticModels = wrapper.getStaticModels();
TemplateHashModel fileStatics =
(TemplateHashModel) staticModels.get("java.io.File");
之后你就可以得到模板的哈希表模型,它会暴露所有java.lang.System类的静态方法和静态字段(final类型和非final类型)作为哈希表的键。设想你已经将之前的模型放到根模型中了:
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实例,然后用它们来代替默认包装器。
8.10、访问枚举类型
在JRE 1.5版本之后,从方法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实例,然后用它们来代替默认包装器