在07年的时候,写过一个模板引擎,当时叫CommonTemplate,以前JavaEye有个开源系列介绍:
http://www.iteye.com/news/3381,后来功能越来越多,性能却越来越差,在金大为发给我
性能对比结果后,看到惨不忍睹的差距,就打算抛弃原设计进行重写,但因工作忙,就搁置了,最近看温少发了几个EL和JSON的解析器,有点手痒,就抽了个周未,拿出来再改了改,主要将模板改成了字节码编译,并简化了语法及缩小使用范围,只针对HTML场景使用,并将名称改成了HTTL,名字含义是把HTML中的M(Marker)改成了T(Template),放在GoogleCode上:
http://code.google.com/p/httl,性能和Java硬编码输出模板内容差不多,比Velocity/FreeMarker等快10倍左右。
语法方面的区别:
发现基于文本指令的,基于HTML标签的,基于HTML注释的,都有不少模板引擎实现,
为了标新立异以及使用的直观性,HTTL采用基于HTML属性的指令,如:
<table if="user.role == 'admin'">
<tr foreach="book in books">
<td>${book.title}</td>
</tr>
</table>
选型方面的区别:
Velocity采用JavaCC编译成AST树,解释执行。
FreeMarker类似,只是采用FreeCC。
Smarty4j采用ASM生成字节码,对条件等字节码的生成比JDK稍逊,而且需在运行时判断context变量的类型,无法强类型编译模板。
HTTL采用先将模板转译成Java代码,再由JDK或Javassist编译成字节码,并在模板上声明传入类型,在编译期就推演所有变量类型,减少反射和运行时类型判断。
部分优化策略示例:
(1) 强类型编译
对于表达式${user.name}的编译:
Smarty4j弱类型字节码生成:
Object user = context.get("user"); // 无法确定user是Map还是POJO
// 反射获取属性的值,而且要运行期判断是user.getName(),还是user.name字段
Object name = ReflectUtil.get(user, "name"); // 接下来name也要反射
Httl强类型字节码生成:
User user = (User)context.get("user"); // 通过in="User user"声明类型
String name = user.getName(); // 编译期通过User的字段类型推演name的类型,并在编译期决定使用getName()
如果只是编译成弱类型字节码,性能比解释执行快不了多少,淘宝编译Velocity的测试数据显示,只能比JavaCC的AST解释执行快10%左右,参见附件的PPT。(附件的PPT是蒋江涛在InfoQ大会的演讲稿,是公开的)
(2) 对大模板拆分子函数:(未发布)
SunJDK缺省对大于5K字节码的方法不进行JIT优化,
所以当模板的内容较大时,会导致生成的字节码也比较大,
通过拆分子函数,可以解决JIT优化问题。
淘宝编译Velocity的测试数据显示,大模板拆分成小模板性能提升35%,参见附件的PPT。
(3) 编译时就将文本编译成字节,加快输出:
原编译:
writer.write("<table><tr><td>");
writer.write(user.getName());
改为编译成:
output.write( new byte[] {60, 116, 97, 98, 108, 101, 62, 60, 116, 114, 62, 60, 116, 100, 62};);
output.write(user.getName().getBytes());
淘宝编译Velocity的测试数据显示,将String输出预编译为byte[]输出,性能提升50%,参见附件的PPT。
(4) 对同条件if语句优化:(未发布)
if (user.role == "admin") {
// ...
} else if (user.role == "member") {
// ...
} else {
// ...
}
优化后:
int id = System.identityHashCode(user.role);
switch (id) {
case 3452345: // 编译时计算"admin"的identityHashCode
// ...
case 2342452: // 编译时计算"member"的identityHashCode
// ...
default:
// ...
}
(5) 对于赋值生成的price为局部变量,不put回context:
比如:set="price = price * discount / 100"编译:
int price = price * discount / 100;
除非声明out="price",才在模板渲染最后:
context.put("price", price);
(6) 减少int到Integer等元类型的boxed和unboxed,以及instanceof。
因为模板输出的大量是基本类型和字符串,
比如当输出基本类型时,需要转成String,如果使用format(Object)接口,就会将基本类型装箱,
Httl遇到任何类似需要boxed和unboxed的地方,都会重载所有基本类型方法,以减少boxed和unboxed的处理。
出现instanceof的地方也一样,会尽量多态处理。
(7) 所有编译过程都会保持和计算源码位置,
当出错时,错误信息能准确定位到出错行列。
等等。
性能测试:
模板内循环显示100行数据。
每模板各运行一万次。
模板大小约800字符。
模板每次运行输出内容约27K字符。
测试类:PerformanceTest.java
测试结果:
测试代码:
http://code.google.com/p/httl/downloads/list
更多信息参见:
http://code.google.com/p/httl
HTTL缺省使用Jdk的javax,tools编译字节码,需要500ms左右,如果换成Javassist编译,编译时间可以降到200ms左右,但字节码执行效率略差一点,但每个模板只编译一次,所以编译慢点也能忍受,如果想换成Javassist,只需在httl.properties中加入:
compiler=com.googlecode.httl.support.compilers.JavassistCompiler
java.version=1.4
注:Javassist不支持1.5的语法,所以要设置java.version=1.4