SpringBlade/doc/guide/beetl.md
2016-12-13 08:46:42 +08:00

126 KiB
Raw Blame History

Beetl2.7 中文文档

Beetl作者李家智 <xiandafu@126.com>

1. 什么是Beetl

Beetl目前版本是2.7.3,相对于其他java模板引擎具有功能齐全语法直观,性能超高,以及编写的模板容易维护等特点。使得开发和维护模板有很好的体验。是新一代的模板引擎。总得来说,它的特性如下:

  • 功能完备作为主流模板引擎Beetl具有相当多的功能和其他模板引擎不具备的功能。适用于各种应用场景从对响应速度有很高要求的大网站到功能繁多的CMS管理系统都适合。Beetl本身还具有很多独特功能来完成模板编写和维护这是其他模板引擎所不具有的。
  • 非常简单类似Javascript语法和习俗只要半小时就能通过半学半猜完全掌握用法。拒绝其他模板引擎那种非人性化的语法和习俗。同时也能支持html 标签使得开发CMS系统比较容易
  • 超高的性能Beetl 远超过主流java模板引擎性能(引擎性能5-6倍与freemaker2倍于JSP。参考附录而且消耗较低的CPU。
  • 易于整合Beetl能很容易的与各种web框架整合如Spring MVCJFinalStrutsNutzJoddServlet等。
  • 支持模板单独开发和测试即在MVC架构中即使没有M和C部分也能开发和测试模板。
  • 扩展和个性化Beetl支持自定义方法格式化函数虚拟属性标签和HTML标签. 同时Beetl也支持自定义占位符和控制语句起始符号也支持使用者完全可以打造适合自己的工具包。

关于性能

通过与主流模板引擎FreemarkerVecloity以及JSP对比Beetl6倍于Freemarker2倍于JSP。这是因为宏观上通过了优化的渲染引擎IO的二进制输出字节码属性访问增强微观上通过一维数组保存上下文Context,静态文本合并处理通过重复使用字节数组来防止java频繁的创建和销毁数组还使用模板缓存运行时优化等方法。详情参考附录

独特功能

Beetl有些功能是发展了10多年的模板引擎所不具备的这些功能非常利于模板的开发和维护如下

  1. 自定义占位符和控制语句起始符号这有利于减小模板语法对模板的倾入性比如在html模板中如果定义控制语句符号是<!--: -->,那么,大部分模板文件都能通过浏览器打开。有的使用者仅仅采用了单个符号@ (或者单个符号“”)以及回车换号作为控制语句起始符号,这又能提高开发效率
  2. 可单独测试的模板。无需真正的控制层和模型层Beetl的模板就可以单独开发和测试
  3. 同时支持较为松散的MVC和严格的MVC如果在模板语言里嵌入计算表达式复杂条件表达式以及函数调用有干涉业务逻辑嫌疑你可以禁止使用这些语法。
  4. 强大的安全输出通过安全输出符号能在模板变量变量属性引用for循环占位符输出try-catch中等各个地方提供安全输出保证渲染正常。
  5. 模板变量运行将模板的某一部分输出像js那样赋值给一个变量稍后再处理。利用模板变量能完成非常复杂的页面布局简单的布局可使用include,layout标签函数) 6. 类型推测能在运行的时候推测模板变量类型从而优化性能也可以通过注解的方法显示的说明模板变量属性这是非必须的但有助于IDE自动提示功能 7. 可插拔的设计,错误信息提示,模板引擎缓存机制,模板资源管理,本地调用的安全管理器,严格MVC限制模板引擎本身都有默认的实现但又完全可以自定义以适合特定需求 8. 增强的语法如for-elsefor, select-case安全输出符号!,省略的三元表达式 等,这些语法特别适合模板开发 9. 局部渲染技术结合现在js的ajax技术。 10. 性能超高,具有最快的模板解释引擎同时又有较低的CPU消耗。5-6倍于国内使用的Freemaker。适合各类模板应用如代码生成工具CMS系统普通网站超高访问量的门户系统和富客户端JS框架整合的后台管理应用

小白如何开始

  • 需要通读基本用法大部分都是讲解语法而语法跟js很接近所以可以快速预览但Beetl是针对模板设计 所以像安全输出标签和html标签全局变量临时变量和共享变量布局技术以及直接调用java代码等还需要认真读一遍。
  • 如果从事web开发还需要阅读web集成里的第一节“web提供的全局变量”如果web里还使用ajax技术可以阅读“整合ajax的局部渲染技术”。
  • 包含有spring,jfinal,jodd,struts 等demo可以作为参考学习用 http://ibeetl.com/community/?/article/4
  • 任何问题都可以在ibeetl.com 社区上提问。目前答复率是100%,提问需要详细说明自己的期望,出错信息,附上代码或者图片

联系作者

作者:闲.大赋 (李家智)等(参考附录查看代码贡献者)

QQ技术交流群219324263

邮件:xiandafu@126.com

Beetl社区ibeetl.com

源码主页:https://github.com/javamonkey/beetl2.0

在线体验和代码分享 http://ibeetl.com/beetlonline/

2. 基本用法

2.1. 安装

如果使用maven请使用如下坐标

<dependency>
	<groupId>com.ibeetl</groupId>
	<artifactId>beetl</artifactId>
	<version>2.7.3</version>
</dependency>

如果非maven工程直接下载http://git.oschina.net/xiandafu/beetl2.0/attach_files

2.2. 从GroupTemplate开始

StringTemplateResourceLoader resourceLoader = new StringTemplateResourceLoader();
Configuration cfg = Configuration.defaultConfiguration();
GroupTemplate gt = new GroupTemplate(resourceLoader, cfg);
Template t = gt.getTemplate("hello,${name}");
t.binding("name", "beetl");
String str = t.render();
System.out.println(str);

Beetl的核心是GroupTemplate创建GroupTemplate需要俩个参数一个是模板资源加载器一个是配置类模板资源加载器Beetl内置了6种分别是

  • StringTemplateResourceLoader字符串模板加载器用于加载字符串模板如本例所示
  • FileResourceLoader文件模板加载器需要一个根目录作为参数构造传入getTemplate方法的String是模板文件相对于Root目录的相对路径
  • ClasspathResourceLoader文件模板加载器模板文件位于Classpath里
  • WebAppResourceLoader用于webapp集成假定模板根目录就是WebRoot目录参考web集成章
  • MapResourceLoader : 可以动态存入模板
  • CompositeResourceLoader 混合使用多种加载方式

代码第5行将变量name传入模板里其值是“Beetl”。 代码第6行是渲染模板得到输出template提供了多种获得渲染输出的方法如下

  • tempalte.render() 返回渲染结果,如本例所示
  • template.renderTo(Writer) 渲染结果输出到Writer里
  • template.renderTo(OutputStream ) 渲染结果输出到OutputStream里
  1. 关于如何使用模板资源加载器,请参考下一节
  2. 如何对模板进行配置,请参考下一节

2.3. 模板基础配置

Beetl提供不但功能齐全而且还有很多独特功能通过简单的配置文件就可以定义众多的功能默认情况下Configuration类总是会先加载默认的配置文件位于/org/beetl/core/beetl-default.properties作为新手通常只需要关注3,4,5,6行定界符的配置以及12行模板字符集的配置就可以了,其他配置会在后面章节陆续提到)下,其内容片断如下:

#默认配置
ENGINE=org.beetl.core.engine.FastRuntimeEngine
DELIMITER_PLACEHOLDER_START=${
DELIMITER_PLACEHOLDER_END=}
DELIMITER_STATEMENT_START=<%
DELIMITER_STATEMENT_END= %>
DIRECT_BYTE_OUTPUT = FALSE
HTML_TAG_SUPPORT = true
HTML_TAG_FLAG = #
HTML_TAG_BINDING_ATTRIBUTE = var
NATIVE_CALL = TRUE
TEMPLATE_CHARSET = UTF-8
ERROR_HANDLER = org.beetl.core.ConsoleErrorHandler
NATIVE_SECUARTY_MANAGER= org.beetl.core.DefaultNativeSecurityManager
MVC_STRICT = FALSE

#资源配置resource后的属性只限于特定ResourceLoader
RESOURCE_LOADER=org.beetl.core.resource.ClasspathResourceLoader
#classpath 根路径
RESOURCE.root= /
#是否检测文件变化
RESOURCE.autoCheck= true
#自定义脚本方法文件的Root目录和后缀
RESOURCE.functionRoot = functions
RESOURCE.functionSuffix = html
#自定义标签文件Root目录和后缀
RESOURCE.tagRoot = htmltag
RESOURCE.tagSuffix = tag
#####  扩展 ##############
## 内置的方法
FN.date = org.beetl.ext.fn.DateFunction
......
##内置的功能包
FNP.strutil = org.beetl.ext.fn.StringUtil
......
##内置的默认格式化函数
FTC.java.util.Date = org.beetl.ext.format.DateFormat
.....
## 标签类
TAG.include= org.beetl.ext.tag.IncludeTag

第2行配置引擎实现类默认即可.

第3,4行指定了占位符号默认是${ },也可以指定为其他占位符。

第5,6行指定了语句的定界符号默认是<% %>,也可以指定为其他定界符号

第7行指定IO输出模式默认是FALSE,即通常的字符输出,在考虑高性能情况下,可以设置成true。详细请参考高级用法

第8,9行指定了支持HTML标签且符号为#,默认配置下,模板引擎识别<#tag ></#tag>这样的类似html标签并能调用相应的标签函数或者模板文件。你也可以指定别的符号bg: 则识别<bg:

第10行 指定如果标签属性有var,则认为是需要绑定变量给模板的标签函数

第11行指定允许本地Class直接调用

第12行指定模板字符集是UTF-8

第13行指定异常的解析类默认是ConsoleErrorHandler他将在render发生异常的时候在后台打印出错误信息System.out)。

第14行指定了本地Class调用的安全策略

第15行配置了是否进行严格MVC通常情况下此处设置为false.

第18行指定了默认使用的模板资源加载器

第20到22行配置了模板资源加载器的一些属性如设置根路径为/,即Classpath的顶级路径并且总是检测模板是否更改

第23行配置了自定义的方法所在的目录以及文件名后缀。beetl既支持通过java类定义方法也支持通过模板文件来定义方法

第26行配置了自定义的html标签所在的目录以及文件名后缀。beetl既支持通过java类定义标签也支持通过模板文件来定义标签

第31行注册了一个date方法,其实现类是org.beetl.ext.fn.DateFunction

第34行注册了一个方法包strutil,其实现类org.beetl.ext.fn.StringUtil,此类的每个public方法都将注册为beetl的方法

第37行注册了一个日期格式化函数

第40行注册了一个include标签函数

模板开发者可以创建一个beetl.properties的配置文件此时该配置文件将覆盖默认的配置文件属性比如你的定界符考虑是<!--:--> ,则在beetl.properties加入一行即可,并将此配置文件放入Classpath根目录下即可。 Configuration.defaultConfiguration()总是先加载系统默认的然后再加载Beetl.properties的配置属性如果有重复用后者代替前者的配置

#自定义配置
DELIMITER_STATEMENT_START=<!--:
DELIMITER_STATEMENT_END=-->

2.4.0 新功能:beetl 支持通过模板本生来完成函数即模板函数或者通过模板来实现HTML标签而不用写java代码可以beetl.properties为这种应用设置的不同的语句定界符来跟常规模板做区分如下

FUNCTION_TAG_LIMITER=<% ; %>

分号分割开如果配置文件没有FUNCTION_TAG_LIMITER=则模板函数html标签使用同DELIMITER_STATEMENT_STARTDELIMITER_STATEMENT_END

2.4. 模板资源加载器

资源加载器是根据String值获取Resource实例的工场类同时资源加载器还要负责响应模板引擎询问模板是否变化的调用。对于新手来说无需考虑模板资源加载器如何实现只需要根据自己场景选择系统提供的三类模板资源加载器即可

2.4.1. 字符串模板加载器

在创建GroupTemplate过程中如果传入的是StringTemplateResourceLoader则允许通过调用gt.getTemplate(String template)来获取模板实例对象如2.1所示

2.4.2. 文件资源模板加载器

更通常情况下模板资源是以文件形式管理的集中放在某一个文件目录下如webapp的模板根目录就可能是WEB-INF/template里因此可以使用FileResourceLoader来加载模板实例如下代码

String root = System.getProperty("user.dir")+File.separator+"template";
FileResourceLoader resourceLoader = new FileResourceLoader(root,"utf-8");
Configuration cfg = Configuration.defaultConfiguration();
GroupTemplate gt = new GroupTemplate(resourceLoader, cfg);
Template t = gt.getTemplate("/s01/hello.txt");
String str = t.render();
System.out.println(str);

第1行代码指定了模板根目录即位于项目工程下的template目录 第2行构造了一个资源加载器并指定字符集为UTF-8 (也可不指定因为配置文件默认就是UTF-8); 第5行通过模板的相对路径/s01/hello.txt来加载模板

2.4.3. Classpath资源模板加载器

还有种常情况下模板资源是打包到jar文件或者同Class放在一起因此可以使用ClasspathResourceLoader来加载模板实例如下代码

ClasspathResourceLoader resourceLoader = new ClasspathResourceLoader("org/beetl/sample/s01/");
Configuration cfg = Configuration.defaultConfiguration();
GroupTemplate gt = new GroupTemplate(resourceLoader, cfg);
Template t = gt.getTemplate("/hello.txt");
String str = t.render();
System.out.println(str);

第1行代码指定了模板根目录即搜索模板的时候从根目录开始如果new ClasspathResourceLoader("template/"),则表示搜索template下的模板。此处用空构造函数表示搜索路径是根路径且字符集默认字符集UTF-8.

第4行通过模板的相对路径org/beetl/sample/s01/hello.txt来加载模板

2.4.4. WebApp资源模板加载器

WebAppResourceLoader 是用于web应用的资源模板加载器默认根路径是WebRoot目录。也可以通过制定root属性来设置相对于WebRoot的的模板根路径从安全角考虑建议放到WEB-INF目录下

如下是Jfinal集成 里初始化GroupTemplate的方法

Configuration cfg = Configuration.defaultConfiguration();
WebAppResourceLoader resourceLoader = new WebAppResourceLoader();
groupTemplate = new GroupTemplate(resourceLoader, cfg);

WebAppResourceLoader 假定 beetl.jar 是位于 WEB-INF/lib 目录下因此可以通过WebAppResourceLoader类的路径来推断出WebRoot路径从而指定模板根路径。所有线上环境一般都是如此如果是开发环境或者其他环境不符合此假设你需要调用resourceLoader.setRoot() 来指定模板更路径

2.4.5. 自定义资源模板加载器

有时候模板可能来自文件系统不同目录或者模板一部分来自某个文件系统另外一部分来自数据库还有的情况模板可能是加密混淆的模板此时需要自定义资源加载继承ResouceLoader才能实现模板功能这部分请参考高级部分

2.5. 定界符与占位符号

Beetl模板语言类似JS语言和习俗只需要将Beetl语言放入定界符号里即可如默认的是<% %> ,占位符用于静态文本里嵌入占位符用于输出,如下是正确例子

<%
var a = 2;
var b = 3;
var result = a+b;
%>
hello 2+3=${result}

千万不要在定界符里使用占位符号,因为占位符仅仅嵌在静态文本里,如下例子是错误例子

<%
var a = "hi";
var c = ${a}+"beetl"; //应该是var c = a+"beetl"
%>

每次有人问我如上例子为啥不能运行的时候我总是有点憎恶velocity 带来的这种非人性语法

定界符和占位符 通常还有别的选择,如下定界符

  • @ 和回车换行 (此时,模板配置DELIMITER_STATEMENT_END= 或者 DELIMITER_STATEMENT_END=null 都可以)
  • #: 和回车换行
  • <!--: 和 -->
  • <!--# 和 -->
  • <? 和 ?>

占位符--#{ }-##

你也可以与团队达成一致意见来选择团队喜爱择定界符号和占位符号。

定界符号里是表达式,如果表达式跟定界符或者占位符有冲突,可以在用 “\” 符号,如

@for(user in users){
email is ${user.name}\@163.com
@}
${[1,2,3]} //输出一个json列表
${ {key:1,value:2 \}  } //输出一个json map} 需要加上\

2.6. 注释

Beetl语法类似js语法所以注释上也同js一样 单行注释采用//

多行注视采用/**/

<%
/*此处是一个定义变量*/
var a = 3; //定义一个变量.

/* 以下内容都将被注释
%>

<% */ %>

第2行是一个多行注释

第3行是一个单行注释

第5行到第8行采用的是多行注释因此里面有内容也是注释模板将不予处理

2.7. 临时变量定义

在模板中定义的变量成为临时变量这类似js中采用var 定义的变量,如下例子

<%
var a = 3;
var b = 3,c = "abc",d=true,e=null;
var f = [1,2,3];
var g = {key1:a,key2:c};
var i = a+b;
%>

2.8. 全局变量定义

全局变量是通过template.binding传入的变量,这些变量能在模板的任何一个地方包括子模板都能访问到。如java代码里

template.binding("list",service.getUserList());

//在模板里
<%
for(user in list){
%>
hello,${user.name};
<% } %>

2.9. 共享变量

共享变量指在所有模板中都可以引用的变量可过groupTemplate.setSharedVars(Map<String, Object> sharedVars)传入的变量,这些变量能在 所有模板 的任何一个地方

//.....
GroupTemplate gt = new GroupTemplate(resourceLoader, cfg);
Map<String,Object> shared = new HashMap<String,Object>();
shared.put("name", "beetl");
gt.setSharedVars(shared);
Template t = gt.getTemplate("/org/beetl/sample/s0208/t1.txt");
String str = t.render();
System.out.println(str);
t = gt.getTemplate("/org/beetl/sample/s0208/t2.txt");
str = t.render();
System.out.println(str);
//t1.txt
hi,${name}
//t2.txt
hello,${name}

2.10. 模板变量

模板变量是一种特殊的变量,即可以将模板中任何一段的输出赋值到该变量,并允许稍后在其他地方使用,如下代码

<%
var content = {
        var c = "1234";
        print(c);
%>
模板其他内容

<% }; %>

第2行定义了一个模板变量content = { …} ; 此变量跟临时变量一样,可以在其他地方使用,最常见的用户是用于复杂的布局。请参考高级用法布局

2.11. 引用属性

属性引用是模板中的重要一部分beetl支持属性同javascript的支持方式一样如下

  1. Beetl支持通过”.”号来访问对象的的属性如果javascript一样。如果User对象有个getName()方法,那么在模板中,可以通过${xxx.name}来访问
  2. 如果模板变量是数组或者List类这可以通过[] 来访问,如${userList[0]}
  3. 如果模板变量是Map类这可以通过[]来访问,如${map[“name”]},如果key值是字符串类型也可以使用${map.name}.但不建议这么使用因为会让模板阅读者误以为是一个Pojo对象
  4. Beetl也支持Generic Get方式即如果对象有一个public Object get(String key)方法,可以通过”.”号或者[]来访问,譬如 ${activityRecord.name}或者${activityRecord[“name”] }都将调用activityRecord的 get(String key)方法。如果对象既有具体属性又有Generic get这种模型设计方式是不值得鼓励则以具体属性优先级高.
  5. Beetl也可以通过[]来引用属性,如${user[“name”]} 相当于${user.name}.这跟javascript保持一致。但建议不这么做因为容易让阅读模板的人误认为这是一个Map类型
  6. Beetl 还可以定位额外的对象属性而无需更改java对象这叫着虚拟属性对于所有集合数组都有共同的虚拟熟悉size.虚拟属性是“.~”+虚拟1属性名
template.binding("list",service.getUserList());
template.binding("pageMap",service.getPage());

//在模板里
总共 ${list.~size}
<%
for(user in list){
%>
hello,${user.name};
<% } %>

当前页${pageMap['page']},总共${pageMap["total"]}

2.12. 属性赋值

Beetl2.7.0 开始支持对象赋值,如:

<%
var user = ....
user.name="joelli";
user.friends[0] = getNewUser();
user.map["name"] = "joelli";
%>

2.13. 算数表达式

Beetl支持类似javascript的算术表达式和条件表达式如+ - * / % 以及(),以及自增++,自减--

<%
var a = 1;
var b = "hi";
var c = a++;
var d = a+100.232;
var e = (d+12)*a;
var f = 122228833330322.1112h
%>

Beetl里定义的临时变量类型默认对应的java是Int型或者double类型对于模板常用情况说已经够了.如果需要定义长精度类型对应java的BigDecimal则需要在数字末尾加上h以表示这是长精度BigDecimal其后的计算和输出以及逻辑表达式都将按照长精度类型来考虑。

2.14. 逻辑表达式

Beetl支持类似Javascript,java的条件表达式 如><==!=>= , <= 以及 !, 还有&&和 || ,还有三元表达式等,如下例子

<%
var a = 1;
var b=="good";
var c = null;

if(a!=1&&b=="good"&&c==null){
        ......
}
%>

三元表达式如果只考虑true条件对应的值的话可以做简化,如下俩行效果是一样的。

<%
 var  a = 1 ;
%>
${a==1?"ok":''}
${a==1?"ok"}

2.15. 循环语句

Beetl支持丰富的循环方式如for-in,for(exp;exp;exp)以及while循环以及循环控制语句break;continue; 另外如果没有进入for循环体还可以执行elsefor指定的语句。

2.15.1. for-in

for-in循环支持遍历集合对象对于List和数组来说以及Iterator对象就是集合对象对于Map来说对象就是Map.entry,如下俩个例子

<%
for(user in userList){
        print(userLP.index);
        print(user.name);
}
%>

第三行代码userLP是Beetl隐含定义的变量能在循环体内使用。其命名规范是item名称后加上LP他提供了当前循环的信息

  • **userLP.index **当前的索引从1开始
  • **userLP.size **集合的长度
  • userLP.first 是否是第一个
  • userLP.last 是否是最后一个
  • userLP.even 索引是否是偶数
  • userLP.odd 索引是否是奇数

如何记住后缀是LP有俩个诀窍英语棒的是Loop的缩写拼音好的是老婆的拼音缩写这可以让程序员每次写到这的时候都会想想老婆不管有没有哈哈

如下是Map使用例子

<%
for(entry in map){
        var key = entry.key;
        var value = entry.value;
        print(value.name);
}
%>
2.15.2. for(exp;exp;exp)

对于渲染逻辑更为常见的是经典的for循环语句如下例子

<%
var a = [1,2,3];
for(var i=0;i<a.~size;i++){
        print(a[i]);
}
%>
2.15.3. while

对于渲染逻辑同样常见的有的while循环语句如下例子

<%
var i = 0;
while(i<5){
        print(i);
        i++;
}
%>
2.15.4. elsefor

不同于通常程序语言如果没有进入循环体则不需额外的处理模板渲染逻辑更常见情况是如果没有进入循环体还需要做点什么因此对于for循环来说还有elsefor 用来表达如果循环体没有进入则执行elsefor 后的语句

<%
var list = [];
for(item in list){

}elsefor{
        print("未有记录");
}
%>

2.16. 条件语句

2.16.1. if else

同js一样支持if else,如下例子

<%
var a =true;
var b = 1;
if(a&&b==1){

}else if(a){

}else{

}
%>
2.16.2. switch-case

同js一样支持switch-case,如下例子

<%
var b = 1;
switch(b){
        case 0:
                print("it's 0");
                break;
        case 1:
                print("it's 1");
                break;
        default:
                print("error");
}
%>

switch变量可以支持任何类型而不像js那样只能是整形

2.16.3. select-case

select-case 是switch case的增强版。他允许case 里有逻辑表达式同时也不需要每个case都break一下默认遇到合乎条件的case执行后就退出。

<%
var b = 1;
select(b){
        case 0,1:
                print("it's small int");
        case 2,3:
                print("it's big int");
        default:
                print("error");
}
%>

select 后也不需要一个变量这样case 后的逻辑表达式将决定执行哪个case.其格式是

<%
select {
        case boolExp,orBoolExp2:
                doSomething();
}
%>
<%
var b = 1;
select{
        case b<1,b>10:
                print("it's out of range");
                break;
        case b==1:
                print("it's 1");
                break;
        default:
                print("error");
}
%>

2.17. try-catch

通常模板渲染逻辑很少用到try-catch 但考虑到渲染逻辑复杂性以及模板也有不可控的地方所以提供try catch在渲染失败的时候仍然能保证输出正常

<%
try{
        callOtherSystemView()
}catch(error){
        print("暂时无数据");
}
%>

error代表了一个异常你可以通过error.message 来获取可能的错误信息

也可以省略catch部分这样出现异常不做任何操作

2.18. 虚拟属性

虚拟属性也是对象的属性,但是虚拟的,非模型对象的真实属性,这样的好处是当模板需要额外的用于显示的属性的时候但又不想更改模型,便可以采用这种办法 如beetl内置的虚拟属性.~size 针对了数组以及集合类型。

${user.gender}
${user.~genderShowName}

~genderShowName 是虚拟属性其内部实现根据boolean变量gender来显示性别

如何完成虚拟属性,请参考高级用法

2.19. 函数调用

Beetl内置了少量实用函数可以在Beetl任何地方调用。如下例子是调用date 函数,不传参数情况下,返回当前日期

<%
var date = date();
var len = strutil.length("cbd");
println("len="+len);
%>

注意函数名支持namespace方式因此代码第3行调用的函数是strutil.length

定义beetl的方法非常容易有三种方法

  • 实现Function类的call方法并添加到配置文件里或者显示的通过代码注册registerFunction(name,yourFunction)
  • 可以直接调用registerFunctionPackage(namespace,yourJavaObject),这时候yourJavaObject里的所有public方法都将注册为Beetl方法方法名是namespace+"."+方法名
  • 可以直接写模板文件并且以html作为后缀放到root/functions目录下这样此模板文件自动注册为一个函数其函数名是该模板文件名。

详情请参考高级用法

Beetl内置函数请参考附录以下列出了常用的函数

  • date 返回一个java.util.Date类型的变量如 date() 返回一个当前时间(对应java的java.util.Date); ${date( "2011-1-1" , "yyyy-MM-dd" )} 返回指定日期
  • print 打印一个对象 print(user.name);
  • println 打印一个对象以及回车换行符号,回车换号符号使用的是模板本身的,而不是本地系统的.如果仅仅打印一个换行符则直接调用println() 即可
  • nvl 函数nvl如果对象为null则返回第二个参数否则返回自己 nvl(user,"不存在")
  • isEmpty 判断变量或者表达式是否为空变量不存在变量为null变量是空字符串变量是空集合变量是空数组此函数都将返回true
  • isNotEmpty 同上,判断对象是否不为空
  • has 变量名为参数,判断是否存在此全局变量,如 has(userList),类似于1.x版本的exist("userList"),但不需要输入引号了
  • assert 如果表达式为false则抛出异常
  • trunc 截取数字保留指定的小数位如trunc(12.456,2) 输出是12.45
  • decode 一个简化的if else 结构,如 decode(a,1,"a=1",2,"a=2","不知道了")},如果a是1这decode输出"a=1",如果a是2则输出"a==2", 如果是其他值,则输出"不知道了"
  • debug 在控制台输出debug指定的对象以及所在模板文件以及模板中的行数如debug(1),则输出1 [在3行@/org/beetl/core/lab/hello.txt],也可以输出多个如debug("hi",a),则输出hi,a=123,[在3行@/org/beetl/core/lab/hello.txt]
  • parseInt 将数字或者字符解析为整形 如 parseInt("123");
  • parseLong 将数字或者字符解析为长整形parseInt(123.12);
  • parseDouble 将数字或者字符解析为浮点类型 如parseDouble("1.23")
  • range 接收三个参数初始值结束值还有步增可以不需要则默认为1返回一个Iterator常用于循环中如for(var i in range(1,5)) {print(i)},将依次打印1234.
  • flush 强制io输出。
  • json将对象转成json字符串如 var data = json(userList) 可以跟一个序列化规则 如,var data = json(userList,"[*].id:i"),具体参考 https://git.oschina.net/xiandafu/beetl-json
  • pageCtx 仅仅在web开发中设置一个变量然后可以在页面渲染过程中调用此api获取如pageCtx("title","用户添加页面")在其后任何地方可以pageCtx("title") 获取该变量
  • type.new 创建一个对象实例,如 var user = type.new("com.xx.User"); 如果配置了IMPORT_PACKAGE则可以省略包名type.new("User")
  • type.name 返回一个实例的名字var userClassName = type.name(user),返回"User"

2.20. 安全输出

安全输出是任何一个模板引擎必须重视的问题否则将极大困扰模板开发者。Beetl中如果要输出的模板变量为null则beetl将不做输出这点不同于JSPJSP输出null也不同于Feemarker如果没有用!,它会报错.

模板中还有俩种情况会导致模板输出异常

  • 有时候模板变量并不存在(譬如子模板里)
  • 模板变量为null但输出的是此变量的一个属性如${user.wife.name}

针对前俩种种情况可以在变量引用后加上以提醒beetl这是一个安全输出的变量。

如${user.wife.name! },即使user不存在或者user为null或者user.wife为null或者user.wife.name为null beetl都不将输出

可以在!后增加一个常量(字符串,数字类型等),或者另外一个变量,方法,本地调用,作为默认输出,譬如:

${user.wife.name!”单身”}如果user为null或者user.wife为null或者user.wife.name为null输出”单身”

譬如

${user.birthday!@System.constants.DefaultBir} 表示如果user为null或者user. birthday为null输出System.constants.DefaultBir

还有一种情况很少发生,但也有可能,输出模板变量发生的任何异常,如变量内部抛出的一个异常

这需要使用格式${!(变量)},这样,在变量引用发生任何异常情况下,都不作输出,譬如

${!(user.name)},beetl将会调用user.getName()方法如果发生异常beetl将会忽略此异常继续渲染

值得注意的是,在变量后加上!不仅仅可以应用于占位符输出(但主要是应用于占位符输出),也可以用于表达式中,如:

<%
var k = user.name!'N/A'+user.age!;
%>
<%
${k}
%>

如果user为null则k值将为N/A

在有些模板里可能整个模板都需要安全输出也可能模板的部分需要安全输出使用者不必为每一个表达式使用可以使用beetl的安全指示符号来完成安全输出 如:

<%
DIRECTIVE SAFE_OUTPUT_OPEN;
%>
${user.wife.name}
模板其他内容均能安全输出……
<%
//关闭安全输出。
DIRECTIVE SAFE_OUTPUT_CLOSE;
%>

Beetl不建议每一个页面都使用DIRECTIVE SAFE_OUTPUT_OPEN这样如果如果真有不期望的错误不容易及时发现其次安全输出意味着beetl会有额外的代码检测值是否存在或者是否为null性能会略差点。所以建议及时关闭安全输出这不是必须的但页面所有地方是安全输出可能不容易发现错误

在for-in 循环中 也可以为集合变量增加安全输出指示符号这样如果集合变量为null也可以不进入循环体

<%
var list = null;
for(item in list!){

}eslefor{
        print("no data");
}
%>
2.20.1. 变量是否存在
<%
if(has(flag)){
        print("flag变量存在可以访问")
}
%>

如果需要判断变量是否存在,如果存在,还有其他判断条件,通常都这么写

<%
if(has(flag)||flag==0){
        //code
}
%>

如果flag不存在或者flag存在但值是0都将执行if语句

但是,有更为简便的方法是直接用安全输出,如

<%
if(flag!0==0){
        //code
}
%>

flag!0 取值是这样的如果flag不存在则为0如果存在则取值flag的值类似三元表达式 has(flag)?falg:0

2.20.2. 安全输出表达式

安全输出表达式可以包括

  • 字符串常量,如 ${user.count!"无结果"}
  • boolean常量 ${user.count!false}
  • 数字常量,仅限于正数,因为如果是负数,则类似减号,容易误用,因此,如果需要表示负数,请用括号,如${user.count!(-1)}
  • class直接调用如${user.count!@User.DEFAULT_NUM}
  • 方法调用,如 ${user.count!getDefault() }
  • 属性引用,如 ${user.count!user.maxCount }
  • 任何表达式,需要用括号

2.21. 格式化

几乎所有的模板语言都支持格式化Beetl也不列外如下例子Beetl提供的内置日期格式

<% var date = date(); %>
Today is ${date,dateFormat="yyyy-MM-dd"}.
Today is ${date,dateFormat}
salary is ${salary,numberFormat="##.##"}

格式化函数只需要一个字符串作为参数放在=号后面如果没有为格式化函数输入参数则使用默认值dateFormat格式化函数默认值是local

Beetl也允许为指定的java class设定格式化函数譬如已经内置了对java.util.Date,java.sql.Date 设置了了格式化函数,因此上面的例子可以简化为

${date,yyyy-MM-dd}

Beetl针对日期和数字类型提供的默认的格式化函数在org/beetl/core/beetl-default.properties里注册了

##内置的格式化函数
FT.dateFormat =  org.beetl.ext.format.DateFormat
FT.numberFormat =  org.beetl.ext.format.NumberFormat
##内置的默认格式化函数
FTC.java.util.Date = org.beetl.ext.format.DateFormat
FTC.java.sql.Date = org.beetl.ext.format.DateFormat
FTC.java.sql.Time = org.beetl.ext.format.DateFormat
FTC.java.sql.Timestamp = org.beetl.ext.format.DateFormat
FTC.java.lang.Short = org.beetl.ext.format.NumberFormat
FTC.java.lang.Long = org.beetl.ext.format.NumberFormat
FTC.java.lang.Integer = org.beetl.ext.format.NumberFormat
FTC.java.lang.Float = org.beetl.ext.format.NumberFormat
FTC.java.lang.Double = org.beetl.ext.format.NumberFormat
FTC.java.math.BigInteger = org.beetl.ext.format.NumberFormat
FTC.java.math.BigDecimal = org.beetl.ext.format.NumberFormat
FTC.java.util.concurrent.atomic.AtomicLong = org.beetl.ext.format.NumberFormat
FTC.java.util.concurrent.atomic.AtomicInteger = org.beetl.ext.format.NumberFormat

2.22. 标签函数

所谓标签函数即允许处理模板文件里的一块内容功能等于同jsp tag。如Beetl内置的layout标签

index.html

<%
layout("/inc/layout.html",{title:'主题'}){
%>
Hello,this is main part
<% } %>

layout.html

title is ${title}
body content ${layoutContent}
footer

第1行变量title来自于layout标签函数的参数

第2行layoutContent 是layout标签体{}渲染后的结果

关于layout标签参考高级主题布局

Beetl内置了另外一个标签是include,允许 include 另外一个模板文件

<%
include("/inc/header.html"){}
%>

在标签中,{} 内容将依据标签的实现而执行layout标签将执行{}中的内容而include标签则忽略标签体内容。

关于如何实现标签函数,请参考高级主题,如下是一个简单的的标签函数:

public class CompressTag extends Tag{
        @Override
        public void render(){
                BodyContent  content = getBodyContent();
                String content = content.getBody();
                String zip = compress(conent);
                ctx.byteWriter.write(zip);
        }
}

2.23. HTML标签

Beetl 也支持HTML tag形式的标签 区分beetl的html tag 与 标准html tag。如设定HTML_TAG_FLAG=#则如下html tag将被beetl解析

<#footer style=”simple”/>
<#richeditor id=”rid” path="${ctxPath}/upload" name=”rname”  maxlength=”${maxlength}”> ${html} …其他模板内容   </#richdeitor>
<#html:input  id=aaaa />

如对于标签footer,Beetl默认会寻找WebRoot/htmltag/footer.tag(可以通过配置文件修改路径和后缀) ,内容如下:

<<<<<<< HEAD


@if(style==simple){
 请联系我 ${session.user.name}
@}else{
请联系我 ${session.user.name},phone:${session.user.phone}
@}

=======
```javascript
<% if(style==simple){ %>
 请联系我 ${session.user.name}
<% }else{ %>
请联系我 ${session.user.name},phone:${session.user.phone}
<% } %>
>>>>>>> 56cfdcade432bba9d453b48401dc67e1f1b6dbc4

如下还包含了自定义html标签一些一些规则

  • 可以在自定义标签里引用标签体的内容标签体可以是普通文本beetl模板以及嵌套的自定义标签等。如上<#richeditor 标签体里可用“tagBody”来引用
  • HTML自定义标签 的属性值均为字符串 如<#input value=”123” />,在input.tag文件里 变量value的类型是字符串
  • 可以在属性标签里引用beetl变量如<#input value=”${user.age}” />此时在input.tag里value的类型取决于user.age
  • 在属性里引用beetl变量不支持格式化如<#input value=”${user.date,yyyy-MM-dd }” />,如果需要格式化需要在input.tag文件里自行格式化
  • 在标签属性里传json变量需要谨慎因为json包含了"}",容易与占位符混合导致解析出错,因此得使用""符号,如<#input value=”${ {age:25} }” />
  • html tag 属性名将作为 其对应模板的变量名。
  • 默认机制下全局变量都将传给html tag对应的模板文件这个跟include一样。当然这机制也可以改变对于标签来说通常是作为一个组件存在也不一定需要完全传送所有全局变量而只传送request,session,这样变量因此需要重新继承org.beetl.ext.tag.HTMLTagSupportWrapper.并重载callHtmlTag方法。并注册为htmltag标签。具体请参考https://github.com/javamonkey/beetl2.0/blob/master/beetl-core/src/test/java/org/beetl/core/tag/HtmlTagTest.java

如果采用模板来写html标签功能不够强大beetl支持写标签函数参考上一节来实现html标签标签函数args[0]表示标签名这通常没有什么用处args[1] 则是标签的属性参数是个mapkey是html tag的属性value是其属性值如下用java完成的html 标签用于输出属性值

public class SimpleHtmlTag extends Tag{
        @Override
        public void render(){
                String tagName = (String) this.args[0];
                Map attrs = (Map) args[1];
                String value = (String) attrs.get("attr");
                try{
                        this.ctx.byteWriter.writeString(value);
                }catch (IOException e){

                }
        }
}

如果注册gt.registerTag("simpleTag", SimpleHtmlTag.class); 则如下模板输出了attr属性值abc

<#simpleTag attr="abc"></#simpleTag>

HTML_TAG_FLAG默认为#用来区别是否是beetl的html tag你也可以设置成其他符号比如 "my:",这样,<my:table></my:table> 其实是一个指向table.tag的标签实现

2.24. 绑定变量的HTML标签

对于html标签参考上一节Beetl还 支持将标签实现类java代码里的对象作为临时变量被标签体引用。此时需要实现GeneralVarTagBinding (此类是Tag的子类 该类提供另外3个个方法 - void binds(Object… array) 子类在render方法里调用此类以实现变量绑定绑定顺序同在模板中申明的顺序 - void bind(String name, Object value) 子类在render方法里调用此类以实现变量绑定name是模板中申明的变量名用此方法绑定不如binds更灵活不再推荐 - Object getAttributeValue 获得标签的属性 - Map getAttributes 获得标签的所有属性

public class TagSample extends GeneralVarTagBinding{
        @Override
        public void render(){
                int limit = Integer.parseInt((String) this.getAttributeValue("limit"));
                for (int i = 0; i < limit; i++){
                        this.binds(i)
                        this.doBodyRender();
                }
        }
}
//在某处注册一下标签TagSample
//gt.registerTag("tag", TagSample.class);

如上例子render方法将循环渲染标签体limit次且每次都将value赋值为i。我们再看看模板如何写的

<#tag limit="3";value>
	${value}
</#tag>

类似于常规html标签需要在标签的最后的属性定义后面加上分号 ";" 此分号表示这个是一个需要在标签运行时需要绑定变量的标签。后跟上要绑定的变量列表如上例只绑定了一个value变量如果需要绑定多个变量则用逗号分开如var1,var2 上。如果后面没有变量列表,只有分号,则默认绑定到标签名同名的变量上. 如果标签有namesapce则默认绑定订的变量名不包含namespace

注意由于标签使用因为太长可能换行或者是文本格式化导致换行目前beetl只允许在属性之间换行否则将报标签解析错误。

默认情况下如果标签属性出现了var(可以通过配置文件改成其他属性名),也认为是绑定变量的标签,如上面的例子也可以这么写

<#tag limit="3" var="value">
	${value}
</#tag>

var属性的值可以是个以逗号分开的变量名列表如var="total,customer,index"

2.25. 直接调用java方法和属性

可以通过符号@来表明后面表达式调用是java风格可以调用对象的方法属性

${@user.getMaxFriend(lucy)}
${@user.maxFriend[0].getName()}
${@com.xxxx.constants.Order.getMaxNum()}
${@com.xxxx.User$Gender.MAN}
<%
var max = @com.xxxx.constants.Order.MAX_NUM;
var c =1;
var d = @user.getAge(c);
%>

可以调用instance的public方法和属性也可以调用静态类的属性和方法 ,需要加一个 @指示此调用是直接调用class其后的表达式是java风格的。

  • GroupTemplate可以配置为不允许直接调用Class具体请参考配置文件.
  • 也可以通过安全管理器配置到底哪些类Beetl不允许调用具体请参考高级用法。默认情况java.lang.Runtime,和 java.lang.Process不允许在模板里调用。你自己的安全管理器也许可以配置为不能直接访问DAO类避免了以前jsp可以访问任意代码带来的危害
  • 请按照java规范写类名和方法名属性名。这样便于beetl识别到底调用的是哪个类哪个方法。否则会抛出错误
  • 可以省略包名只用类名。beetl将搜索包路径找到合适的类需要设置配置“IMPORT_PACKAGE=包名.;包名.”,包名后需要跟一个“.”, 或者调用Configuration.addPkg)方法具体请参考附件配置文件说明
  • 内部类包括枚举访问同java一样如User类有个内部枚举类Gender访问是User$Gender
  • 表达式是java风格但参数仍然是beetl表达式比如 @user.sayHello(user.name).这里user.sayHello是java调用user.name 仍然是beetl表达式

2.26. 严格MVC控制

如果在配置文件中设置了严格MVC则以下语法将不在模板文件里允许否则将报出STRICK_MVC 错误

  • 定义变量,为变量赋值,如var a = 12是非法的
  • 算术表达式 如${user.age+12}是非法的
  • 除了只允许布尔以外,不允许逻辑表达式和方法调用 如if(user.gender==1)是非法的
  • 方法调用,如${subString(string,1)}是非法的
  • Class方法和属性调用如${@user.getName()}是非法的
  • 严格的MVC非常有助于逻辑与视图的分离特别当逻辑与视图是由俩个团队来完成的。如果你嗜好严格MVC可以调用groupTemplate.enableStrict()

通过重载AntlrProgramBuilder可以按照自己的方法控制到底哪些语法是不允许在模板引擎中出现的但这已经超出了Beetl模板的基础使用

2.27. 指令

指令格式为: DIRECTIVE 指令名 指令参数(可选) Beetl目前支持安全输出指令分别是

  • DIRECTIVE SAFE_OUTPUT_OPEN ; 打开安全输出功能,此指令后的所有表达式都具有安全输出功能,
  • DIRECTIVE SAFE_OUTPUT_CLOSE ; 关闭安全输出功能。详情参考安全输出
  • DIRECTIVE DYNAMIC varName1,varName2 …指示后面的变量是动态类型Beetl应该考虑为Object. 也可以省略后面的变量名则表示模板里所有变量都是Object
<% DIRECTIVE DYNAMIC idList;
for(value in idList) .....

DYNAMIC 通常用在组件模板里因为组件模板可以接收任何类型的对象。如列表控件可以接收任何含有id和 value属性的对象。

  1. 注意 DYNAMIC 后的变量名也允许用引号这主要是兼容Beetl1.x版本
  2. Beetl1.x 指令都是大写,当前版本也允许小写,如 directive dynamic idList

2.28. 类型声明

Beetl 本质上还是强类型的模板引擎即模板每个变量类型是特定的在模板运行过程中beetl 会根据全局变量自动推测出模板中各种变量和表达式类型。 也可以通过类型申明来说明beetl全局变量的类型如下格式

<%
/**
*@type (List<User> idList,User user)
*/
for(value in idList) .....

类型申明必须放到多行注释里,格式是@type( … ),里面的申明类似java方法的参数申明。正如你看到的类型申明是在注释里也就表明了这在Beetl模板引擎中不是必须的或者你只需要申明一部分即可之所以提供可选的类型说明是因为

  • 提高一点性能
  • 最重要的是提高了模板的可维护性。可以让模板维护者知道变量类型也可以让未来的ide插件根据类型声明来提供属性提示重构等高级功能

需要注意的是,如果在类型声明里提供的是类名,而不是类全路径,这样必须在配置文件里申明类的搜索路径(需要设置配置IMPORT_PACKAGE=包名.;包名.或者调用Configuration.addPkg))默认的搜索路径有java.util. 和 java.lang.

2.29. 错误处理

Beetl能较为详细的显示错误原因包括错误行数错误符号错误内容附近的模板内容以及错误原因如果有异常还包括异常和异常信息。 默认情况下,仅仅在控制台显示,如下代码:

<%
var a = 1;
var b = a/0;
%>

运行此模板后,错误提示如下

>>DIV_ZERO_ERROR:0 位于3行 资源:/org/beetl/sample/s0125/error1.txt
1|<%
2|var a = 1;
3|var b = a/0;
4| %>
<%
var a = 1;
var b = a
var c = a+2;
%>

运行此模板后

>>缺少符号(PARSER_MISS_ERROR):缺少输入 ';'  'var' 位于4行 资源:/org/beetl/sample/s0125/error2.txt
1|<%
2|var a = 1;
3|var b = a
4|var c = a+2;
5| %>
  1. 默认的错误处理器仅仅像后台打印错误并没有抛出异常如果需要在render错误时候抛出异常到控制层则可以使用org.beetl.core.ReThrowConsoleErrorHandler。不仅打印异常还抛出BeetlException
  2. 可以自定义异常处理器,比如把错误输出到 作为渲染结果一部分输出或者输出更美观的html内容等具体参考高级用法
  3. 可以在配置文件不设置异常这样Beetl引擎将不处理异常用户可以在外部来处理可以在外部调用ErrorHandler子类来显示异常

2.30. Beetl小工具

BeetlKit 提供了一些便利的方法让你立刻能使用Beetl模板引擎。提供了如下方法

  • public static String render(String template, Map<String, Object> paras) 渲染模板使用paras参数渲染结果作为字符串返回
  • public static void renderTo(String template, Writer writer, Map<String, Object> paras) 渲染模板使用paras参数渲染结果作为字符串返回
  • public static void execute(String script, Map<String, Object> paras) 执行某个脚本
  • public static Map execute(String script, Map<String, Object> paras, String[] locals) 执行某个脚本将locals指定的变量名和模板执行后相应值放入到返回的Map里
  • public static Map executeAndReturnRootScopeVars(String script) 执行某个脚本返回所有顶级scope的所有变量和值
  • public static String testTemplate(String template, String initValue) 渲染模板template其变量来源于intValue脚本运行的结果其所有顶级Scope的变量都将作为template的变量
  • public static String testTemplate(String template, String initValue) 渲染模板template其变量来源于intValue脚本运行的结果其所有顶级Scope的变量都将作为template的变量
String template = "var a=1,c=2+1;";
Map result = executeAndReturnRootScopeVars(template);
System.out.println(result);
//输出结果是{c=3, a=1}

BeetlKit 不要用于线上系统。仅仅作为体验Beetl功能而提供的如果需要在线上使用这些功能请参考该类源码自行扩展

2.31. 琐碎功能

  • 对齐:我发现别的模板语言要是做到对齐,非常困难,使用Beetl你完全不用担心比如veloctystringtemlatefreemarker例子都出现了不对齐的情况影响了美观Beetl完全无需担心输出对齐
  • Escape:可以使用\ 做escape 符号,如$monkey$ 将作为一个普通的文本,输出为monkey.再如为了在后加上美元符号占位符恰好又是美元符号可以用这俩种方式hello,its $money$$, 或者Hello,its $money+"$"$ 。如果要输出\符号本生,则需要用俩个\,这点与javascriptjava 语义一致.

3. 高级功能

3.1. 配置GroupTemplate

Beetl建议通过配置文件配置配置GroupTemplate主要考虑到未来可能IDE插件会支持Beetl模板模板的属性和函数等如果能通过配置文件获取将有助于IDE插件识别。 配置GroupTemplate有俩种方法

  • 配置文件: 默认配置在/org/beetl/core/beetl-default.properties 里Beetl首先加载此配置文件然后再加载classpath里的beetl.properties,并用后者覆盖前者。配置文件通过Configuration类加载因此加载完成后也可以通过此类API来修改配置信息
  • 通过调用GroupTemplate提供的方法来注册函数格式化函数标签函数等

配置文件分为三部分,第一部分是基本配置,在第一节讲到过。第二部分是资源类配置,可以在指定资源加载类,以及资源加载器的属性,如下

RESOURCE_LOADER=org.beetl.core.resource.ClasspathResourceLoader
#资源配置resource后的属性只限于特定ResourceLoader
#classpath 根路径
RESOURCE.root= /
#是否检测文件变化
RESOURCE.autouCheck= true

第1行指定了类加载器

第4行指定了模板根目录的路径此处/ 表示位于classpath 根路径下

第6行是否自动检测模板变化默认为true开发环境下自动检测模板是否更改。关于如何如何自定义ResouceLoader请参考下一章

配置文件第三部分是扩展部分,如方法,格式化函数等

#####  扩展 ##############
## 内置的方法
FN.date = org.beetl.ext.fn.DateFunction
FN.nvl = org.beetl.ext.fn.NVLFunction
.................
##内置的功能包
FNP.strutil = org.beetl.ext.fn.StringUtil

##内置的格式化函数
FT.dateFormat =  org.beetl.ext.format.DateFormat
FT.numberFormat =  org.beetl.ext.format.NumberFormat
.................

##内置的默认格式化函数
FTC.java.util.Date = org.beetl.ext.format.DateFormat
FTC.java.sql.Date = org.beetl.ext.format.DateFormat

## 标签类
TAG.include= org.beetl.ext.tag.IncludeTag
TAG.includeFileTemplate= org.beetl.ext.tag.IncludeTag
TAG.layout= org.beetl.ext.tag.LayoutTag
TAG.htmltag= org.beetl.ext.tag.HTMLTagSupportWrapper

FN前缀表示FunctionFNP前缀表示FunctionPackageFT表示format函数FTC表示类的默认Format函数TAG表示标签类。Beetl强烈建议通过配置文件加载扩展。以便随后IDE插件能识别这些注册函数

3.2. 自定义方法

3.2.1. 实现Function
public class Print implements Function{
        public String call(Object[] paras, Context ctx){
                Object o = paras[0];
                if (o != null){
                        try{
                                ctx.byteWriter.write(o.toString());
                        }catch (IOException e){
                                throw new RuntimeException(e);
                        }
                }
                return "";
        }
}

call方法有俩个参数第一个是数组这是由模板传入的对应着模板的参数第二个是Context包含了模板的上下文主要提供了如下属性

  • byteWriter 输出流
  • template 模板本身
  • gt GroupTemplate
  • globalVar 该模板对应的全局变量
  • byteOutputMode 模板的输出模式,是字节还是字符
  • safeOutput 模板当前是否处于安全输出模式
  • 其他属性建议不熟悉的开发人员不要乱动
  1. call方法要求返回一个Object如果无返回返回null即可
  2. 为了便于类型判断call方法最好返回一个具体的类如date函数返回的就是java.util.Date
  3. call方法里的任何异常应该抛出成Runtime异常
3.2.2. 使用普通的java类

尽管实现Function对于模板引擎来说是效率最高的方式但考虑到很多系统只有util类这些类里的方法仍然可以注册为模板函数。其规则很简单就是该类的所有public方法。如果需还要Context 变量则需要在方法最后一个参数加上Context即可

public class util{
	public String print(Object a, Context ctx){
		//balabala...
	}
}

注意

  1. 从beetl效率角度来讲采用普通类效率不如实现Function调用
  2. 采用的普通java类尽量少同名方法。这样效率更低。beetl调用到第一个适合的同名方法。而不像java那样找到最匹配的
  3. 方法名支持可变数组作为参数
  4. 方法名最后一个参数如果是Context则beetl会传入这个参数。
3.2.3. 使用模板文件作为方法

可以不用写java代码模板文件也能作为一个方法。默认情况下需要将模板文件放到Root的functions目录下且扩展名为.html(可以配置文件属性来修改此俩默认值) 方法参数分别是para0,para1…..

如下root/functions/page.fn

<%
//para0,para1 由函数调用传入
var current = para0,total = para1,style=para2!'simple'
%>
当前页面 ${current},总共${total}

则在模板中

<%
page(current,total);
%>

允许使用return 表达式返回一个变量给调用者如模板文件functions\now.html

<%
return date();
%>

在任何模板里都可以调用:

hello time is ${now(),'yyyy-MM-dd'}

也可以在functions建立子目录这样function则具有namespace其值就是文件夹名

3.3. 自定义格式化函数

需要实现Format接口

public class DateFormat implements Format{
        public Object format(Object data, String pattern){
                if (data == null)
                        return null;
                if (Date.class.isAssignableFrom(data.getClass())){
                        SimpleDateFormat sdf = null;
                        if (pattern == null){
                                sdf = new SimpleDateFormat();
                        }else{
                                sdf = new SimpleDateFormat(pattern);
                        }
                        return sdf.format((Date) data);
                }else{
                        throw new RuntimeException("Arg Error:Type should be Date");
                }
        }
}

data 参数表示需要格式化的对象pattern表示格式化模式开发时候需要考虑pattern为null的情况

也可以实现ContextFormat 类抽象方法从而得到Context获取外的格式化信息。

public abstract Object format(Object data,String pattern,Context ctx);

3.4. 自定义标签

标签形式有俩种一种是标签函数第二种是html tag。第二种实际上在语法解析的时候会转化成第一种其实现是HTMLTagSupportWrapper此类将会寻找root/htmltag目录下同名的标签文件作为模板来执行。类似普通模板一样在此就不详细说了

3.4.1. 标签函数

标签函数类似jsp2.0的实现方式需要实现Tag类的render方法即可

public class DeleteTag extends Tag{
        @Override
        public void render(){
                // do nothing,just ignore body
                ctx.byteWriter.write("被删除了,付费可以看")
        }
}

如上一个最简单的Tag将忽略tag体并输出内容

public class XianDeDantengTag extends Tag{
        @Override
        public void render(){
                doBodyRender();
        }
}

此类将调用父类方法doBodyRender渲染tag body体

public class CompressTag extends Tag{
        @Override
        public void render(){
                BodyContent  content = getBodyContent();
                String content = content.getBody();
                String zip = compress(conent);
                ctx.byteWriter.write(zip);
        }
}

此类将调用父类方法getBodyContent 获得tag body后压缩输出

tag类提供了如下属性和方法供使用

  • args 传入标签的参数
  • gt GroupTemplate
  • ctx Context
  • bw 当前的输出流
  • bs 标签体对应的语法树,不熟悉勿动

3.5. 自定义虚拟属性

可以为特定类注册一个虚拟属性,也可以为一些类注册虚拟属性

  • public void registerVirtualAttributeClass(Class cls, VirtualClassAttribute virtual) 实现VirtualClassAttribute方法可以为特定类注册一个需要属性如下代码

    gt.registerVirtualAttributeClass(User.class, new VirtualClassAttribute() {
            @Override
            public String eval(Object o, String attributeName, Context ctx){
                    User user = (User) o;
                    if(attributeName.equals("ageDescritpion")){
                            if (user.getAge() < 10){
                                    return "young";
                            }else{
                                    return "old";
                            }
                    }
            }
    });
    

    User类的所有虚拟属性将执行eval方法此方法根据年纪属性来输出对应的描述。

  • public void registerVirtualAttributeEval(VirtualAttributeEval e) 为一些类注册需要属性VirtualAttributeEval.isSupport方法将判断是否应用虚拟属性到此类

    如下是虚拟属性类的定义

    public interface VirtualClassAttribute{
            public Object eval(Object o, String attributeName, Context ctx);
    }
    
    public interface VirtualAttributeEval extends VirtualClassAttribute{
            public boolean isSupport(Class c, String attributeName);
    }
    

3.6. 使用额外的资源加载器

某些情况下模板来源不止一处GroupTemplate配置了一个默认的资源加载器如果通过gt.getTemplate(key),将调用默认的ResourceLoader获取模板内容然后转化为beetl脚本放入到缓存里。你也可以传入额外的资源管理器加载模板通过调用gt.getTemplate(key,otherLoader)来完成;

GroupTemplate gt = new GroupTemplate(conf,fileLoader)
//自定义,参考下一节
MapResourceLoader dbLoader = new MapResourceLoader(getData());
Template t = gt.getTemplate("db:1", dbLoader);

private Map getData(){
        Map data = new HashMap();
        data.put("db:1", "${a}");
        return data;
}

对于更复杂的模板资源来源,也可以自定义一个资源加载来完成,参考下一节

3.7. 自定义资源加载器

如果模板资源来自其他地方如数据库或者混合了数据库和物理文件或者模板是加密的则需要自定义一个资源加载器。资源加载器需要实现ResourceLoader类。如下

public interface ResourceLoader{

        /**
         * 根据key获取Resource
         *
         * @param key
         * @return
         */
        public Resource getResource(String key);

        /** 检测模板是否更改,每次渲染模板前,都需要调用此方法,所以此方法不能占用太多时间,否则会影响渲染功能
         * @param key
         * @return
         */
        public boolean isModified(Resource key);

        /**
         * 关闭ResouceLoader通常是GroupTemplate关闭的时候也关闭对应的ResourceLoader
         */
        public void close();

        /** 一些初始化方法
         * @param gt
         */
        public void init(GroupTemplate gt);

        /**  用于includelayout等根据相对路径计算资源实际的位置.
         * @param resource 当前资源
         * @param key
         * @return
         */
        public String getResourceId(Resource resource, String key);
}

如下是一个简单的内存ResourceLoader

public class MapResourceLoader implements ResourceLoader{
        Map data;

        public MapResourceLoader(Map data){
                this.data = data;
        }

        @Override
        public Resource getResource(String key){
                String content = (String) data.get(key);
                if (content == null)
                        return null;
                return new StringTemplateResource(content, this);
        }

        @Override
        public boolean isModified(Resource key){
                return false;
        }

        @Override
        public boolean exist(String key){
                return data.contain(key);
        }

        @Override
        public void close(){

        }

        @Override
        public void init(GroupTemplate gt){

        }

        @Override
        public String getResourceId(Resource resource, String id){
                //不需要计算相对路径
                return id;
        }
}

init方法可以初始化GroupTemplate比如读取配置文件的root属性autoCheck属性字符集属性以及加载functions目录下的所有模板方法 如FileResourceLoader 的 init方法

@Override
public void init(GroupTemplate gt){
        Map<String, String> resourceMap = gt.getConf().getResourceMap();
        if (this.root == null){
                this.root = resourceMap.get("root");
        }
        if (this.charset == null){
                this.charset = resourceMap.get("charset");
        }
        if (this.functionSuffix == null){
                this.functionSuffix = resourceMap.get("functionSuffix");
        }

        this.autoCheck = Boolean.parseBoolean(resourceMap.get("autoCheck"));

        File root = new File(this.root, this.functionRoot);
        this.gt = gt;
        if (root.exists()){
                readFuntionFile(root, "", "/".concat(functionRoot).concat("/"));
        }
}

readFuntionFile 方法将读取functions下的所有模板并注册为方法

protected void readFuntionFile(File funtionRoot, String ns, String path){
        String expected = ".".concat(this.functionSuffix);
        File[] files = funtionRoot.listFiles();
        for (File f : files){
                if (f.isDirectory()){
                        //读取子目录
                        readFuntionFile(f, f.getName().concat("."), path.concat(f.getName()).concat("/"));
                } else if (f.getName().endsWith(functionSuffix)){
                        String resourceId = path + f.getName();
                        String fileName = f.getName();
                        fileName = fileName.substring(0, (fileName.length() - functionSuffix.length() - 1));
                        String functionName = ns.concat(fileName);
                        FileFunctionWrapper fun = new FileFunctionWrapper(resourceId);
                        gt.registerFunction(functionName, fun);
                }
        }
}

Resource类需要实现OpenReader方法以及isModified方法。对于模板内容存储在数据库中openReader返回一个ClobisModified 则需要根据改模板内容对应的lastUpdate通常数据库应该这么设计来判断模板是否更改

public abstract class Resource{

        /**
         * 打开一个新的Reader
         *
         * @return
         */
        public abstract Reader openReader();

        /**
         * 检测资源是否改变
         *
         * @return
         */
        public abstract boolean isModified();

参考例子可以参考beetl自带的ResourceLoader

3.8. 使用CompositeResourceLoader

组合加载器可以包含多个已有的ResourceLoader如下代码创建一个包含俩个文件和内存的ResourceLoader

FileResourceLoader fileLoader1 = new FileResourceLoader(path1);
FileResourceLoader fileLoader2 = new FileResourceLoader(path2);
Map data = getData();
// 根据id加载
MapResourceLoader mapLoader = new MapResourceLoader(data);

CompositeResourceLoader loader = new CompositeResourceLoader();
loader.addResourceLoader(new StartsWithMatcher("http:").withoutPrefix(), fileLoader2);
loader.addResourceLoader(new StartsWithMatcher("db:"), mapLoader);
loader.addResourceLoader(new AllowAllMatcher(), fileLoader1);

GroupTemplate gt = new GroupTemplate(loader, conf);
Template t = gt.getTemplate("/xxx.html");

如上例子groupTemplate从CompositeResourceLoader里加载/xxx.html由于http:和db:前缀都不匹配因此将实际采用fileLoader1加载path1+/xxx.html如下是xxx.html文件内容

<%
include("/xxx2.html"){}
include("http:/xxx.html"){}
%>

第2行仍然是由fileLoader1加载但第3行以http:前缀开头因此将fileLoader2加载path2+/xxx.html.xxx.html内容如下

<%
include("db:1"){}
%>

因为以db:开头因此会采用MapResourceLoader加载内容是key为db:1对模板

3.9. 自定义错误处理器

错误处理器需要实现ErrorHandler接口的processExcption(BeetlException beeExceptionos, Writer writer);

  • beeExceptionos模板各种异常
  • writer 模板使用的输出流。系统自带的并未采用此Writer而是直接输出到控制台

自定义错误处理可能是有多个原因,比如

  1. 想将错误输出到页面而不是控制台

  2. 错误输出美化一下,而不是自带的格式

  3. 错误输出的内容做调整,如不输出错误行的模板内容,而仅仅是错误提示

  4. 错误输出到日志系统里

  5. 不仅仅输出日志还抛出异常。默认自带的不会抛出异常ReThrowConsoleErrorHandler 继承了ConsoleErrorHandler方法打印异常后抛出

    public class ReThrowConsoleErrorHandler extends ConsoleErrorHandler{
            @Override
            public void processExcption(BeetlException ex, Writer writer){
                    super.processExcption(ex, writer);
                    throw ex;
            }
    }
    

beetl 提供 ErrorInfo类来wrap BeetlException转化为较为详细的提示信息他具有如下信息

  • type 一个简单的中文描述
  • errorCode 内部使用的错误类型标识
  • errorTokenText 错误发生的节点文本
  • errorTokenLine 错误行
  • msg 错误消息有可能没有因为有时候errorCode描述的已经很清楚了
  • cause 错误的root 异常,也可能没有。

BeetlException 也包含了一个关键信息就是 resourceId即出错所在的模板文件

3.10. 自定义安全管理器

所有模板的本地调用都需要通过安全管理器校验默认需要实现NativeSecurityManager 的public boolean permit(String resourceId, Class c, Object target, String method) 方法

如下是默认管理器的实现方法

public class DefaultNativeSecurityManager implements NativeSecurityManager{

        @Override
        public boolean permit(String resourceId, Class c, Object target, String method){
                if (c.isArray()){
                        //允许调用,但实际上会在在其后调用中报错。不归此处管理
                        return true;
                }
                String name = c.getSimpleName();
                String pkg = c.getPackage().getName();
                if (pkg.startsWith("java.lang")){
                        if (name.equals("Runtime") || name.equals("Process") || name.equals("ProcessBuilder")
                                        || name.equals("System")){
                                return false;
                        }
                }
                return true;
        }
}

3.11. 注册全局共享变量

groupTemplate.setSharedVars(Map<String, Object> sharedVars)

3.12. 布局

布局可以通过Beetl提供的include,layout 以及模板变量来完成。模板变量能完成复杂的布局

  • 采用layout include

<<<<<<< HEAD


@ //content.html内容如下
@ layout("/inc/layout.html"){
 this is 正文
 ..........
@}

======= javascript <% //content.html内容如下 layout("/inc/layout.html"){ %> this is 正文 .......... <% } %>

56cfdcade432bba9d453b48401dc67e1f1b6dbc4

如上一个子页面将使用layout布局页面layout 页面内容如下

<<<<<<< HEAD

@ include("/inc/header.html"){} 
this is content:${layoutContent}
this is footer:

======= javascript <% include("/inc/header.html"){} %> this is content:${layoutContent} this is footer:

56cfdcade432bba9d453b48401dc67e1f1b6dbc4

layoutContent 是默认变量也可以改成其他名字具体请参考layout标签函数

全局变量总是能被布局用的页面所使用,如果布局页面需要临时变量,则需要显示的传入,如:

```javascript
<%
var user= model.user;
include("/inc/header.html",{title:'这是一个测试页面',user:user}){} 
%>
```

这样title和user成为全局变量能被header.html 及其子页面引用到
  • 继承布局采用模板变量和include

    <%
            var jsPart = {
    %>
    web页面js部分
    
    <% }; %>
    
    <%
            var htmlPart = {
    %>
    web页面html部分
    
    <% };
    include("/inc/layout.html",{jsSection:jsPart,htmlSection:htmlPart}){}
    %>
    

    layout.html页面如下

    <body>
    <head>
    ${jsSection}
    </head>
    <body>
    .......
    ${htmlSection}
    </body>
    

3.13. 性能优化

Beetl性能已经很快了有些策略能更好提高性能

  • 使用二进制输出,此策略可以使模板在语法分析的时候将静态文本转化为二进制,省去了运行时刻编码时间,这是主要性能提高方式。但需要注意,此时需要提供一个二进制输出流,而不是字符流,否则性能反而下降
  • 使用FastRuntimeEngine默认配置。 此引擎能对语法树做很多优化,从而提高运行性能,如生成字节码来访问属性而不是传统的反射访问。关于引擎,可能在新的版本推出更好的引擎,请随时关注。
  • 通过@type 来申明全局变量类型,这不能提高运行性能,但有助于模板维护
  • 自定义ResourceLoader的isModified必须尽快返回因此每次渲染模板的时候都会调用此方法

为什么Beetl性能这么好…………(待续)

3.14. 分布式缓存模板

Beetl模板引擎模板在同一个虚拟机里缓存Beetl 脚本。也可以将缓存脚本到其他地方只要实现Cache接口并设置ProgramCacheFactory.cache即可这样GroupTemplate将从你提供的Cache中存取Beetl脚本

此功能未被很好测试

3.15. 定制输出

占位符输出允许定制。如所有日期类型都按照某个格式化输出而不需要显示的使用格式化输出或者为了防止跨脚本站点攻击需要对类型为String的值做检查等不必使用格式化函数可以直接对占位符输出进行定制代码如下

PlaceholderST.output = new PlaceholderST.Output(){
        @Override
        public void write(Context ctx, Object value) throws IOException {
                //定制输出
                ctx.byteWriter.writeString("ok"+value!=null?value.toString:"");
        }
};

如果PlaceholderST静态变量output 不为null将使用output 来输出

3.16. 定制模板引擎

Beetl在线体验http://ibeetl.com/beetlonline/)面临一个挑战,允许用户输入任何脚本做练习或者分享代码。但又需要防止用户输入恶意的代码,如

<%
for(var i=0;i<10000000;i++){
        //其他代码
}
%>

此时需要定制模板引擎遇到for循环的时候应该限制循环次数譬如在线体验限制最多循5次这是通过定义替换GeneralForStatement类来完成的这个类对应了for(exp;exp;exp) ,我们需要改成如下样子:

class RestrictForStatement extends GeneralForStatement{
        public RestrictForStatement(GeneralForStatement gf){
                super(gf.varAssignSeq, gf.expInit, gf.condtion, gf.expUpdate, gf.forPart, gf.elseforPart, gf.token);
        }

        public void execute(Context ctx){
                if (expInit != null){
                        for (Expression exp : expInit){
                                exp.evaluate(ctx);
                        }
                }
                if (varAssignSeq != null){
                        varAssignSeq.execute(ctx);
                }

                boolean hasLooped = false;
                int i = 0;
                for (; i < 5; i++){
                        boolean bool = (Boolean) condtion.evaluate(ctx);
                        if (bool){
                                hasLooped = true;
                                forPart.execute(ctx);
                                switch (ctx.gotoFlag){
                                        case IGoto.NORMAL:
                                                break;
                                        case IGoto.CONTINUE:
                                                ctx.gotoFlag = IGoto.NORMAL;
                                                continue;
                                        case IGoto.RETURN:
                                                return;
                                        case IGoto.BREAK:
                                                ctx.gotoFlag = IGoto.NORMAL;
                                                return;
                                }
                        }else{
                                break;
                        }
                        if (this.expUpdate != null){
                                for (Expression exp : expUpdate){
                                        exp.evaluate(ctx);
                                }
                        }
                }

                if (i >= 5){
                        try{
                                ctx.byteWriter.writeString("--Too may Data in loopIgnore the left Data  for Online Engine--");
                                ctx.byteWriter.flush(); 
                        } catch (IOException e){
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                        }
                }
        }

        @Override
        public void infer(InferContext inferCtx){
                super.infer(inferCtx);
        }

}

尽管上面代码很复杂但实际上是改写了原来的GeneralForStatement将原来的24行while(true) 替换成for (; i < 5; i++) 用来控制最大循环并且62行检测如果循环退出后i等于5则提示Too Many Data in Loop.

现在需要将此类替换原有的GeneralForStatement

public class OnlineTemplateEngine extends DefaultTemplateEngine{
        public Program createProgram(Resource resource, Reader reader, Map<Integer, String> textMap, String cr,GroupTemplate gt){
                Program program = super.createProgram(resource, reader, textMap, cr, gt);
                modifyStatemetn(resource,program,gt);
                return program;
        }
        private void modifyStatemetn(Resource resource,Program program,GroupTemplate gt){
                Statement[] sts = program.metaData.statements;
                StatementParser parser = new StatementParser(sts, gt, resource.getId());
                parser.addListener(WhileStatement.class, new RestrictLoopNodeListener());
                parser.addListener(GeneralForStatement.class, new RestrictLoopNodeListener());
                parser.parse();
        }
}

继承FastRuntimeEngine有所不同因为改引擎会copy出一个脚本做分析优化因此俩个脚本都需要做修改

public class OnlineTemplateEngine extends FastRuntimeEngine{
        public Program createProgram(Resource resource, Reader reader, Map<Integer, String> textMap, String cr,GroupTemplate gt){
                FilterProgram program = (FilterProgram)super.createProgram(resource, reader, textMap, cr, gt);
                modifyStatemetn(resource,program,gt);
                modifyStatemetn(resource,program.getCopy(),gt);
                return program;
        }
}
class RestrictLoopNodeListener implements Listener{
        @Override
        public Object onEvent(Event e){
                Stack stack = (Stack) e.getEventTaget();
                Object o = stack.peek();
                if (o instanceof GeneralForStatement){
                        GeneralForStatement gf = (GeneralForStatement) o;
                        RestrictForStatement rf = new RestrictForStatement(gf);
                        return rf;
                }else{
                        return null;
                }
        }
}

该监听器返回一个新的RestrictForStatement 类用来替换来的GeneralForStatement。如果返回null则不需替换。这通常发生在你仅仅通过修改该类的某些属性就可以的场景

完成这些代码后,在配置文件中申明使用新的引擎

ENGINE=org.bee.tl.online.OnlineTemplateEngine

这样就完成了模板引擎定制。

3.17. 直接运行Beetl脚本

Beetl模板本质上会转化为Beetl脚本来执行这点跟jsp转为servlet来执行类似。GroupTemplate提供方法可以直接执行Beetl脚本

  • public Map runScript(String key, Map<String, Object> paras) throws ScriptEvalError
  • public Map runScript(String key, Map<String, Object> paras, Writer w) throws ScriptEvalError
  • public Map runScript(String key, Map<String, Object> paras, Writer w, ResourceLoader loader) throws ScriptEvalError

key为资源名paras为脚本的全局变量w可选参数如果执行脚本有输出则输出到w里,loader参数可选如果指定则使用此laoder加载脚本

执行脚本完毕后返回到Map里的值可能包含如下

  • 模板的顶级的临时变量,key为临时变量名
  • return 值将返回到map里 key为return

如下脚本(此时就不需要脚本定界符了)

var a = 1;
var b = date();
var c = '2';
return a+1;

调用runScript后map里将返回key分别为a,b,creturn。 值分别为1当前日期字符串'2以及3。

4. Web集成

4.1. Web提供的全局变量

Web集成模块向模板提供web标准的变量做如下说明

  • request 中的所有attribute.在模板中可以直接通过attribute name 来引用如在controller层 request.setAttribute("user",user),则在模板中可以直接用${user.name} .
  • session 提供了session会话模板通过session["name"],或者session.name 引用session里的变量
  • request 标准的HTTPSerlvetRequest,可以在模板里引用request属性getter如${request.requestURL}。
  • parameter 用户读取用户提交的参数。如${parameter.userId} (仅仅2.2.7以上版本支持)
  • ctxPath Web应用ContextPath
  • servlet 是WebVariable的实例包含了HTTPSession,HTTPSerlvetRequest,HTTPSerlvetResponse.三个属性模板中可以通过request.response,session 来引用,如 ${serlvet.request.requestURL};
  • 所有的GroupTemplate的共享变量
  • pageCtx是一个内置方法 仅仅在web开发中用于设置一个变量然后可以在页面渲染过程中调用此api获取如pageCtx("title","用户添加页面")在其后任何地方可以pageCtx("title") 获取该变量。(仅仅2.2.7以上版本支持)

你可以在模板任何地方访问这些变量

如果你需要扩展更多属性你也可以配置beetl.properties配置文件的WEBAPP_EXT属性实现WebRenderExt接口在渲染模板之前增加自己的扩展如:

RESOURCE.root=/WEB-INF/views
WEBAPP_EXT = com.park.oss.util.GlobalExt
public class GlobalExt implements WebRenderExt{
        static long version = System.currentTimeMillis();
        @Override
        public void modify(Template template, GroupTemplate arg1, HttpServletRequest arg2, HttpServletResponse arg3) {
                //js,css 的版本编号
                template.binding("sysVersion",version);
        }
}

这样每次在模板里都可以访问变量sysVersion了不需要再controller里设置或者通过servlet filter来设置

4.2. 集成技术开发指南

Beetl默认提供了WebRender用于帮助web集成开发所有内置的集成均基于此方法。如果你认为Beetl内置的各个web框架集成功能不够你可以继承此类或者参考此类源码重新写其代码如下

package org.beetl.ext.web;

import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.util.Enumeration;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.beetl.core.GroupTemplate;
import org.beetl.core.Template;
import org.beetl.core.exception.BeetlException;

/**
 *  通常web渲染的类将request变量赋值给模板同时赋值的还有session,request,ctxPath
 *  其他框架可以继承此类做更多的定制
 * @author joelli
 *
 */
public class WebRender{
	GroupTemplate gt = null;

	public WebRender(GroupTemplate gt){
		this.gt = gt;
	}

	/**
	 * @param key 模板资源id
	 * @param request
	 * @param response
	 * @param args 其他参数将会传给modifyTemplate方法
	 */
	public void render(String key, HttpServletRequest request, HttpServletResponse response, Object... args){
		Writer writer = null;
		OutputStream os = null;
		try{
			//response.setContentType(contentType);
			Template template = gt.getTemplate(key);
			Enumeration<String> attrs = request.getAttributeNames();

			while (attrs.hasMoreElements()){
				String attrName = attrs.nextElement();
				template.binding(attrName, request.getAttribute(attrName));
			}
			WebVariable webVariable = new WebVariable();
			webVariable.setRequest(request);
			webVariable.setResponse(response);
			webVariable.setSession(request.getSession());

			template.binding("session", new SessionWrapper(webVariable.getSession()));

			template.binding("servlet", webVariable);
			template.binding("request", request);
			template.binding("ctxPath", request.getContextPath());

			modifyTemplate(template, key, request, response, args);

			String strWebAppExt = gt.getConf().getWebAppExt();
			if(strWebAppExt!=null){
				WebRenderExt renderExt = this.getWebRenderExt(strWebAppExt);
				renderExt.modify(template, gt, request, response);
			}
			if (gt.getConf().isDirectByteOutput()){
				os = response.getOutputStream();
				template.renderTo(os);
			}else{
				writer = response.getWriter();
				template.renderTo(writer);
			}

		} catch (IOException e){
			handleClientError(e);
		} catch (BeetlException e){
			handleBeetlException(e);
		} finally{
			try{
				if (writer != null)
					writer.flush();
				if (os != null)
					os.flush();
			} catch (IOException e){
				handleClientError(e);
			}
		}
	}

	/**
	 * 可以添加更多的绑定
	 * @param template 模板
	 * @param key 模板的资源id
	 * @param request
	 * @param response
	 * @param args  调用render的时候传的参数
	 */
	protected void modifyTemplate(Template template, String key, HttpServletRequest request,
		HttpServletResponse response, Object... args){
	}

	/**处理客户端抛出的IO异常
	 * @param ex
	 */
	protected void handleClientError(IOException ex){
		//do nothing
	}

	/**处理客户端抛出的IO异常
	 * @param ex
	 */
	protected void handleBeetlException(BeetlException ex){
		throw ex;
	}
}

4.3. Serlvet集成

只需要在Servlet代码里引用ServletGroupTemplate就能集成Beetl他提供了一个render(String child, HttpServletRequest request, HttpServletResponse response)方法。例子如下:

protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
    response.setContentType("text/html;charset=UTF-8");
    //模板直接访问users
    request.setAttribute("users",service.getUsers());
    ServletGroupTemplate.instance().render("/index.html", request, response);
}

ServletGroupTemplate同其他web集成一样将读取配置文件来配置如果需要通过代码配置可以在Serlvet listener里 ServletGroupTemplate.instance().getGroupTemplate()方法获取GroupTemplate

4.4. SpringMVC集成

需要做如下配置即可

<bean id="beetlConfig" class="org.beetl.ext.spring.BeetlGroupUtilConfiguration" init-method="init"/>
<bean id="viewResolver" class="org.beetl.ext.spring.BeetlSpringViewResolver">
        <property name="contentType" value="text/html;charset=UTF-8"/>
</bean>

同其他集成方式一样模板的配置将放在beetl.properties中。

如果想获取GroupTemplate,可以调用如下代码

BeetlGroupUtilConfiguration config = (BeetlGroupUtilConfiguration) this.getApplicationContext().getBean("beetlConfig");
GroupTemplate group = config.getGroupTemplate();

Controller代码如下:

@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(HttpServletRequest req) {
    ModelAndView view = new ModelAndView("/index");
    //total 是模板的全局变量,可以直接访问
    view.addObject("total",service.getCount());
    return view;
}

http://git.oschina.net/xiandafu/springbeetlsql 有完整例子

4.5. SpringMVC集成高级

spring集成还允许注册被spring容器管理的FunctionTag等也还允许配置多个视图解析器等功能

<bean name="beetlConfig" class="org.beetl.ext.spring.BeetlGroupUtilConfiguration" init-method="init">
	<property name="configFileResource" value="/WEB-INF/beetl.properties"/>
	<property name="functions">
		<map>
			<entry key="testFunction" value-ref="testFunction"/>
		</map>
	</property>

	<property name="functionPackages">
		<map>
			<entry key="fp" value-ref="testFunctionPackage"/>
		</map>
	</property>

	<property name="tagFactorys">
		<map>
			<entry key="html.output" value-ref="testTagFactory"/>
			<entry key="html.output2" value-ref="testTagFactory2"/>
		</map>
	</property>

</bean>

<bean name="testTagFactory" class="org.beetl.ext.spring.SpringBeanTagFactory">
	<property name="name" value="testTag"/>
</bean>
<bean name="testTagFactory2" class="org.beetl.ext.spring.SpringBeanTagFactory">
	<property name="name" value="testTag2"/>
</bean>


<bean name="beetlViewResolver" class="org.beetl.ext.spring.BeetlSpringViewResolver">
	<property name="config" ref="beetlConfig"/>
	<property name="contentType" value="text/html;charset=UTF-8"/>
</bean>

如上图所示BeetlGroupUtilConfiguration有很多属性列举如下

  • configFileResource 属性指定了配置文件所在路径如果不指定则默认在classpath下
  • functions 指定了被spring容器管理的functionkey为注册的方法名value-ref 指定的bean的名称
  • functionPackages指定了被spring容器管理的functionPackagekey为注册的方法包名value-ref 指定的bean的名称
  • tagFactorys 注册tag类key是tag类的名称value-ref指向一个org.beetl.ext.spring.SpringBeanTagFactory实例该子类是一个Spring管理的Bean。属性name对应的bean就是tag类。需要注意由于Tag是有状态的因此必须申明Scope为 "prototype"。如代码:
@Service
@Scope("prototype")
public class TestTag extends Tag {
}
  • typeFormats: 同functions参数是 Map<Class, Format>其中key为类型Class
  • formats同functions参数是 Map<String, Format>其中key为格式化函数名
  • virtualClassAttributes 同functions参数Map<Class, VirtualClassAttribute>其中key为类型Class
  • virtualAttributeEvals 类型为List
  • resourceLoader资源加载器 ,值是 实现ResourceLoader的一个Bean
  • errorHandler 错误处理值是实现ErrorHandler的一个Bean
  • sharedVars,同functions类型是Map<String, Object>,可以在此设置共享变量
  • configProperties类型是Properties可以覆盖配置文件的某些属性

如下配置指定了三个视图解析器一个用于beetl页面渲染一个用于cms采用了beetl技术另外一个一些遗留的页面采用jsp

<bean name="beetlConfig" class="org.beetl.ext.spring.BeetlGroupUtilConfiguration" init-method="init">
	<property name="configFileResource" value="/WEB-INF/beetl.properties"/>
</bean>


<bean name="cmsbeetlConfig" class="org.beetl.ext.spring.BeetlGroupUtilConfiguration" init-method="init">
	<property name="configFileResource" value="/WEB-INF/cms-beetl.properties"/>
</bean>


<!-- Beetl视图解析器1 -->
<bean name="beetlViewResolver" class="org.beetl.ext.spring.BeetlSpringViewResolver">
	<!-- 多视图解析器需要设置viewNames和order -->
	<property name="viewNames">
		<list>
			<value>/template/**</value>
		</list>
	</property>
	<property name="suffix" value=".btl"/>
	<property name="contentType" value="text/html;charset=UTF-8"/>
	<property name="order" value="0"/>
	<!-- 多GroupTemplate需要指定使用的bean -->
	<property name="config" ref="beetlConfig"/>

</bean>

<!-- Beetl视图解析器2 -->
<bean name="cmsBeetlViewResolver" class="org.beetl.ext.spring.BeetlSpringViewResolver">
	<!-- 多视图解析器需要设置viewNames和order -->
	<property name="viewNames">
		<list>
			<value>/cmstemplate/**</value>
		</list>
	</property>
	<property name="contentType" value="text/html;charset=UTF-8"/>
	<property name="order" value="1"/>
	<!-- 多GroupTemplate需要指定使用的bean -->
	<property name="config" ref="cmsbeetlConfig"/>

</bean>

<!-- JSP视图解析器 -->
<bean name="JSPViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
	<!-- 注意JSP的这个视图解析器order必须在最后 -->
	<property name="order" value="256"/>
	<!-- beetl配置不支持前缀这不同于jsp 和 freemaker -->
	<property name="prefix" value="/WEB-INF/"/>
	<property name="suffix" value=".jsp"/>
	<property name="contentType" value="text/html;charset=UTF-8"/>
</bean>

Beetl视图解析器属性同spring自带的视图解析器一样支持contentType,order,prefix,suffix等属性。

注意视图解析器里属性viewNames这个用于判断controller返回的path到底应该交给哪个视图解析器来做。

  • 以/template开头的是beetlViewResolver来渲染。
  • 以/cmstemplate是交给cmsBeetlViewResolver渲染。
  • 如果都没有匹配上则是jsp渲染

如果你想更改此规则你只能增加canHandle方法指定你的逻辑了。详情参考org.springframework.web.servlet.view.UrlBasedViewResolver.canHandle

对于仅仅需要redirect和forward的那些请求需要加上相应的前缀

  • 以"redirect:"为前缀时表示重定向不产生BeetlView渲染模版而直接通过Servlet的机制返回重定向响应.redirect:前缀后面的内容为重定向地址可以采用相对地址相对当前url),绝对地址(完整的url),如果采用/开头的地址会自动的在前面接上当前Web应用的contextPath即contextPath为test的Web应用中使用redirect:/admin/login.html 实际重定向地址为 /test/admin/login.html
  • 以"forward:"为前缀时表示转发不产生BeetlView渲染模版。而是直接通过Servlet的机制转发请求关于转发和重定向的区别请自行查看Servlet API) forward:前缀后面的内容为转发地址,一般都是以/开头相对于当前Web应用的根目录

其他集成需要注意的事项:

  • spring集成请不要使用spring的 前缀配置,改用beetl的RESOURCE.ROOT 配置否则includelayout会找不到模板

4.6. Spring Boot集成

Spring Boot 通过java config来配置 beetl需要的BeetlGroupUtilConfiguration和 BeetlSpringViewResolver参考代码如下

@Configuration
public class BeetlConf {

	@Value("${beetl.templatesPath}") String templatesPath;//模板跟目录 
	@Bean(initMethod = "init", name = "beetlConfig")
	public BeetlGroupUtilConfiguration getBeetlGroupUtilConfiguration() {
		BeetlGroupUtilConfiguration beetlGroupUtilConfiguration = new BeetlGroupUtilConfiguration();
		try {
			ClasspathResourceLoader cploder = new ClasspathResourceLoader(BeetlConf.class.getClassLoader(),templatesPath);
			beetlGroupUtilConfiguration.setResourceLoader(cploder);
			return beetlGroupUtilConfiguration;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

	}

	@Bean(name = "beetlViewResolver")
	public BeetlSpringViewResolver getBeetlSpringViewResolver(@Qualifier("beetlConfig") BeetlGroupUtilConfiguration beetlGroupUtilConfiguration) {
		BeetlSpringViewResolver beetlSpringViewResolver = new BeetlSpringViewResolver();
		beetlSpringViewResolver.setContentType("text/html;charset=UTF-8");
		beetlSpringViewResolver.setOrder(0);
		beetlSpringViewResolver.setConfig(beetlGroupUtilConfiguration);
		return beetlSpringViewResolver;
	}

 }

spring boot集成需要注意的是要添加spring-devtools.properties文件,并配置如下选项

restart.include.beetl=/beetl-xxx.jar
restart.include.beetlsql=/beetlsql-xxx..jar

spring-devtools.properties 为spring boot的配置文件,位于META-INF目录下

4.7. Jodd集成

需要配置web.xml,将所有请求交给jodd处理参考:http://jodd.org/doc/madvoc/setup.html

<filter>
	<filter-name>madvoc</filter-name>
	<filter-class>jodd.madvoc.MadvocServletFilter</filter-class>
	<init-param>
			<param-name>madvoc.webapp</param-name>
			<param-value>test.MyWebApplication</param-value>
	</init-param>
    <init-param>
		<param-name>madvoc.configurator</param-name>
		<param-value>test.MyAutomagicMadvocConfigurator</param-value>
	</init-param>

</filter>
<filter-mapping>
	<filter-name>madvoc</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

MyWebApplication 和 MyAutomagicMadvocConfigurator 需要自己参照如下例子写一个前者用来设置beetl作为视图渲染后者配置Jodd不要扫描beetl struts集成里引用的struts类

public class MyAutomagicMadvocConfigurator extends AutomagicMadvocConfigurator {
	public MyAutomagicMadvocConfigurator(){
		super();
		//不扫描beetl 里jar文件里的action和result否则会扫描StrutsResultSupport不相干的class
		this.rulesJars.exclude("**/*beetl*.jar");
	}
}
public class MyWebApplication  extends WebApplication{
	@Override
	protected void init(MadvocConfig madvocConfig, ServletContext servletContext) {
		//设置默认
		madvocConfig.setDefaultActionResult(BeetlActionResult.class);
	}
 }

最后可以写Action了浏览器输入/index.html,jodd将执行world方法并渲染ok.html模板。如果你想配置GroupTemplate正如其他集成框架一样只需要写一个beetl.properties 即可。

@MadvocAction
public class IndexAction {
	@Out
	String value;
	@Action("/index.html")
	public String world() {
		value = "Hello World!";
		return "/ok.html";
	}
}

https://git.oschina.net/xiandafu/beetl-jodd-sample 有完整例子

4.8. JFinal集成

Beetl提供 JFinal 集成使用BeetlRenderFactory 通过如下注册即可使用beetl模板引擎

import org.beetl.ext.jfinal.BeetlRenderFactory
public class DemoConfig extends JFinalConfig{
        public void configConstant(Constants me){
                me.setMainRenderFactory(new BeetlRenderFactory());
                // 获取GroupTemplate ,可以设置共享变量等操作
                GroupTemplate groupTemplate = BeetlRenderFactory.groupTemplate ;
        }
}

业务逻辑代码:

public void modify(){
	int artId = getParaToInt(0, -1);
	setAttr("title", "修改文章");
	List<Cate> cateLists = Cate.getAllCate();
	//模板里访问cateLists,atr,
	setAttr("cateLists", cateLists);
	setAttr("art", Article.dao.findById(artId));
	render("/modify.html");
}

BeetlRenderFactory 默认使用FileResourceLoader 其根目录位于WebRoot目录下如果你需要修改到别的目录可以设置配置文件

RESOURCE.root= /WEB-INF/template/

https://git.oschina.net/xiandafu/beetl-jfinal-sample 有完整例子采用jfinal+beetl写的一个博客系统

4.9. Nutz集成

Nutz集成提供了 BeetlViewMaker ,实现了 ViewMaker方法如下代码

@At("/ctx")
@Ok("beetl:ctx.btl")
public Context withContext() {
	Context ctx = Lang.context();
	Pager pager = dao.createPager(1, 20);
	pager.setRecordCount(dao.count(UserProfile.class));
	List<UserProfile> list = dao.query(UserProfile.class, null, pager);
	ctx.set("pager", pager);
	ctx.set("list", list);
	return ctx;
}
<html>
<head>
<title>Beetl&Nutz</title>
</head>
<body>
<p>总共 ${list.~size}<p/>
<%
for(user in list){
%>
<p>hello,${user.nickname};<p/>
<% } %>

<p>当前页${pager.pageNumber},总共${pager.pageCount}页<p/>
</body>
</html>

需要注意的是如果使用了nutz的objhttp://www.nutzam.com/core/mvc/view.html则需要在模板顶部申明obj是动态对象

<%
directive dynamic obj
%>

${obj.user.title}
${obj.user.name}

4.10. Struts2集成

需要在struts2配置文件里添加result-types做如下配置

<package name="default" namespace="/" extends="struts-default">
<!--  ....   -->
<result-types>
	<result-type name="beetl"
		class="org.beetl.ext.struts2.Struts2BeetlActionResult" default="true" >
		<param name="contentType">text/html; charset=UTF-8</param>
	</result-type>
</result-types>

<action name="HelloWorld" class="com.beetl.struts.HelloWorld">
	<result>/hello.html</result>
</action>
<action name="Ajax" class="com.beetl.struts.AjaxHtml">
	<result>/table.html#table</result>
</action>
<!--  ....   -->
</package>

该类会根据struts配置文件获取模板如上例的hello.html,并将formbean的属性以及request属性作为全局变量传递给模板

https://git.oschina.net/xiandafu/beetl-struts2-sample 有完整例子

郑重申明

鉴于struts2有安全漏洞而官方补丁打法很消极所以请谨慎使用Struts2Beetl的安全性已经通过在线体验和多个使用Beetl的网站得以体现 一旦你的struts2网站被攻破请先确定是否是struts2 的问题

4.11. 直接Web中运行Beetl模板

对于web应用来说必须通过controller才能渲染模板beetl也可以写完模板后在未完成controller情况下直接渲染模板 此方法既可以作为通常的全栈式开发人员使用,也可以用于前端人员单独开发模板用。

步骤如下:

  • 配置监听器,监听器指定对*.btl的请求进行监听(假定模板名字都是以btl.结尾)。

  • 实现监听器,该监听器继承父类 org.beetl.ext.web.SimpleCrossFilter实现protected abstract GroupTemplate getGroupTemplate()方法。依据不同的集成方式比如你的环境是Servlet则只需要调用ServletGroupTemplate.instance().getGroupTemplate(),如果是Jfinal需要调用BeetlRenderFactory.groupTemplate等

  • SimpleCrossFilter 提供一些有用的方法,可以帮助你定制一些特性,可以参考源码了解

  • 置完成后对于要测试的模板可以新建一个对应的伪模型文件比如要测试模板WebRoot/user/userList.html,可以新建立WebRoot/values/user/userList.html.var 。 values是监听器默认的伪模型的根目录

  • 编辑伪模型文件对应于userList.html需要的全局变量userList.html.var可以申明这些些变量

    var proudct = {id:1,name:'测试产品',pic:'xxxx.jpg'};
    var userList = [{id:2,name:'用户一'}];
    var session= {admin:{id:1,name:'admin'}};
    
  • 通过浏览器直接访问http://ip:port/user/userList.html 监听器会预先执行userList.html.var并将返回值作为模板的全局变量传给userList.html

  • 可以将一些公共的变量放到WebRoot/values/common.var里比如上面代码的session. 监听器会先执行common.var,然后再执行userList.html.var

直接访问模板前提是使用了伪模型这与实际的项目采用的模型并不一致因此当模板采用伪模型验证后需要重启web应用才能使用真正的模型去测试否则模板引擎会报错这是因为beetl默认的FastRuntimeEngine会根据模型优化模板对同一个模板不同的模型会报错除非采用DefaultTemplateEngine 或者页面申明类型变量是动态的。

4.12. 整合ajax的局部渲染技术

越来越多web网站依赖于ajax如table的翻页流行方式是浏览器发出ajax请求后台处理后返回一个json浏览器端将json数据拆开拼成一条一条的行数据然后生成dom节点追加到表格里。 作为另外一种可选技术beetl支持局部渲染技术允许后台处理返回的是一个完成的html片段这样前端浏览器可以直接将这个html片段追加到表格里。在我做的性能测试里俩种方式性能差别不大(http://beetlajax.oschina.mopaas.com/)

比如模板index.html有很多动态内容有动态生成的菜单有右侧的top10也有核心区域的表格大概内容如下

<#menu/>
<#top10> ....</#top10>
<div id="table-container" >
<%
//ajax片段开始
#ajax userTable: {
%>

<table>
        <tr><td width=100>id</td><td width=100>姓名</td></tr>
        <% for(user in users){ %>
        <tr><td>${user.id}</td><td>${user.name}</td></tr>
        <% } %>
</table>

当前页面<span id="current">${page!1}</span><span style="width:20px"></span>
<a href="#"><span  class="page">next</span></a> <a href="#" ><span  class="page">pre</span></a>
<%
//ajax片段结尾
}
%>

#ajax 用于告诉告诉模板引擎,此处是个局部渲染标记,标记为"userTable",对于正常渲染视图"index.html"页面,#ajax标记没什么用处,table仍能得到正常渲染。如果渲染的视图是index.html#userTable,则模板只会渲染#ajax标记得模板片段其他部分将忽略。关于完整例子可以参考http://beetlajax.oschina.mopaas.com/

后台代码如下:

render("/index.html#userTable");

ajax 片段渲染也支持默认情况下不渲染仅仅做为一个片段使用如一个页面有许多后台交互操作并返回相应的html片段可以将这些html片段也放到同一个模板里使用ajax norender表示渲染整个模板的时候默认并不需要渲染此ajax片段

<%
<html>

</html>
#ajax norender success: {
%>
<div id="success"> 操作成功
</div>

<%
}
%>

#ajax norender failure: {
%>
<div id="failure"> 操作失败
</div>

<%
}
%>

这样此页面默认情况下并没有输出success,和 failure片段

注意Ajax片段本质上是从模版的ajax标记处开始渲染因此ajax需要的变量在模版里也必须是全局变量如果你只是个局部变量beetl会报出找不到变量即使你binding了这个变量beetl也认为这个是局部变量

<%
var tableData = paras.table;
#ajax userTable: {
for(user in tableData);
%>

<%
//ajax片段结尾
}
%>

变量tableData是从paras里获取的是个临时变量因此就算你在后台binding了一个tableDatabeetl 也不能识别。在渲染ajax片段的时候会报变量tableData找不到。改正的办法只能是让tableData全局变量。

返回Json好还是返回html片段好这个难以定论.

  • 从后台性能看将模型序列化成json性能会比渲染模板性能更好但是json还需要前端重新解析生成最终html dom节点这可能会延迟最终数据的现实效果。而返回的html片段就是已经生成好的dom
  • 从网络传入来看json无疑更好的html片段会有额外的html标记css属性以及有可能的js调用。传入流量有可能增加50%到100%。但是对于web应用类这些额外数据并不算多。
  • 从开发效率来讲返回html片段的开发效率更高一些因为渲染在后台操作可以随心所欲的用模板语言来渲染来取得后台数据完成复杂渲染而json就比较困难可以说所有的json lib都没有完美的解决办法。
  • 从用户体验上来讲Beetl 采用ajax标记混合了传统的模板渲染和ajax加载。用户进入页面即能看到数据而经典的ajax json方式还需要异步加载显示延迟。另外如果页面同时有多个ajax加载则会对服务器造成很大的压力。
  • 关心服务器cpu消耗 模板方式消耗更多的cpujson方式则少点。但是俩者差距并不大。而且更多的web网站面临的情况是有富余的服务器CPU能力
  • 关心客户端CPU消耗 过多的js无疑是客户端运行慢的主要原因。如果采用经典的json方式返回的json数据必然还需要经过js的计算和渲染。会影响客户机器cpu。

4.13. 在页面输出错误提示信息

2.2.3版本以后新增加org.beetl.ext.web.WebErrorHandler,可以在web开发的时候在页面输出提示信息在产品模式下载后台输出提示信息通过配置属性ESOURCE.autoCheck= true来认为是开发模式仅仅需要配置如下:

ERROR_HANDLER = org.beetl.ext.web.WebErrorHandler

5. 附录

5.1. 内置方法

5.1.1. 常用内置方法
  • date 返回一个java.util.Date类型的变量如 date() 返回一个当前时间(对应java的java.util.Date); ${date( "2011-1-1" , "yyyy-MM-dd" )} 返回指定日期
  • print 打印一个对象 print(user.name);
  • println 打印一个对象以及回车换行符号,回车换号符号使用的是模板本身的,而不是本地系统的.如果仅仅打印一个换行符则直接调用println() 即可
  • nvl 函数nvl如果对象为null则返回第二个参数否则返回自己 nvl(user,"不存在")
  • isEmpty 判断变量或者表达式是否为空变量不存在变量为null变量是空字符串变量是空集合变量是空数组此函数都将返回true
  • isNotEmpty 同上,判断对象是否不为空
  • has 变量名为参数,判断是否存在此全局变量,如 has(userList),类似于1.x版本的exist("userList"),但不需要输入引号了
  • assert 如果表达式为false则抛出异常
  • trunc 截取数字保留指定的小数位如trunc(12.456,2) 输出是12.45
  • decode 一个简化的if else 结构,如 decode(a,1,"a=1",2,"a=2","不知道了")},如果a是1这decode输出"a=1",如果a是2则输出"a==2", 如果是其他值,则输出"不知道了"
  • debug 在控制台输出debug指定的对象以及所在模板文件以及模板中的行数如debug(1),则输出1 [在3行@/org/beetl/core/lab/hello.txt],也可以输出多个如debug("hi",a),则输出hi,a=123,[在3行@/org/beetl/core/lab/hello.txt]
  • parseInt 将数字或者字符解析为整形 如 parseInt("123");
  • parseLong 将数字或者字符解析为长整形parseInt(123.12);
  • parseDouble 将数字或者字符解析为浮点类型 如parseDouble("1.23")
  • range 接收三个参数初始值结束值还有步增可以不需要则默认为1返回一个Iterator常用于循环中如for(var i in range(1,5)) {print(i)},将依次打印1234.
  • flush 强制io输出。
  • json将对象转成json字符串如 var data = json(userList) 可以跟一个序列化规则 如,var data = json(userList,"[*].id:i"),具体参考 https://git.oschina.net/xiandafu/beetl-json
  • pageCtx 仅仅在web开发中设置一个变量然后可以在页面渲染过程中调用此api获取如pageCtx("title","用户添加页面")在其后任何地方可以pageCtx("title") 获取该变量
  • type.new 创建一个对象实例,如 var user = type.new("com.xx.User"); 如果配置了IMPORT_PACKAGE则可以省略包名type.new("User")
  • type.name 返回一个实例的名字var userClassName = type.name(user),返回"User"
5.1.2. 字符串相关方法

strutil方法对参数均不做空指针检测你可自定义方法来覆盖这些内置的方法

  • strutil.startWith ${ strutil.startWith(“hello”,”he”) 输出是true
  • strutil.endWith ${ strutil.endWith(“hello”,”o”) 输出是true
  • strutil.length ${ strutil. length (“hello”),输出是5
  • strutil.subString ${ strutil.subString (“hello”,1),输出是“ello”
  • strutil.subStringTo ${ strutil.subStringTo (“hello”,1,2),输出是“e”
  • strutil.split ${ strutil.split (“hello,joeli”,”,”),输出是数组有俩个元素第一个是hello第二个是joelli”
  • strutil.contain ${ strutil.contain (“hello,”el”),输出是true
  • strutil.toUpperCase ${ strutil.toUpperCase (“hello”),输出是HELLO
  • strutil.toLowerCase ${ strutil.toLowerCase (“Hello”),输出是hello
  • strutil.replace ${ strutil.replace (“Hello”,”lo”,”loooo”),输出是helloooo
  • strutil.format ${ strutil.format (“hello,{0}, my age is {1}”,”joeli”,15),输出是hello,joelli, my age is 15. 具体请参考http://docs.oracle.com/javase/6/docs/api/java/text/MessageFormat.html
  • strutil.trim 去掉字符串的尾部空格
  • strutil.formatDate var a = strutil.formatDate(user.bir,yyyy-MM-dd);
  • strutil.index var index = strutil.index("abc","a");返回 索引0
  • strutil.lastIndex var index = strutil.lastIndex("aba","a");返回索引2
5.1.3. 数组相关方法
  • array.range 返回数组或者Collection一部分接受三个参数第一个是数组或者Collection子类第二三个参数分别是起始位置
  • array.remove 删除某个数组或者Collection的一个元素并返回该数组或者Collection.第一个是数组或者Collection子类,第二个参数是元素
  • array.add 向数组或者Collection添加一个元素并返回该数组或者Collection。第一个是数组或者Collection子类,第二个参数是元素
  • array.contain 判断数组或者元素是否包含元素如果包含返回true。否则false。第一个是数组或者Collection子类,第二个参数是元素
  • array.toArray 转化成数组,如array.toArray(1,2,"a");
  • array.collection2Array 将java集合转化为数组 array.collection2Array([1,2,''])
5.1.4. 正则表达式相关方法
  • reg.match(str,regex) str为需要处理的字符串regex是表达式
  • reg.replace(str,regex,replace)str为需要处理的字符串regex是表达式替换的字符串替换字符串
  • reg.find(str,regex) 返回找到的符合表达式的第一个字符串,否则返回空字符串
  • reg.findList(str,regex) 找到所有符合表达式的字符串,否则返回空列表
  • reg.split(str,regex),对字符串进行切分,返回列表
  • reg.split(str,regex,limit) 同上limit是最多返回个数
5.1.5. Spring 相关函数

Spring函数并没有内置需要注册如下

<bean name="beetlGroupUtilConfiguration" class="org.beetl.ext.spring.BeetlGroupUtilConfiguration" init-method="init">
	<property name="functions">
		<map>
			<!-- 定义SpEL方法 -->
			<entry key="spel">
				<bean class="org.beetl.ext.spring.SpELFunction"/>
			</entry>
		</map>
		</property>
		 <property name="functionPackages">
		<map>
			<entry key="sputil">
				<bean class="org.beetl.ext.spring.UtilsFunctionPackage"/>
			</entry>
		</map>
	</property>
</bean>

spel(spelString, rootObject) SpEL方法传入一个Spring SpEL表达式以获取表达式结果方法建议以函数的方式定义在BeetlGroupUtilConfiguration的functions中

spelString: SpEL表达式字符串必传(否则返回null) rootObject: 作为spel的根对象对应#root可以是一个Map或Bean对象默认取空Map。由于Beetl运行上下文无法直接获取模版局部变量的变量名建议局部变量采用自定义Map的方式传入

  • 列表筛选以自定义Map为根对象传入局部变量

    <% var intArray = [12, 1, 2, 3]; %>
    ${spel('#root.intArray.?[#this>10]', {intArray: intArray})}
    
  • 以Bean对象为根对象

    <% var now = date(); %>
    ${spel('#root.year + 1900', now)}
    
  • 直接new对象

    ${spel('(new java.util.Date()).year + 1900')}
    
  • 直接引用Spring Bean

    ${spel('@testBean')}
    
  • 默认变量

  • #root 表示SpEL的根对象, 由spel函数第二参数传入默认是一个空map

  • #context 表示Beetl执行上下文

  • #global 表示Beetl的共享变量Map由于Beetl上下文无法获取临时变量名临时变量建议使用根对象的方式传入

  • #ctxPath 表示Servlet Context Path由Beetl WebRender提供

  • #servlet 可以从中获取到Servlet requestresponsesession原生实例由Beetl WebRender提供

  • #parameter 表示请求参数Map由Beetl WebRender提供

  • #request 表示请求对象由Beetl WebRender提供

  • #session 表示会话域属性Map由Beetl WebRender提供

sputil 提供了spring内置的一些功能

// 测试source中是否包含了candidates的某个成员(相当于交集非空)
sputil.containsAny(Collection<?> source, Collection<?> candidates)
// 返回在source集合总第一个也属于candidates集的元素
sputil.findFirstMatch(Collection<?> source, Collection<?> candidates)
// 测试指定文本是否匹配指定的Ant表达式(\*表达式), 多个表达式只要一个匹配即可
sputil.antMatch(String input, String... patterns)
// 返回指定路径表示的文件的扩展名(不带点.)
sputil.fileExtension(String path)
// 忽略大小写的endsWith
sputil.endsWithIgnoreCase(String input, String suffix)
// 忽略大小写的startsWith
sputil.startsWithIgnoreCase(String input, String prefix)
// 测试输入值是否为空白, null视为空白, 无视字符串中的空白字符
sputil.isBlank(String input)
// 首字母大写转换
sputil.capitalize(String input)
// 首字母小写转换
sputil.uncapitalize(String input)
// 在集合或数组元素之间拼接指定分隔符返回字符串
// null表示空集, 其他类型表示单元素集合
sputil.join(Object collection, String delim)
// 同上, 只是会在最后结果前后加上前缀和后缀
// 注意这个函数名叫做joinEx
sputil.joinEx(Object collection, String delim, String prefix, String suffix)
// 对文本进行html转义
sputil.html(String input)
// 对文本进行javascript转义
sputil.javaScript(String input)
5.1.6. Spring security

下列三个函数只需以函数的方式定义在BeetlGroupUtilConfiguration的functions中即可与spel函数一样的函数名声明在functions中可以更改

  • auth() 对应类: org.beetl.ext.spring.AuthenticationFunction 方法无参数 返回值: 返回当前安全上下文中的用户认证凭证Authentication实例 如果当前环境不存在Spring Security安全上下文将返回null值
  • urlIf(<url>, <method>) 对应类: org.beetl.ext.spring.AccessUrlIfFunction 参数: url: 字符串表示的测试URL Path不需要指定Context Path缺省会直接返回true method: 字符串表示的访问方式, 默认为GET, 建议全大写 返回值: 测试当前登录用户是否能访问指定的URL Path, 返回true or false

示例:

urlIf('/system/admin_update.do', 'POST'))

如果当前环境不存在Spring Security安全上下文将返回true 如果当前环境不存在用户认证凭证,作为匿名登录进行测试

  • expIf(<exp>) 对应类: org.beetl.ext.spring.AccessExpressionIfFunction 参数: exp: Spring Security安全表达式缺省会直接返回true 返回值: 测试当前登录用户是否满足指定的安全表达式返回true or false 示例:

    expIf('isAuthenticated()')
    

如果当前环境不存在Spring Security安全上下文将返回true 如果当前环境不存在用户认证凭证,作为匿名登录进行测试

注意: 使用此方法必须开启Spring Security的expression功能(use-expressions="true")

<sec:http auto-config="true" use-expressions="true"></sec:http>

Spring Security Expression相关语法请阅读 http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#el-access

5.1.7. shiro

参考文档 https://my.oschina.net/xiandafu/blog/143109

5.2. 内置格式化方法

5.3. 内置标签函数

  • include include一个模板如 :

    <% include("/header.html"){} %>
    

    如果想往子模板中传入参数则可以后面跟一个json变量

    <% include("/header.html",{'user':user,'id',user.id}){} %>
    

    这样user和id 可以在header.html被引用并成为header.html的全局变量

    (beetl1.2 也叫includeFileTemplate 2.0仍然支持,但不再文档里体现了)

  • layout 提供一个布局功能,每个页面总是由一定布局,如页面头,菜单,页面脚,以及正文。 layout标签允许为正文指定一个布局如下使用方式

    content.html内容如下

    <%
    //content.html内容如下
    layout("/inc/layout.html"){ %>
    this is 正文
    ..........
    <% } %>
    

    layout.html 是布局文件,内容如下

    <% include("/inc/header.html"){} %>
    this is content:${layoutContent}
    this is footer:
    

    运行content.html模板文件后正文文件的内容将被替换到layoutContent的地方变成如下内容

    this is header
    this is content:this is 正文
    ............
    this is footer:
    

    如果想往layout页面传入参数则传入一个json变量如下往layout.html页面传入一个用户登录时间

    <% layout("/inc/header.html",{'date':user.loginDate,'title':"内容页面"}){ %>
    this is 正文
    ..........
    <% } %>
    

    如果layoutContent 命名有冲突可以在layout第三个参数指定

    <% layout("/inc/header.html",{'date':user.loginDate,'title':"内容页面"},"myLayoutContent"){ %>
    this is 正文
    ..........
    <% } %>
    
  • cache 能Cache标签的内容并指定多长时间刷新

    <% :cache('key2',10,false){  %>
    内容体
    <% } %>
    

    需要指定三个参数

    • 第一个是cache的Key值
    • 第二个是缓存存在的时间,秒为单位
    • 第三个表示是否强制刷新false表示不true表示强制刷新

    Cache默认实现org.beetl.ext.tag.cache.SimpleCacheManager. 你可以设置你自己的Cache实现通过调用CacheTag. cacheManager= new YourCacheImplementation();

    可以在程序里调用如下方法手工删除Cache

    public void clearAll();
    
    public void clearAll(String key);
    
    public void clearAll(String... keys);
    
  • includeJSP,可以在模板里包括一个jsp文件

    <%
    includeJSP("/xxxx.jsp",{"key":"value"}){}
    %>
    

    key value 都是字符串将以parameter的形式提供给jsp因此jsp可以通过request.getParameter("key")来获取参数

    主要注意的是,这个标签并非内置,需要手工注册一下

    groupTemplate.registerTag("incdlueJSP",org.beetl.ext.jsp.IncludeJSPTag.class);
    

5.4. 性能优化的秘密

Beetl2.0目前只完成了解释引擎使用解释引擎好处是可以适用于各种场景性能测试表明Beetl2.0引擎是Freemaker的46倍跟最好的 的编译引擎性能相比也相差只有30%百分点。为什么Beetl能跑的如此之快呢简单的说有如下策略

  • 优化IO输出允许使用字节直接输出模板中的静态文本事先转化为字节
  • encode优化对于number类型输出通常是.toString 转化成String然后encode输出这中间浪费了大量的资源Beetl实现了encode输出一步到位
  • Context 采用一维数组语言里的Context通常采用Map实现每次进入{} 就新增一个child Map尽管map很快但不够快。也有其他模板语言采用二位数组提高性能Beetl是通过固定大小一维数组来维护模板的Context因此访问更快也避免了Map和二维素组的频繁创建。其实除了此处beetl很多地方都不采用Map来维护key-value, 而都采用数组索引,以追求性能极限
  • 字节码访问属性通过反射获取性能比较慢就算JVM有优化但优化效果也不确定。Beetl通过字节码生成了属性访问类从而将属性访问速度提高了一个数量级
  • 类型推测Beetl 是强制类型的,因此预先知道类型,可以对模板做一些优化而省去了动态判断类型的时间
  • 使用数组Buffer避免频繁创建和销毁数组
  • 编译引擎将模板编译成类,会产生大量的类,虚拟机很难对这些做优化。而解释引擎只有几十个固定的类,虚拟机容易优化

相关文章

5.5. 性能测试对比

测试用例一 https://github.com/javamonkey/ebm

beetl1

测试用例二 http://git.oschina.net/kiang/teb

beetl2

测试用例三 https://github.com/javamonkey/template-benchmark

beetl3

Benchmark version Threads Samples Score Score Error (99.9%) Unit
Beetl 2.7 1 50 42125.112914 3512.147131 ops/s
Freemarker 2.3 1 50 13099.139808 339.612022 ops/s
Handlebars 4.0 1 50 15808.044125 235.109622 ops/s
Mustache 0.9 1 50 17961.391809 158.524109 ops/s
Rocker 0.1 1 50 33631.370722 417.915637 ops/s
Thymeleaf 3.0 1 50 4625.981276 67.313609 ops/s

注意

Score得分越高表示模板引擎每秒处理量越大性能越好

这个性能测试基本上结合了国内外的模板引擎随着JDK版本的升级JDK8提高了反射能力减少了和Freemarker等模板引擎的性能差距但Beetl依旧以3倍以上的性能优势秒杀Freemarker。

5.6. Beetl 开发团队

作者

  • 闲.大赋

助手

  • 作死模式:核心代码开发
  • 一粟蜉蝣:核心代码开发和版本发布

代码捐助者

  • 逝水fox 出色完成spring集成
  • kraken: 集合方法等扩展
  • 西安玛雅牛:复合加载器
  • 级?!: beetl扩展crossMVC
  • orangetys beetl插件
  • Oo不懂oO: beetl插件
  • 原上一颗草:Beetl早期使用者。
  • 龙图腾飞 WebErrorHandler用来开发模式在 web上显示错误而不是控制台
  • nutz: nutz 集成和MapResourceLoader
  • 天方地圆 :提供正则方法

文档校验

  • 九月
  • Daemons