原文网址 译者:曾道涛
简介
“容器”这一概念对于Velocity来说很重要,它是在系统的各部分之间传递一系列数据的通用技术。也就是说,容器是Java层(或者程序员)和模板层(或者设计师)之间的数据搬运工。作为程序员,你会收集各种类型的对象,包括所有你程序需要的,然后把它们放在容器里。对于设计师来说,这些对象,以及它们的方法和属性,可以通过被称为引用的模板元素来访问。一般来说,你会和设计师一起决定应用程序的数据需求。从某种意义上说,一旦你为设计师生成了一个数据集,也即在模板中提供了“API”访问。因此,在这个阶段的开发过程中,你值得花时间仔细分析。
虽然Velocity允许你创建自己的容器类来满足特殊的需求和技术(比如像一个直接访问LDAP服务器的容器),一个叫VelocityContext的基本实现类已经作为发行版的一部分提供给你。
VelocityContext适合所有的一般需求,所以我们强烈推荐你使用它。只有在特殊情况和高级应用中,才需要你扩展或者创建你自己的容器实现。
使用VelocityContext就像使用一个普通的Java Hashtable类一样简单。虽然接口包含其他有用的方法,我们使用的两个主要方法是:
public Object put(String key, Object value);
public Object get(String key);
请注意,参数value必须继承自java.lang.Object。基本类型像int和float必须用合适的包装类包装。
这就是所有关于容器的基本操作。如需更多信息,可以查看发行版中包含的API文档。
for和foreach()遍历对象的支持
作为一个程序员,对于你放在容器中的对象有很大的自主权。但是正如大多数自主权,这也需要一点点责任,所以要理解Velocity支持什么,以及任何可能出现的问题。Velocity支持几种集合类型在VTL中使用foreach()语句。
• Object [] 普通对象数组,在这里无需多说。如果一个类中提供了迭代器接口,Velocity会自动包装你的数组,但是这不应该涉及到程序员和模板的设计者。更有趣的是,Velocity现在允许模板设计者把数组当作定长链表来处理(Velocity 1.6中就是这样)。这意味着他们既可以在数组上也可以在java.util.List的实例上调用像size(), isEmpty()和get(int)这样的方法,而无需关心它们本身的差异。
• java.util.Collection Velocity通过iterator()方法返回一个迭代器在循环中使用,所以如果你正在你的对象上实现一个集合接口,请确保iterator()方法返回一个有效的迭代器。
• java.util.Map 这里,Velocity通过接口的values()方法返回一个Collection接口,iterator()方法在它上面调用来检索用于循环的迭代器。
• java.util.Iterator 使用的时候需要注意:目前只是暂时支持,关注的问题是迭代器不能重置。如果一个未初始化的迭代器被放进了容器,并且在多个foreach()语句中使用,如果第一个foreach()失败了,后面的都会被阻塞,因为迭代器不会重启。
• java.util.Enumeration 使用的时候需要注意:和java.util.Iterator一样,目前只是暂时支持,关注的问题是枚举不能重置。如果一个未初始化的枚举被放进了容器,并且在多个foreach()语句中使用,如果第一个foreach()失败了,后面的都会被阻塞,因为枚举不会重启。
• 任何拥有public Iterator iterator()方法的公有类永远不会返回null。Velocity会把寻找一个iterator()方法作为最后的手段。这提供了很大的灵活性,也自动支持Java 1.5中的java.util.Iterable接口。
对于Iterator和Enumeration,推荐只有在万不得已的情况下才把它们放进容器,你也应该尽可能地让Velocity找到合适的、可复用的迭代接口。
虽然有充足的理由直接使用java.util.Iterator接口(比如像JDBC这样的大数据集),但是如果能够避免,用其他的可能会更好。“直接”是说像下面这样:
Vector v = new Vector();
v.addElement("Hello");
v.addElement("There");
context.put("words", v.iterator() );
当迭代器本身被放进了容器。当然,你也可以简单地这样做:
context.put("words", v );
两种方式都可以:Velocity能够识别实现了Collection(像List)接口的Vector,并因此找到iterator()方法,在它每次需要的时候调用iterator()来刷新迭代器。一旦Velocity在foreach()中使用了一个空迭代器(先跳过这里…),Velocity没办法为它正在使用的下一个foreach()获取一个新的迭代器。结果是后面任何使用空迭代器引用的foreach()都会阻塞,并且没有输出。
上面这些并不是为了表明在Velocity中遍历集合需要十分小心。恰恰相反,一般来说,只要在你向容器中添加迭代器时小心就可以了。
对静态类的支持
并非所有的类都可以实例化。像java.lang.Math这样的类不提供任何公有的构造函数,但是它包含了有用的静态方法。为了从模板中访问这些静态方法,你可以简单地把这些类自身添加到容器中:
context.put("Math", Math.class);
这样你就可以在模板中用$Math引用调用java.lang.Math中的任何公有静态方法。
容器链
Velocity容器设计的一大创新特性是容器链的概念。有时也被称为contextwrapping,这个高级特性允许你以一种方式把独立的容器连接在一起,使它们对模板来说像一个连续的容器。
最好用一个例子来说明:
VelocityContext context1 = new VelocityContext();
context1.put("name","Velocity");
context1.put("project", "Jakarta");
context1.put("duplicate", "I am in context1");
VelocityContext context2 = new VelocityContext( context1 );
context2.put("lang", "Java" );
context2.put("duplicate", "I am in context2");
template.merge( context2, writer );
在上面这段代码中,我们创建context2,使它连接context1。这意味着在模板中,你可以访问这两个VelocityContext对象中放置的任意项,只要在添加对象的时候没有重复的键。如果遇到这种情况,就像上面的键”duplicate”,最近的容器中存储的对象可以被访问。在上面这个例子中,返回的对象是字符串”I am in context2″。
注意,这种容器中对象的重复或者“覆盖”,不会以任何方式损坏或者修改被覆盖的对象。所以在上面的例子中, 字符串”I am in context1″还存在并且完好无损,仍然可以通过context1.get(“duplicate”)访问。但是在上面的例子中,模板中引用”$duplicate”的值会是”I am in context2″,模板不能访问被覆盖的字符串”I am in context1″。
也要注意,当你使用模板向一个渲染之后再检查的容器中添加信息的时候,你必须要小心。在一个模板中,通过set()语句改变容器只会影响外层的容器。所以在期望模板中的数据已经被添加到内部容器时,请确保你没有丢弃外层的容器。
这个特性有很多用途,目前最常用的就是提供层次数据访问和工具箱。
就像前面提到过的,Velocity容器机制是可以扩展的,但是超出了本指南的当前范围。如果你有兴趣,可以查看包org.apache.velocity.context中的类,看看提供的容器怎么组合在一起。更进一步,有几个例子在发行版的examples/context_example目录下,它们展示了可能的实现,包括一个使用数据库作为后台存储的例子。
请注意,这些例子不被支持,它们仅仅是出于示范和教学目的。
模板中创建的对象
在模板中,有两种情况Java代码必须处理运行时创建的对象:
当模板设计者通过Java代码调用容器中放置的对象的方法:
#set($myarr = [“a”,”b”,”c”] )
$foo.bar( $myarr )
当模板向容器中添加对象,Java代码可以在合并过程完成后访问这些对象:
#set($myarr = [“a”,”b”,”c”] )
#set( $foo = 1 )
#set( $bar = “bar”)
如果非常直接地处理这些情况,有几个事情需要知道:
• 当放在容器中或者传给方法时,VTL RangeOperator [ 1..10 ]和ObjectArray [“a”,”b”]是java.util.ArrayList对象。所以,当你设计接受模板中创建的数组的方法时,你应该牢记这个问题。
• VTL Map引用毫无疑问是用java.util.Map存储的。
• 小数在容器中会是Doubles或者BigDecimals,整数会是 Integer, Long,或者BigIntegers字符串当然还是Strings。
• Velocity在调用方法时会适当地省略参数,所以调用setFoo( int i )把一个整数放入容器,#set()和它等价。
其他容器问题
VelocityContext(或者任何派生自AbstractContext的Context)提供的一个特性是节点特有的自我监视缓存。一般来说,作为一个开发者,当你使用VelocityContext作为你的容器时,不必担心这些。但是,有一个目前已知的使用模式,你必须知道这个特性。
当VelocityContex访问模板中的节点时,它会收集关于这些有序节点的自我监视信息。所以,在下面这几种情况:
• 你使用同一个VelocityContext对象遍历同样的模板。
• 模板缓存关闭。
• 在每次迭代中,你通过getTemplate()请求模板。
你的VelocityContext有可能出现内存泄露(在收集更多自我监视信息时真会出现)。真正发生的是,它为每个它访问的模板收集模板节点的自我监视信息,当模板缓存关闭时,对VelocityContext来说它每次都是访问一个新模板。因此,它收集更多的自我监视信息,并且一直增长。强烈推荐你做以下一条或者更多条:
• 通过模板补偿处理,每次遍历都新建一个VelocityContext。这样就不会收集自我监视缓存数据。在你由于VelocityContext已经填充了数据或对象而想重用它的情况下,你可以简单地把填充好的VelocityContext包装成另一个VelocityContext,外层的那个会收集自我监视信息,你直接丢弃就可以了。例如 VelocityContext useThis = new VelocityContext( populatedVC );它可以很好地工作,因为外层的容器会存储自我监视缓存数据,它可以从内部容器获取任何请求的数据(当它为空时)。你仍然要注意,如果你的模板把数据放入容器并且期待后面的遍历使用这些数据,你需要做另外一个准备,因为任何模板set()语句会被存储在最外层的容器。看“Context chaining”章节中的讨论来获取更多信息。
• 打开模板缓存。这样可以避免在每次遍历中重复解析模板,因此VelocityContext不仅可以避免增加自我监视的缓存信息,还可以带来性能的提升。
• 在循环遍历的过程中重用模板对象。如果缓存关闭了,你不必让Velocity一遍又一遍地重复读取和解析相同的模板,所以VelocityContext就不会每次都收集新的自我监视信息。