编写自定义的 Velocity 指令
Velocity 允许您对指令系统进行扩展,在 Velocity 引擎初始化的时候会加载系统内置指令和用户的自定义指令。系统的内置指令已经在 Velocity 的 Jar 包中的 directive.properties 文件中定义,不建议直接修改该文件。而自定义的指令要求用户在 velocity.properties 文件中定义的,例如:userdirective=net.oschina.toolbox.CacheDirective。如果是多个自定义指令则使用逗号隔开。
所有的自定义指令要求扩展 org.apache.velocity.runtime.directive.Directive 这个类。为了更加形象直观的表现 Velocity 自定义指令的优点,接下来我们将以一个实际的应用场景进行讲解。
在该应用场景中,所有的页面请求直接指向 vm 文件,中间没经过任何的控制器。数据是通过 Velocity 的 toolbox 直接读取并显示在页面上。如果数据是来自数据库的,而且访问量非常大的时候,我们就需要对这些数据进行缓存以便快速响应用户请求和降低系统负载。一种方法是直接在 toolbox 的读取数据的方法中进行数据的缓存;另外一种就是我们接下来要介绍的,通过编写自定义的缓存指令来缓存页面上的某个 HTML 片段。
首先我们定义一个这样的块指令:#cache( “ CacheRegion ” , ” Key ” ) ,其中第一个参数为缓存区域、第二个参数为对应缓存数据的键值。该指令自动将包含在指令内部的脚本执行后的结构缓存起来,当第一次请求时检查缓存中是否存在此 HTML 片段数据,如果存在就直接输出到页面,否则执行块指令中的脚本,执行后的结果输出到页面同时保存到缓存中以便下次使用。使用方法如下所示:
- #cache("News","home")
- ## 读取数据库中最新新闻并显示
- <ul>
- #foreach($news in $NewsTool.ListTopNews(10))
- <li>
- <span class='date'>
- $date.format("yyyy-MM-dd",${news.pub_time})
- </span>
- <span class='title'>${news.title}</span>
- </li>
- #end
- </ul>
- #end
其中 $NewsTool.ListTopNews(10)
是用来从数据库中读取最新发布的 10 条新闻信息。
接下来我们来看 #cache
这个指令对应的源码:
- /**
- * Velocity模板上用于控制缓存的指令
- * @author Winter Lau
- * @date 2009-3-16 下午04:40:19
- */
- public class CacheDirective extends Directive {
- final static Hashtable<String,String> body_tpls = new Hashtable<String, String>();
- @Override
- public String getName() { return "cache"; } //指定指令的名称
- @Override
- public int getType() { return BLOCK; } //指定指令类型为块指令
- /* (non-Javadoc)
- * @see org.apache.velocity.runtime.directive.Directive#render()
- */
- @Override
- public boolean render(InternalContextAdapter context, Writer writer, Node node)
- throws IOException, ResourceNotFoundException, ParseErrorException,
- MethodInvocationException
- {
- //获得缓存信息
- SimpleNode sn_region = (SimpleNode) node.jjtGetChild(0);
- String region = (String)sn_region.value(context);
- SimpleNode sn_key = (SimpleNode) node.jjtGetChild(1);
- Serializable key = (Serializable)sn_key.value(context);
- Node body = node.jjtGetChild(2);
- //检查内容是否有变化
- String tpl_key = key+"@"+region;
- String body_tpl = body.literal();
- String old_body_tpl = body_tpls.get(tpl_key);
- String cache_html = CacheHelper.get(String.class, region, key);
- if(cache_html == null || !StringUtils.equals(body_tpl, old_body_tpl)){
- StringWriter sw = new StringWriter();
- body.render(context, sw);
- cache_html = sw.toString();
- CacheHelper.set(region, key, cache_html);
- body_tpls.put(tpl_key, body_tpl);
- }
- writer.write(cache_html);
- return true;
- }
- }
Directive 是所有指令的基类,Directive 是一个抽象类,它有三个方法必须实现的,分别是:
getName:返回指令的名称
getType:返回指令的类型,行指令:LINE、块指令:BLOCK
render:指令执行的入口
其中 render 方法的最后一个参数 node 表示为该指定对应在 Velocity 模板中的节点对象,通过调用 node 的 jjtGetChild 方法可以获取到传递给该指令的参数以及包含在该指令的脚本内容。
上面的代码中,首先获取传递给指令的参数,也就是缓存的区域名和对应缓存数据的键值。接着判断距上次数据被缓存时,指令所包含的脚本代码是否有更改(以便页面开发人员修改了 vm 脚本时自动刷新缓存数据),然后判断缓存中是否已有数据。当缓存中无数据或者页面代码被修改时,重新执行块指令中的脚本并将执行的结果置入缓存,否则直接将缓存中的数据输出到页面。
上述例子中,传递给 #cache 指令的参数也可以是某个变量,例如
#set($region = "news")
#set($key = "home")
#cache("CACHE_$region",$key)
如此,便以很小的代码侵入,来实现页面的缓存。