Fork me on GitHub

模板引擎

  • 简洁友好的模板语法
  • 精益求精的性能优化
  • 高质量的设计与实现

配置

HTTL所有配置都有合理缺省值,你可以不做任何配置,即可在常见场景有良好表现。当然,因为业务场景千变万化,你可以通过配置,使HTTL能更好的贴近你的业务,和保持内部的使用习惯。

配置加载顺序

HTTL会顺序加载以下配置内容:

  1. httl-default.properties(HTTL缺省)
  2. httl-<mode>.properties(模式配置文件,可以有多个)
  3. httl.properties(应用提供的配置文件)
  4. JVM -D选项(只会读取以httl.开头键值,读入的键值会把键名的httl.去掉)
  5. OS环境变量(只会读取以httl_开头键值,读入的键值会把键名的httl_去掉,并将下划线换成点号)

这5项配置中,后面的优先生效。即后加载配置内容中配置项的值会更新之前加载的,或者说前加载的作为缺省值。

如果使用缺省值,可以不用配置,缺省值参见: httl-default.properties

MVC集成 会加载:/WEB-INF/httl.properties

模式配置加载

模式的作用是用于选择性加载HTTL的配置文件。可以设置多个值(或说成是开启了多个模式)。

模式中设置了develop(开启develop模式),HTTL会对应加载httl-develop.properties。模式名根据自己需要取成有实际意义的。

有了模式可以方便配置一组配置值,并可以灵活切换,以适应不同场景,比如:开发,测试,预发,生产等。

要调整模式可以在应用配置文件httl.properties中加上:

modes+=develop

并添加httl-develop.properties即可。

已内置debug模式配置,用户可直接开启debug模式:

modes+=debug

debug模式将加载配置: httl-debug.properties

配置格式惯例

配置中:

  1. 以复数命名的配置项,表示可以填多个值,用逗号分隔。
  2. =表示覆盖上一级或缺省配置。
  3. +=表示在上一级或缺省配置值的前面插入值(多值配置使用,注意:值插在前面,而不是追加在后面)
  4. -=表示在上一级或缺省配置值上删除值(多值配置使用,如果上一级配置如没有此值,则忽略)

当测试人员或运维人员,在无法修改代码时,可以使用JVM -D选项或OS环境变量,来修改HTTL配置的值。

使用JVM -D选项配置HTTL时注意

  1. 键名要加上httl.前缀。

使用JVM -D选项配置HTTL的示例:

$ java -Dhttl.reloadable=true -Dhttl.modes+=debug

Linux下Bash中使用环境变量配置HTTL时注意

  1. 键名要加上httl_前缀。
  2. 环境变量名不支持使用点号,改用下划线。
  3. +=-=设置环境变量会出错,改用=+=-

Bash中使用环境变量配置HTTL的示例:

$ export httl_reloadable=true
$ export httl_cache_capacity=1024
$ export httl_modes=+debug

配置一览表

属性值配置
配置项 配置说明 示例值
modes 配置模式 debug
import.packages 领域模型包导入 com.foo.domain
import.variables 公共变量导入 HttpServletRequest request
import.macros 公共宏导入 commons_macros.httl
import.methods 静态方法导入 java.lang.Math
import.getters 属性访问方法 get
import.sizers 大小访问方法 size
import.sequences 双点号序列 Mon Tue Wed Thu Fri Sat Sun Mon
forbid.methods 双点号序列 add,remove,clear
template.directory 模板文件目录 /WEB-INF/templates
template.suffix 模板文件后缀 .httl
attribute.namespace 属性语法名称空间 httl
cache.capacity 模板缓存容量 10000
reloadable 是否热加载 false
precompiled 是否预编译 false
source.in.class 源代码嵌入字节码 false
text.in.class 模板文本嵌入字节码 false
remove.directive.blank.line 移除指令空白行 true
code.directory 编译前Java代码保存目录 /log/java
compile.directory 字节码编译目录 /log/classes
compile.version 生成字节码版本 1.7
lint.unchecked 生成编译出错详细 false
dump.directory dump上下文数据目录 /tmp/dumps
dump.codec dump数据格式 $json.codec
dump.once 是否只dump一次 false
dump.override dump文件是否覆盖 false
for.variable 迭代状态变量名 for
filter.variable 过滤器变量名 filter
default.variable.type 默认变量类型 java.lang.Object
use.render.variable.type 以第一次渲染的变量类型编译 true
extends.directory 被继承模板目录 layouts
extends.variable 自动继承变量所指模板 layout
extends.default 自动继承默认模板 default.httl
extends.nested 自动继承嵌入子模板名称 nested
input.encoding 资源加载编码 UTF-8
output.encoding 模板输出编码 UTF-8
output.stream 是否使用二进制输出 true
output.writer 是否使用文本输出 true
message.directory 国际化文件目录 /WEB-INF
message.basename 国际化文件名前缀 messages
message.suffix 国际化文件后缀 .properties
message.format 国际化信息格式 message
message.encoding 国际化信息加载编码 UTF-8
localized 是否传入地区信息 false
locale 缺省地区信息 zh_CN
time.zone 缺省时区 +8
date.format 日期格式 yyyy-MM-dd HH:mm:ss
number.format 数字格式 ###,##0.###
null.value 空值输出内容 null
true.value True值输出内容 true
false.value False值输出内容 false
logger.level 日志输出级别 DEBUG
扩展点配置
配置项 配置说明 示例值
engine 引擎实现 httl.spi.engines.DefaultEngine
template.parser 模板语法解析器 httl.spi.parsers.TemplateParser
expression.parser 表达式语法解析器 httl.spi.parsers.ExpressionParser
translator 表达式翻译器 httl.spi.translators.CompiledTranslator
compiler 字节码编译器 httl.spi.compilers.JdkCompiler
loaders 资源加载器 httl.spi.loaders.ClasspathLoader
formatters 动态值格式化器 httl.spi.formatters.DateFormatter
value.switchers 动态值位置切换器 httl.spi.switchers.ScriptValueSwitcher
value.filters html动态值过滤器 httl.spi.filters.EscapeXmlFilter
script.value.filters js动态值过滤器 httl.spi.filters.EscapeStringFilter
style.value.filters css动态值过滤器 httl.spi.filters.EscapeStringFilter
text.switchers 静态文本位置切换器 httl.spi.switchers.ScriptTextSwitcher
text.filters html静态文本过滤器 httl.spi.filters.ClearBlankLineFilter
script.text.filters js静态文本过滤器 httl.spi.filters.ClearBlankLineFilter
style.text.filters css静态文本过滤器 httl.spi.filters.ClearBlankLineFilter
cache 模板缓存 java.util.concurrent.ConcurrentHashMap
resolvers 环境变量决策器 httl.spi.resolvers.EngineResolver
map.converter 渲染参数转换器 httl.spi.converters.BeanMapConverter
out.converter 渲染输出转换器 httl.spi.converters.ResponseOutConverter
codecs 对象转码器列表 httl.spi.codecs.FastjsonCodec
default.codec 缺省对象转码器 httl.spi.codecs.FastjsonCodec
loggers 日志输出 httl.spi.loggers.Log4jLogger

模板引擎配置

缺省使用注释语法:(缺省值不用配)

engine=httl.spi.engines.DefaultEngine
translator=httl.spi.translators.CompiledTranslator
template.parser=httl.spi.parsers.TemplateParser
expression.parser=httl.spi.parsers.ExpressionParser

其中,engine负责组装,parser负责解析语法树,translator负责将语法树转成模板实例。 除非你想改变语法,或优化解析性能,否则此三项不需要配置。

你可以设置for状态的变量名,缺省为for:(缺省值不用配)

for.variable=for

模板缓存配置

缺省为强缓存,即所有模板和表达式加载后全部缓存:(缺省值不用配)

cache=httl.spi.caches.TemplateAdaptiveCache
cache.capacity=

当配置的capacity大于0,AdaptiveCache将适配到LRU(最近最少使用)实现,否则将适配到全缓存实现。

如果你的模板非常之多,内存不足以缓存所有模板,可以配置缓存容量:(将自动启用LRU丢弃策略)

cache.capacity=10000

开发阶段,可以开启热加载:(将根据文件的最后修改时间自动清理缓存)

reloadable=true

模板加载配置

(1) 可以配置模板的根目录:

如果你配置了:

template.directory=/META-INF/templates
template.suffix=.httl

下面的写法,将查找的是/META-INF/templates/foo.httl模板:

engine.getTemplate("/foo.httl");

注意:此目录和template.suffix关联,如果查找其它后缀的文件,将不会带上此目录。

比如下面的写法,将查找的是/foo.txt文件:(不会带上模板目录)

engine.getResource("/foo.txt");

(2) 可以配置模板加载缺省编码,缺省为UTF-8:(缺省值不用配)

input.encoding=UTF-8

(3) 可以配置是否允许热加载,缺省为false:(缺省值不用配)

reloadable=false

开启热加载后,模板引擎在getTemplate()时会检查文件的lastModified时间,如果比上次加载的时间新,就重新加载。

请注意:旧的模板不会被卸载,经常改文件会导致内存perm区越来越大,只能在开发阶段使用。

(4) 可以配置启动时预编译所有模板,会调用loader.list()扫描模板文件。

在template.directory目录下搜索,并通过template.suffix后缀过滤模板文件。

预编译缺省关闭:(缺省值不用配)

precompiled=false
template.suffix=.httl

从Classpath下加载

缺省从Classpath下加载,即模板放在任意jar包中:(缺省值不用配)

loaders=httl.spi.loaders.ClasspathLoader
template.directory=

从文件加载

loaders=httl.spi.loaders.FileLoader
template.directory=/home/admin/templates

从jar包中加载

loaders=httl.spi.loaders.JarLoader
template.directory=/home/admin/tempaltes.jar

从zip包中加载

loaders=httl.spi.loaders.ZipLoader
template.directory=/home/admin/tempaltes.zip

从指定url加载

loaders=httl.spi.loaders.UrlLoader
template.directory=http://myhost/tempaltes

从war包加载

loaders=httl.spi.loaders.ServletLoader
template.directory=/WEB-INF/templates

需在web.xml中配置ServletLoader的listener:

<listener>
    <listener-class>httl.spi.loaders.ServletLoader</listener-class>
</listener>

从内存字符串加载

loaders=httl.spi.loaders.StringLoader

然后编码加入模板内容:

StringLoader.add("foo.httl","#set(User user)${user.name}");

从多个源加载

loaders=httl.spi.loaders.ClasspathLoader,httl.spi.loaders.FileLoader

或者使用"+="保留缺省的classpath加载的同时,增加新的加载器,多个值用逗号分隔:

loaders+=httl.spi.loaders.FileLoader

模板编译器

用于将模板类编译成字节码,缺省使用根据JDK版本自适应编译器:(缺省值不用配)

compiler=httl.spi.compilers.AdaptiveCompiler

当前运行环境为JDK1.6以前版本时,AdaptiveCompiler将适配到JavassistCompiler,否则将适配到JdkCompiler。

你可以强制指定使用jdk自带的编译器:(必需要用JDK运行,JRE不行,JDK比JRE多编译工具包)

compiler=httl.spi.compilers.JdkCompiler

你也可以换成javassist编译:(如果为JRE运行,请使用javassist编译)

compiler=httl.spi.compilers.JavassistCompiler

当然,也就需要增加javassist的jar包依赖:

javassist-3.15.0-GA.jar

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.15.0-GA</version>
</dependency>

(1) 输出选项:

如果设置output.stream=true,在编译期就会将模板文件转换成byte[]数据, 在输出时,直接向OutputStream中输出byte[]流,以免运行期每次输出都要转一次。

缺省全部开启:(缺省值不用配)

output.stream=true
output.writer=true

如果output.stream和output.writer同时开启,每份模板将编译成两份class,并返回自适应Template代理类。 当用户调用template.render(Map,OutputStream)时,实际执行输出byte[]的Template类, 当用户调用template.render(Map,Writer)时,实际执行输出String的Template类。

如果output.stream和output.writer同时关闭,只生成writer模板,相当于只开启output.writer。

注意: 如果只开启output.stream=true,必须用template.render(Map,OutputStream),否则数据转换会导致性能更低。 如果只开启output.writer=true,必须用template.render(Map,Writer),否则数据转换会导致性能更低。

如果你从来不传入OutputStream或Writer,请关闭相应配置开关,减少编译开销。

(2) 内存选项:

缺省将模板源码和模板文本都不编译到字节码中:(缺省值不用配)

source.in.class=false
text.in.class=false

它通过一个Map缓存做中介,把模板源码和模板文本都放到了runtime属性中,以节省内存perm区大小。

编译到字节码中,在小模板时,可能会稍快,但不会有质的飞跃,在模板大于8K时,会导致JVM的JIT优化失效,会更慢,不建议改变此选项。

(3) 版本选项:

java版本的配置会影响字节码生成的版本。(缺省为当前JDK版本)

compile.version=1.6

(4) 调试选项:

如果你想知道编译后的字节码是什么样的,可以设置编译目录,目录必须预先创建,编译器会向该目录输出.class文件。

编译前java代码保存目录目录缺省关闭:(缺省值不用配)

code.directory=

编译后class保存目录缺省关闭:(缺省值不用配)

compile.directory=

(5) 出错选项:

如果模板编译失败,你可以开启lint.unchecked选项来获取更详细的编译出错信息。

但该选项耗损更多内存,只在开发时使用,缺省关闭。(缺省值不用配)

lint.unchecked=false

输出格式化器

缺省加载了日期格式化器:(缺省值不用配)

formatters=httl.spi.formatters.DateFormatter
date.format=yyyy-MM-dd HH:mm:ss
time.zone=

你也可以设置时区,设置后,格式化的结果会带上时区的值:(缺省为当前系统时区)

time.zone=+8

你可以使用"+="保留缺省的日期格式化器的同时,增加新的格式化器,多个用逗号分隔:

formatters+=httl.spi.formatters.NumberFormatter
number.format=###,##0.###

你还可以设置null,true,false值的输出,

null值缺省会输出空白,true,false原样输出:(缺省值不用配)

null.value=
true.value=true
false.value=false

比如可以配为:

null.value=N/A
true.value=yes
false.value=no

输出过滤器

过滤器分为两类,一类是针对模板文本的,一类是针对动态插值的。

模板文本过滤会在编译的时候执行,编译时即把模板文本替换掉,不影响输出时的性能。

动态插值的过滤会在输出的时候执行,需小心过滤引起性能问题,多个用逗号分隔。

缺省加载了动态插值HTML过滤,防止HTML注入XSS攻击:(缺省值不用配)

value.filters=httl.spi.filters.EscapeXmlFilter
text.filters=

你可以配置在编译时将静态文本中的空白行删除:(编译时执行,不影响渲染速度)

text.filters=httl.spi.filters.ClearBlankLineFilter

你也可以配置在编译时将静态文本中的连续空白符压缩成单个空格:(编译时执行,不影响渲染速度)

text.filters=httl.spi.filters.CompressBlankFilter

你也可以用filters同时设置value.filters和text.filters:

filters=httl.spi.filters.CompressBlankFilter

等价于:

value.filters+=httl.spi.filters.CompressBlankFilter
text.filters+=httl.spi.filters.CompressBlankFilter

HTTL缺省会移除指令所在行空白,如果需要保留,请配置:

remove.directive.blank.line=false

类型导入

导入包名

缺省导入了java.util包:(缺省值不用配)

import.packages=java.util

这样你就可以在模板内用短类名,而不用带上全包名。

你可以使用"+="保留缺省导入包的同时,导入新的包,多个包用逗号分隔:

import.packages+=com.foo,com.bar

导入变量类型声明

缺省导入了parent,context,template,engine四个变量:(缺省值不用配)

import.variables=Context parent,Template super,Template this,Engine engine

如果使用的是HTTL内置的MVC集成,在集成中已缺省导入request,response,session,application四个变量:(缺省值不用配)

import.variables+=HttpServletRequest request,HttpServletResponse response,HttpSession session,ServletContext application

这样你就可以在模板内直接使用这些变量,而不用每个模板都声明:

#set(HttpServletRequest request, HttpServletResponse response)

你可以使用"+="保留缺省导入包的同时,导入新的包,多个包用逗号分隔:

import.variables+=Foo foo,Bar bar

导入方法

缺省导入DefaultMethod类中的方法:(缺省值不用配)

import.methods=httl.spi.methods.DefaultMethod

你可以使用"+="保留缺省方法的同时,导入新的方法,多个类用逗号分隔:

import.methods+=com.foo.MyMethod

比如DefaultMethod有静态方法:(也可以是非静态方法)

public static String format(Date self, String format) {
    return ...;
}
public static char toChar(String self) {
    return ...;
}
public static Date now() {
    return ...;
}

则可以在模板中调用:

${date.format("yyyy-MM-dd")}
${str.toChar()}
${now()}

原理:静态方法的第一个参数传入被操作者本身的引用,后面的参数调用时传入。

如果你需要setEngine()注入引擎实例,或setXxx(String)注入配置项,可以导入非静态方法,但静态方法编译后更快。

比如:

// 将注入引擎本身
private Engine engine;
public setEngine(Engine engine) {
    this.engine = engine;
}

// 将注入httl.properties中input.encoding=UTF-8配置的值
private String inputEncoding;
public setInputEncoding(String inputEncoding) {
    this.inputEncoding = inputEncoding;
}

// 非静态方法导入
public String include(String templateName) {
    return engine.getTemplate(templateName, inputEncoding).toString();
}

使用方式一样:

${include("foo.httl")}

禁止方法

HTTL不允许调用void方法,保持模板的无副作用,以及多次渲染的幂等性。

如果有一些写方法有返回值,可以通过forbid.methods配置禁止,防止开发人员误调用。

缺省禁止了一些常见写方法:(缺省值不用配)

forbid.methods=add,put,save,insert,modify,update,delete,remove,clear

导入属性获取器

缺省导入了get,getProperty,getAttribute四个属性取值方法:(缺省值不用配)

import.getters=get,getProperty,getAttribute

将查找String和Object参数的属性取值方法,如:get(String)或get(Object)

导入大小获取器

缺省导入了size,length,getSize,getLength四个大小取值方法:(缺省值不用配)

import.sizers=size,length,getSize,getLength

将查找空参数的大小取值方法,如:size()

大小获取器,将用于#if()判断,${a ? b : c}三元操作符判断等判断空值的地方。

导入宏

导指定模板中的所有宏,多个模板用逗号分隔,这些宏可在其它任意模板中使用,如:

import.macros=common_macros.httl

导入序列

用法如:

#for(weekday :"Monday" .."Sunday")
${weekday}
#end

缺省包含星期和月份序列:(缺省值不用配)

import.sequences=Mon Tue Wed Thu Fri Sat Sun Mon,\
Monday Tuesday Wednesday Thursday Friday Saturday Sunday Monday,\
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec Jan,\
January February March May June July August September October November December January

如果最前和最后一个值相同,表示可以循环。

你可以使用"+="保留缺省序列的同时,增加新的序列,序列中的值用空格分隔,多个序列用逗号分隔:

import.sequences+=金 木 水 火 土 金

日志输出配置

缺省使用log4j输出日志:(缺省值不用配)

loggers=httl.spi.loggers.Log4jLogger

如果你需要使用其它日志工具输出日志,可配置自己的适配器。

比如使用JDK的日志输出:

loggers=httl.spi.loggers.JdkLogger

你也可以同时输出到多个日志工具:

loggers=httl.spi.loggers.Log4jLogger,httl.spi.loggers.JdkLogger

或者使用"+="保留缺省的log4j输出的同时,增加新的输出,多个用逗号分隔:

loggers+=httl.spi.loggers.JdkLogger

注:当日志级别为DEBUG时,HTTL将向日志中输出编译的模板源码,方便排错。

变量决策

变量决策即从哪里获取变量,缺省只从上下文中获取属性:(缺省值不用配)

resolvers=httl.spi.resolvers.ContextResolver

如果你使用HTTL内置的MVC集成,已经自动导入ServletResolver,

它会读取requestsessionservletContext等中获取变量:

resolvers+=httl.spi.resolvers.ServletResolver

比如:用户的locale区域国际化信息,换上面的配置先从请求中获取,这样就可以根据用户区域设置显示不同语言。

如果你想在模板中直接都读取到httl.properties配置项即Engine.getProperty()中的值,可以配置:

resolvers+=httl.spi.resolvers.EngineResolver

如果你想在模板中直接都读取到JVM启动参数java -Dkey=value即System.getProperty()中的值,可以配置:

resolvers+=httl.spi.resolvers.SystemResolver

如果你想在模板中直接都读取到环境变量export key=value即System.getenv()中的值,可以配置:

resolvers+=httl.spi.resolvers.EnvironmentResolver

如果你想全局设置变量值,可以配置:

resolvers+=httl.spi.resolvers.GlobalResovler

然后,通过静态方法全局设置变量值:

GlobalResovler.put(key, value); // static