Beetl2.2使用說明書20151201


1. 什么是Beetl

Beetl目前版本是2.2.8,相對於其他java模板引擎,具有功能齊全,語法直觀,性能超高,以及編寫的模板容易維護等特點。使得開發和維護模板有很好的體驗。是新一代的模板引擎。總得來說,它的特性如下:

  • 功能完備:作為主流模板引擎,Beetl具有相當多的功能和其他模板引擎不具備的功能。適用於*各種應用場景*,從對響應速度有很高要求的大網站到功能繁多的CMS管理系統都適合。Beetl本身還具有很多獨特功能來完成模板編寫和維護,這是其他模板引擎所不具有的。

  • 非常簡單:類似Javascript語法和習俗,只要半小時就能通過半學半猜完全掌握用法。拒絕其他模板引擎那種非人性化的語法和習俗。同時也能支持html 標簽,使得開發CMS系統比較容易

  • 超高的性能:Beetl 遠超過主流java模板引擎性能(引擎性能5-6倍與freemaker,2倍於JSP。參考附錄),而且消耗較低的CPU

  • 易於整合:Beetl能很容易的與各種web框架整合,如Spring MVC,JFinal,Struts,Nutz,Jodd,Servlet等。

  • 支持模板單獨開發和測試,即在MVC架構中,即使沒有M和C部分,也能開發和測試模板。

  • 擴展和個性化:Beetl支持自定義方法,格式化函數,虛擬屬性,標簽,和HTML標簽. 同時Beetl也支持自定義占位符和控制語句起始符號也支持使用者完全可以打造適合自己的工具包.

關於性能

通過與主流模板引擎Freemarker,Vecloity以及JSP對比,Beetl6倍於Freemarker,2倍於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:8080/beetlonline/

2. 基本用法

2.1. 從GroupTemplate開始

1
2
3
4
5
6
7
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內置了4種,分別是

  • StringTemplateResourceLoader:字符串模板加載器,用於加載字符串模板,如本例所示

  • FileResourceLoader:文件模板加載器,需要一個根目錄作為參數構造,,傳入getTemplate方法的String是模板文件相對於Root目錄的相對路徑

  • ClasspathResourceLoader:文件模板加載器,模板文件位於Classpath里

  • WebAppResourceLoader:用於webapp集成,假定模板根目錄就是WebRoot目錄,參考web集成章

  • MapResourceLoader : 可以動態存入模板

代碼第5行將變量name傳入模板里,其值是“Beetl”。 代碼第6行是渲染模板,得到輸出,template提供了多種獲得渲染輸出的方法,如下

  • tempalte.render() 返回渲染結果,如本例所示

  • template.renderTo(Writer) 渲染結果輸出到Writer里

  • template.renderTo(OutputStream ) 渲染結果輸出到OutputStream里

  1. 關於如何使用模板資源加載器,請參考下一節

  2. 如何對模板進行配置,請參考下一節

2.2. 模板基礎配置

Beetl提供不但功能齊全,而且還有很多獨特功能,通過簡單的配置文件,就可以定義眾多的功能,默認情況下,Configuration類總是會先加載默認的配置文件(位於/org/beetl/core/beetl-default.properties,作為新手,通常只需要關注3,4,5,6行定界符的配置,以及11行模板字符集的配置就可以了,其他配置會在后面章節陸續提到)下,其內容片斷如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#默認配置
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的配置屬性,如果有重復,用后者代替前者的配置

1
2
3
#自定義配置
DELIMITER_STATEMENT_START=<!--:
DELIMITER_STATEMENT_END=-->

2.3. 模板資源加載器

資源加載器是根據String值獲取Resource實例的工場類,同時資源加載器還要負責響應模板引擎詢問模板是否變化的調用。對於新手來說,無需考慮模板資源加載器如何實現,只需要根據自己場景選擇系統提供的三類模板資源加載器即可

2.3.1. 字符串模板加載器

在創建GroupTemplate過程中,如果傳入的是StringTemplateResourceLoader,則允許通過調用gt.getTemplate(String template)來獲取模板實例對象,如2.1所示

2.3.2. 文件資源模板加載器

更通常情況下,模板資源是以文件形式管理的,集中放在某一個文件目錄下(如webapp的模板根目錄就可能是WEB-INF/template里),因此,可以使用FileResourceLoader來加載模板實例,如下代碼:

1
2
3
4
5
6
7
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.3.3. Classpath資源模板加載器

還有種常情況下,模板資源是打包到jar文件或者同Class放在一起,因此,可以使用ClasspathResourceLoader來加載模板實例,如下代碼:

1
2
3
4
5
6
ClasspathResourceLoader resourceLoader = new ClasspathResourceLoader(); Configuration cfg = Configuration.defaultConfiguration(); GroupTemplate gt = new GroupTemplate(resourceLoader, cfg); Template t = gt.getTemplate("/org/beetl/sample/s01/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.3.4. WebApp資源模板加載器

WebAppResourceLoader 是用於web應用的資源模板加載器,默認根路徑是WebRoot目錄。也可以通過制定root屬性來設置相對於WebRoot的的模板根路徑,從安全角考慮,建議放到WEB-INF目錄下

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

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

WebAppResourceLoader 假定 beetl.jar 是位於 WEB-INF/lib 目錄下,因此,可以通過WebAppResourceLoader類的路徑來推斷出WebRoot路徑從而指定模板根路徑。所有線上環境一般都是如此,如果是開發環境或者其他環境不符合此假設,你需要調用resourceLoader.setRoot() 來指定模板更路徑

2.3.5. 自定義資源模板加載器

有時候模板可能來自文件系統不同目錄,或者模板一部分來自某個文件系統,另外一部分來自數據庫,還有的情況模板可能是加密混淆的模板,此時需要自定義資源加載,繼承ResouceLoader才能實現模板功能,這部分請參考高級部分

2.4. 定界符與占位符號

Beetl模板語言類似JS語言和習俗,只需要將Beetl語言放入定界符號里即可,如默認的是<% %> ,占位符用於靜態文本里嵌入占位符用於輸出,如下是正確例子

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

千萬不要在定界符里使用占位符號,因為占位符僅僅嵌在靜態文本里,如下例子是錯誤例子

1
2
3
4
<%
var a = "hi"; var c = ${a}+"beetl"; //應該是var c = a+"beetl" %> 

每次有人問我如上例子為啥不能運行的時候,我總是有點憎惡velocity 帶來的這種非人性語法

定界符號里是表達式,如果表達式跟定界符有沖突,可以在表達式里用 “\” 符號,如

1
2
${[1,2,3]} //輸出一個json列表 ${ {key:1,value:2 \} } //輸出一個json map,} 需要加上\ 

定界符和占位符 通常還有別的選擇,如下定界符

  • @ 和回車換行 (此時,模板配置DELIMITER_STATEMENT_END= 或者 DELIMITER_STATEMENT_END=null 都可以)

  • #: 和回車換行

  • <!--: 和 -→

  • <!--# 和 -→

  • <? 和 ?>

占位符: - - #{ } - # #

你也可以與團隊達成一致意見來選擇團隊喜愛擇定界符號和占位符號。

2.5. 注釋

Beetl語法類似js語法,所以注釋上也同js一樣: 單行注釋采用//

多行注視采用/**/

1
2
3
4
5
6
7
8
<%
/*此處是一個定義變量*/ var a = 3; //定義一個變量. /* 以下內容都將被注釋 %> <% */ %> 

第2行是一個多行注釋

第3行是一個單行注釋

第5行到第8行采用的是多行注釋,因此里面有內容也是注釋,模板將不予處理

2.6. 臨時變量定義

在模板中定義的變量成為臨時變量,這類似js中采用var 定義的變量,如下例子

1
2
3
4
5
6
7
8
<%

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.7. 全局變量定義

全局變量是通過template.binding傳入的變量,這些變量能在模板的任何一個地方,包括子模板都能訪問到。如java代碼里

1
2
3
4
5
6
7
8
template.binding("list",service.getUserList()); //在模板里 <% for(user in list){ %> hello,${user.name}; <%}%> 

2.8. 共享變量

共享變量指在所有模板中都可以引用的變量,可過groupTemplate.setSharedVars(Map<String, Object> sharedVars)傳入的變量,這些變量能在 所有模板 的任何一個地方

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.....
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); 
1
2
3
4
//t1.txt
hi,${name} //t2.txt hello,${name} 

2.9. 模板變量

模板變量是一種特殊的變量,即可以將模板中任何一段的輸出賦值到該變量,並允許稍后在其他地方使用,如下代碼

1
2
3
4
5
6
7
8
<%
var content = { var c = "1234"; print(c); %> 模板其他內容<%}; %> 

第2行定義了一個模板變量content = { …} ; 此變量跟臨時變量一樣,可以在其他地方使用,最常見的用戶是用於復雜的布局。請參考高級用法布局

2.10. 引用屬性

屬性引用是模板中的重要一部分,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屬性名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template.binding("list",service.getUserList()); template.binding("pageMap",service.getPage()); //在模板里 總共 ${list.~size} <% for(user in list){ %> hello,${user.name}; <%}%> 當前頁${pageMap['page']},總共${pageMap["total"]} 

2.11. 算數表達式

Beetl支持類似javascript的算術表達式和條件表達式,如+ - * / % 以及(),以及自增++,自減--

1
2
3
4
5
6
7
8
<%
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.12. 邏輯表達式

Beetl支持類似Javascript,java的條件表達式 如>, <, == ,!=,>= , ⇐ 以及 !, 還有&&和 || ,還有三元表達式等,如下例子

1
2
3
4
5
6
7
8
9
<%
var a = 1; var b=="good"; var c = null; if(a!=1&&b=="good"&&c==null){ ...... } %> 

三元表達式如果只考慮true條件對應的值的話,可以做簡化,如下倆行效果是一樣的。

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

2.13. 循環語句

Beetl支持豐富的循環方式,如for-in,for(exp;exp;exp),以及while循環,以及循環控制語句break;continue; 另外,如果沒有進入for循環體,還可以執行elsefor指定的語句。

2.13.1. for-in

for-in循環支持遍歷集合對象,對於List和數組來說以及Iterator,對象就是集合對象,對於Map來說,對象就是Map.entry,如下倆個例子

1
2
3
4
5
6
7
<%
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使用例子

1
2
3
4
5
6
7
8
<%
for(entry in map){ var key = entry.key; var value = entry.value; print(value.name); } %> 

2.13.2. for(exp;exp;exp)

對於渲染邏輯更為常見的是經典的for循環語句,如下例子

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

2.13.3. while

對於渲染邏輯同樣常見的有的while循環語句,如下例子

1
2
3
4
5
6
7
<%
var i = 0; while(i<5){ print(i); i++; } %> 

2.13.4. elsefor

不同於通常程序語言,如果沒有進入循環體,則不需額外的處理,模板渲染邏輯更常見情況是如果沒有進入循環體,還需要做點什么,因此,對於for循環來說,還有elsefor 用來表達如果循環體沒有進入,則執行elsefor 后的語句

1
2
3
4
5
6
7
8
<%
var list = []; for(item in list){ }elsefor{ print("未有記錄"); } %> 

2.14. 條件語句

2.14.1. if else

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<%
var a =true; var b = 1; if(a&&b==1){ }else if(a){ }else{ } %> 

2.14.2. switch-case

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<%
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.14.3. select-case

select-case 是switch case的增強版。他允許case 里有邏輯表達式,同時,也不需要每個case都break一下,默認遇到合乎條件的case執行后就退出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<%
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.其格式是

1
2
3
4
5
6
select { case boolExp,orBoolExp2: doSomething(); } %> 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<%
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.15. try-catch

通常模板渲染邏輯很少用到try-catch 但考慮到渲染邏輯復雜性,以及模板也有不可控的地方,所以提供try catch,在渲染失敗的時候仍然能保證輸出正常

1
2
3
4
5
6
7
8
<%
try{ callOtherSystemView() }catch(error){ print("暫時無數據"); } %> 

error代表了一個異常,你可以通過error.message 來獲取可能的錯誤信息

也可以省略catch部分,這樣出現異常,不做任何操作

2.16. 虛擬屬性

虛擬屬性也是對象的屬性,但是虛擬的,非模型對象的真實屬性,這樣的好處是當模板需要額外的用於顯示的屬性的時候但又不想更改模型,便可以采用這種辦法 如beetl內置的虛擬屬性.~size 針對了數組以及集合類型。

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

~genderShowName 是虛擬屬性,其內部實現根據boolean變量gender來顯示性別

如何完成虛擬屬性,請參考高級用法

2.17. 函數調用

Beetl內置了少量實用函數,可以在Beetl任何地方調用。如下例子是調用date 函數,不傳參數情況下,返回當前日期

1
2
3
4
5
6
<%
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") 獲取該變量

2.18. 安全輸出

安全輸出是任何一個模板引擎必須重視的問題,否則,將極大困擾模板開發者。Beetl中,如果要輸出的模板變量為null,則beetl將不做輸出,這點不同於JSP,JSP輸出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將會忽略此異常,繼續渲染

值得注意的是,在變量后加上!不僅僅可以應用於占位符輸出(但主要是應用於占位符輸出),也可以用於表達式中,如:

1
2
3
4
5
6
7
8
<%

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

如果user為null,則k值將為N/A

在有些模板里,可能整個模板都需要安全輸出,也可能模板的部分需要安全輸出,使用者不必為每一個表達式使用!,可以使用beetl的安全指示符號來完成安全輸出 如:

1
2
3
4
5
6
7
8
9
<%
DIRECTIVE SAFE_OUTPUT_OPEN; %> ${user.wife.name} 模板其他內容均能安全輸出…… <% //關閉安全輸出。 DIRECTIVE SAFE_OUTPUT_CLOSE; %> 

Beetl不建議每一個頁面都使用DIRECTIVE SAFE_OUTPUT_OPEN,這樣,如果如果真有不期望的錯誤,不容易及時發現,其次,安全輸出意味着beetl會有額外的代碼檢測值是否存在或者是否為null,性能會略差點。所以建議及時關閉安全輸出(這不是必須的,但頁面所有地方是安全輸出,可能不容易發現錯誤)

在for-in 循環中 ,也可以為集合變量增加安全輸出指示符號,這樣,如果集合變量為null,也可以不進入循環體,如:

1
2
3
4
5
6
7
8
<%
var list = null; for(item in list!){ }eslefor{ print("no data"); } %> 

2.18.1. 變量是否存在

判斷變量是否存在,可以采用內置的has或者isEmpty方法來判斷,參數是變量,如

1
2
3
4
5
<%
if(has(flag)){ print("not exit") } %> 

如果需要判斷變量是否存在,如果存在,還有其他判斷條件,通常都這么寫

1
2
3
4
5
<%
if(has(flag)||flag==0){ //code } %> 

如果flag不存在,或者flag存在,但值是0,都將執行if語句

但是,有更為簡便的方法是直接用安全輸出,如

1
2
3
4
5
<%
if(flag!0==0){ //code } %> 

flag!0 取值是這樣的,如果flag不存在,則為0,如果存在,則取值flag的值,類似三元表達式 has(flag)?falg:0

2.18.2. 安全輸出表達式

安全輸出表達式可以包括

  • 字符串常量,如 ${user.count!"無結果"}

  • boolean常量 ${user.count!false}

  • 數字常量,僅限於正數,因為如果是負數,則類似減號,容易誤用,因此,如果需要表示負數,請用括號,如${user.count!(-1)}

  • class直接調用,如${user.count!@User.DEFAULT_NUM}

  • 方法調用,如 ${user.count!getDefault() }

  • 屬性引用,如 ${user.count!user.maxCount }

  • 任何表達式,需要用括號

2.19. 格式化

幾乎所有的模板語言都支持格式化,Beetl也不列外,如下例子Beetl提供的內置日期格式

1
2
3
4
<% 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 設置了了格式化函數,因此上面的例子可以簡化為

1
${date,yyyy-MM-dd}. 

Beetl針對日期和數字類型提供的默認的格式化函數,在org/beetl/core/beetl-default.properties里,注冊了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
##內置的格式化函數 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.20. 標簽函數

所謂標簽函數,即允許處理模板文件里的一塊內容,功能等於同jsp tag。如Beetl內置的layout標簽

index.html

1
2
3
4
5
<%
layout("/inc/layout.html",{title:'主題'}){ %> Hello,this is main part <%} %> 

layout.html

1
2
3
title is ${title} body content ${layoutContent} footer 

第1行變量title來自於layout標簽函數的參數

第2行layoutContent 是layout標簽體{}渲染后的結果

關於layout標簽,參考高級主題布局

Beetl內置了另外一個標簽是include,允許 include 另外一個模板文件

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

在標簽中,{} 內容將依據標簽的實現而執行,layout標簽將執行{}中的內容,而include標簽則忽略標簽體內容。

關於如何實現標簽函數,請參考高級主題,如下是一個簡單的的標簽函數:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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.21. HTML標簽

Beetl 也支持HTML tag形式的標簽, 區分beetl的html tag 與 標准html tag。如設定HTML_TAG_FLAG=#,則如下html tag將被beetl解析

1
2
3
<#footer style=simple/> <#richeditor id=ridpath="${ctxPath}/upload" name=rnamemaxlength=${maxlength}> ${html} 其他模板內容 </#richdeitor> <#html:input id=aaaa/> 

如對於標簽footer,Beetl默認會尋找WebRoot/htmltag/footer.tag(可以通過配置文件修改路徑和后綴) ,內容如下:

1
2
3
4
5
<%if(style==simple){%> 請聯系我 ${session.user.name} <%}else{%> 請聯系我 ${session.user.name},phone:${session.user.phone} <%}%> 

如下還包含了自定義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文件里自行格式化

  • 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] 則是標簽的屬性,參數是個map,key是html tag的屬性,value是其屬性值,如下用java完成的html 標簽用於輸出屬性值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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

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

HTML_TAG_FLAG默認為#用來區別是否是beetl的html tag,你也可以設置成其他符號,比如 "my:",這樣,<my:table></my:table> 其實是一個指向table.tag的標簽實現

2.22. 綁定變量的HTML標簽

對於html標簽(參考上一節),Beetl還 支持將標簽實現類(java代碼)里的對象作為臨時變量,被標簽體引用。此時需要實現GeneralVarTagBinding (此類是Tag的子類) 該類提供另外3個個方法 - void binds(Object… array) 子類在render方法里調用此類以實現變量綁定,綁定順序同在模板中申明的順序 - void bind(String name, Object value) 子類在render方法里調用此類以實現變量綁定,name是模板中申明的變量名,用此方法綁定不如binds更靈活,不再推薦 - Object getAttributeValue 獲得標簽的屬性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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。我們再看看模板如何寫的

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

類似於常規html標簽,需要在標簽的最后的屬性定義后面加上分號 ";" 此分號表示這個是一個需要在標簽運行時需要綁定變量的標簽。后跟上要綁定的變量列表,如上例只綁定了一個value變量,如果需要綁定多個變量,則用逗號分開,如var1,var2 上。如果后面沒有變量列表,只有分號,則默認綁定到標簽名同名的變量上. 如果標簽有namesapce,則默認綁定訂的變量名不包含namespace

注意,由於標簽使用因為太長可能換行或者是文本格式化導致換行,目前beetl只允許在屬性之間換行,否則,將報標簽解析錯誤。

默認情況下,如果標簽屬性出現了var(可以通過配置文件改成其他屬性名),也認為是綁定變量的標簽,如上面的例子也可以這么寫

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

var屬性的值可以是個以逗號分開的變量名列表,如var="total,customer,index"

2.23. 直接調用java方法和屬性

1
2
3
4
5
6
7
${@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; %> 

可以調用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

2.24. 嚴格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.25. 指令

指令格式為: DIRECTIVE 指令名 指令參數(可選) Beetl目前支持安全輸出指令,分別是

  • DIRECTIVE SAFE_OUTPUT_OPEN ; 打開安全輸出功能,此指令后的所有表達式都具有安全輸出功能,

  • DIRECTIVE SAFE_OUTPUT_CLOSE ; 關閉安全輸出功能。詳情參考安全輸出

  • DIRECTIVE DYNAMIC varName1,varName2 …指示后面的變量是動態類型,Beetl應該考慮為Object. 也可以省略后面的變量名,則表示模板里所有變量都是Object

1
2
<% DIRECTIVE DYNAMIC idList; for(value in idList) ..... 

DYNAMIC 通常用在組件模板里,因為組件模板可以接收任何類型的對象。如列表控件,可以接收任何含有id和 value屬性的對象。

1 注意 DYNAMIC 后的變量名也允許用引號,這主要是兼容Beetl1.x版本

2 Beetl1.x 指令都是大寫,當前版本也允許小寫,如 directive dynamic idList

2.26. 類型聲明

Beetl 本質上還是強類型的模板引擎,即模板每個變量類型是特定的,在模板運行過程中,beetl 會根據全局變量自動推測出模板中各種變量和表達式類型。 也可以通過類型申明來說明beetl全局變量的類型,如下格式

1
2
3
4
5
<%
/** *@type (List<User> idList,User user) */ for(value in idList) ..... 

類型申明必須放到多行注釋里,格式是@type( … ),里面的申明類似java方法的參數申明。正如你看到的類型申明是在注釋里,也就表明了這在Beetl模板引擎中不是必須的,或者你只需要申明一部分即可,之所以提供可選的類型說明,是因為

  • 提高一點性能

  • 最重要的是,提高了模板的可維護性。可以讓模板維護者知道變量類型,也可以讓未來的ide插件根據類型聲明來提供屬性提示,重構等高級功能

需要注意的是,如果在類型聲明里提供的是類名,而不是類全路徑,這樣必須在配置文件里申明類的搜索路徑((需要設置配置IMPORT_PACKAGE=包名.;包名.,或者調用Configuration.addPkg)),默認的搜索路徑有java.util. 和 java.lang.

2.27. 錯誤處理

Beetl能較為詳細的顯示錯誤原因,包括錯誤行數,錯誤符號,錯誤內容附近的模板內容,以及錯誤原因,如果有異常,還包括異常和異常信息。 默認情況下,僅僅在控制台顯示,如下代碼:

1
2
3
4
<%
var a = 1; var b = a/0; %> 

運行此模板后,錯誤提示如下:

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

運行此模板后

1
2
3
4
5
6
>>缺少符號(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.28. 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的變量

1
2
3
4
String template = "var a=1,c=2+1;"; Map result = executeAndReturnRootScopeVars(template); System.out.println(result); //輸出結果是{c=3, a=1} 

BeetlKit 不要用於線上系統。僅僅作為體驗Beetl功能而提供的,如果需要在線上使用這些功能,請參考該類源碼自行擴展

2.29. 變量不存在

對於編程語言來說,變量不存在則在編譯期就會報錯,對於那種解釋語言,變量也是要么存在,要么不存在,不可能一段代碼里有倆種情況,但對於模板語言來說,一段模板里的變量可能有這倆種狀態,如新增和修改共用一個模板文件就會出現這種情況。如果變量有可能不存在,則用安全輸出符號"!",如下代碼:

1
<span>${user.name!}</span> 

對於邏輯表達式,也可以使用安全輸出符號,如:

1
2
3
4
<%
if(null==user.name!) //安全輸出符號后如果未有表達式,則返回null if(''==user.name!'') //如果user.name不存在或者為null,則返回空字符串。或者user.name本身是空字符串 %> 

系統提供了一些函數,也可以用於判斷變量是否存在

1
2
3
4
5
6
7
8
<%
if(has(user)) //接受一個變量名,如果存在user變量,返回true if(isEmpty(user.name) /* 如果user不存在,或者為null,或者user.name是空為null, 或者user.name是空字符串,都返回true */ %> 

isEmtpy 可以接受表達式,可以判斷集合,數組,字符串為空情況,詳細參考該函數說明

2.30. 瑣碎功能

  • 對齊:我發現別的模板語言要是做到對齊,非常困難,使用Beetl你完全不用擔心,比如velocty,stringtemlate,freemarker例子都出現了不對齊的情況,影響了美觀,Beetl完全無需擔心輸出對齊

  • Escape:可以使用\ 做escape 符號,如\$monkey\$ 將作為一個普通的文本,輸出為$monkey$.再如為了在后加上美元符號(占位符恰好又是美元符號)可以用這倆種方式hello,it’s $money$\$, 或者Hello,it’s $money+"\$"$ 。如果要輸出\符號本生,則需要用倆個\\,這點與javascript,java 語義一致.

3. 高級用法

3.1. 配置GroupTemplate

Beetl建議通過配置文件配置配置GroupTemplate,主要考慮到未來可能IDE插件會支持Beetl模板,模板的屬性,和函數等如果能通過配置文件獲取,將有助於IDE插件識別。 配置GroupTemplate有倆種方法

  • 配置文件: 默認配置在/org/beetl/core/beetl-default.properties 里,Beetl首先加載此配置文件,然后再加載classpath里的beetl.properties,並用后者覆蓋前者。配置文件通過Configuration類加載,因此加載完成后,也可以通過此類API來修改配置信息

  • 通過調用GroupTemplate提供的方法來注冊函數,格式化函數,標簽函數等

配置文件分為三部分,第一部分是基本配置,在第一節講到過。第二部分是資源類配置,可以在指定資源加載類,以及資源加載器的屬性,如下

1
2
3
4
5
6
RESOURCE_LOADER=org.beetl.core.resource.ClasspathResourceLoader #資源配置resource后的屬性只限於特定ResourceLoader #classpath 根路徑 RESOURCE.root= / #是否檢測文件變化 RESOURCE.autouCheck= true 

第一行指定了類加載器,第二行指定了模板根目錄的路徑,此處/ 表示位於classpath 根路徑下,第三行是否自動檢測模板變化,默認為true,開發環境下自動檢測模板是否更改。關於如何如何自定義ResouceLoader,請參考下一章

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#####  擴展 ############## ## 內置的方法 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前綴表示Function,fnp前綴表示FunctionPackage,FT表示format函數,FTC表示類的默認Format函數,TAG表示標簽類。Beetl強烈建議通過配置文件加載擴展。以便隨后IDE插件能識別這些注冊函數

3.2. 自定義方法

3.2.1. 實現Function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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即可,如

1
2
3
4
5
6
7
8
public class util { public String print(Object a, Context ctx) { ............... } 

注意

1 從beetl效率角度來講,采用普通類效率不如實現Function調用

2 采用的普通java類盡量少同名方法。這樣效率更低。beetl調用到第一個適合的同名方法。而不像java那樣找到最匹配的

3 方法名支持可變數組作為參數

4 方法名最后一個參數如果是Context,則beetl會傳入這個參數。

3.2.3. 使用模板文件作為方法

可以不用寫java代碼,模板文件也能作為一個方法。默認情況下,需要將模板文件放到Root的functions目錄下,且擴展名為.html(可以配置文件屬性來修改此倆默認值) 方法參數分別是para1,para2…..

如下root/functions/page.fn

1
2
3
4
5
<%
//para0,para1 由函數調用傳入 var current = para0,total = para1,style=para2!'simple' %> 當前頁面 ${current},總共${total} 

則在模板中

1
2
3
<%
page(current,total); %> 

允許使用return 表達式返回一個變量給調用者,如模板文件functions\now.html

1
2
3
<%
        return date(); %> 

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

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

也可以在functions建立子目錄,這樣function則具有namespace,其值就是文件夾名

3.3. 自定義格式化函數

需要實現Format接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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,獲取外的格式化信息。

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

3.4. 自定義標簽

標簽形式有倆種,一種是標簽函數,第二種是html tag。第二種實際上在語法解析的時候會轉化成第一種,其實現是HTMLTagSupportWrapper,此類將會尋找root/htmltag目錄下同名的標簽文件作為模板來執行。類似普通模板一樣,在此就不詳細說了

3.4.1. 標簽函數

標簽函數類似jsp2.0的實現方式,需要實現Tag類的render方法即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class DeleteTag extends Tag { @Override public void render() { // do nothing,just ignore body ctx.byteWriter.write("被刪除了,付費可以看") } } 

如上一個最簡單的Tag,將忽略tag體,並輸出內容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class XianDeDantengTag extends Tag { @Override public void render() { doBodyRender(); } } 

此類將調用父類方法doBodyRender,渲染tag body體

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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方法可以為特定類注冊一個需要屬性,如下代碼:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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方法將判斷是否應用虛擬屬性到此類

如下是虛擬屬性類的定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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)來完成;

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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類。如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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); /** 用於include,layout等根據相對路徑計算資源實際的位置.  * @param resource 當前資源  * @param key  * @return  */ public String getResourceId(Resource resource, String key); } 

如下是一個簡單的內存ResourceLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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下的所有模板,並注冊為方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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返回一個Clob,isModified 則需要根據改模板內容對應的lastUpdate(通常數據庫應該這么設計)來判斷模板是否更改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public abstract class Resource { /**  * 打開一個新的Reader  *  * @return  */ public abstract Reader openReader(); /**  * 檢測資源是否改變  *  * @return  */ public abstract boolean isModified(); 

參考例子可以參考beetl自帶的ResourceLoader

3.8. 使用CompositeResourceLoader

組合加載器,可以包含多個已有的ResourceLoader,如下代碼創建一個包含倆個文件和內存的ResourceLoader

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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文件內容

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

第2行仍然是由fileLoader1加載,但第3行以http:前綴開頭,因此將fileLoader2加載path2+/xxx.html.xxx.html內容如下

1
2
3
<%
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方法,打印異常后拋出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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) 方法

如下是默認管理器的實現方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

1
2
3
4
5
6
 <%
 //content.html內容如下: layout("/inc/layout.html"){%> this is 正文 .......... <%}%> 

如上一個子頁面將使用layout布局頁面,layout 頁面內容如下

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

layoutContent 是默認變量,也可以改成其他名字,具體請參考layout標簽函數

全局變量總是能被布局用的頁面所使用,如果布局頁面需要臨時變量,則需要顯示的傳入,如:

1
2
3
 <%
 var user= model.user; include("/inc/header.html",{title:'這是一個測試頁面',user:user}){} %> 

這樣,title和user成為全局變量,能被header.html 及其子頁面引用到

  • 繼承布局:采用模板變量和include

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
 <%
        var jsPart = { %> web頁面js部分 <%};%> <% var htmlPart = { %> web頁面html部分 <%}; include("/inc/layout.html",{jsSecition:jsPart,htmlSection:htmlPart}){} %> 

layout.html頁面如下:

1
2
3
4
5
6
7
8
<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. 定制模板引擎

Beetl在線體驗(http://ibeetl.com:8080/beetlonline/)面臨一個挑戰,允許用戶輸入任何腳本做練習或者分享代碼。但又需要防止用戶輸入惡意的代碼,如

1
2
3
4
5
<%
for(var i=0;i<10000000;i++){ //其他代碼 } %> 

此時,需要定制模板引擎,遇到for循環的時候,應該限制循環次數,譬如,在線體驗限制最多循5次,這是通過定義替換GeneralForStatement類來完成的,這個類對應了for(exp;exp;exp) ,我們需要改成如下樣子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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 loop,Ignore 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,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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出一個腳本做分析優化,因此,倆個腳本都需要做修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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; } } 
  • StatementParser 是關鍵類,他允許對模板的Program進行解析,並替換其中的Statement。parser.addListener 方法接受倆個參數,第一個是需要找的類,第二個是執行的監聽器。

  • 可以參考在線體驗的源碼:http://git.oschina.net/xiandafu/beetlonline/blob/master/src/org/bee/tl/online/OnlineTemplateEngine.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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,則不需替換。這通常發生在你僅僅通過修改該類的某些屬性就可以的場景

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

1
ENGINE=org.bee.tl.online.OnlineTemplateEngine 

這樣就完成了模板引擎定制。

3.16. 直接運行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

如下腳本(此時就不需要腳本定界符了)

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

調用runScript后,map里將返回key分別為a,b,c,return。 值分別為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以上版本支持)

你可以在模板任何地方訪問這些變量

4.2. 集成技術開發指南

Beetl默認提供了WebRender用於幫助web集成開發,所有內置的集成均基於此方法。如果你認為Beetl內置的各個web框架集成功能不夠,你可以繼承此類,或者參考此類源碼重新寫,其代碼如下

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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); 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)方法。例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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集成

需要做如下配置即可

1
2
3
4
5
6
<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,可以調用如下代碼

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

Controller代碼如下:

1
2
3
4
5
6
7
@RequestMapping(value = "/", method = RequestMethod.GET) public ModelAndView index(HttpServletRequest req) { ModelAndView view = new ModelAndView("/index"); //total 是模板的全局變量,可以直接訪問 view.addObject("total",service.getCount()); return view; } 

4.5. SpringMVC集成高級

spring集成還允許注冊被spring容器管理的Function,Tag等,也還允許配置多個視圖解析器等功能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<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容器管理的function,key為注冊的方法名,value-ref 指定的bean的名稱

  • functionPackages,指定了被spring容器管理的functionPackage,key為注冊的方法包名,value-ref 指定的bean的名稱

  • tagFactorys ,注冊tag類,key是tag類的名稱,value-ref指向一個org.beetl.ext.spring.SpringBeanTagFactory實例,該子類是一個Spring管理的Bean。屬性name對應的bean就是tag類。需要注意,由於Tag是有狀態的,因此,必須申明Scope為 "prototype"。如代碼:

1
2
3
4
@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<VirtualAttributeEval>

  • resourceLoader,資源加載器 ,值是 實現ResourceLoader的一個Bean

  • errorHandler ,錯誤處理,值是實現ErrorHandler的一個Bean

  • sharedVars,同functions,類型是Map<String, Object>,可以在此設置共享變量

  • configProperties,類型是Properties,可以覆蓋配置文件的某些屬性

如下配置,指定了三個視圖解析器,一個用於beetl頁面渲染,一個用於cms,采用了beetl技術,另外一個一些遺留的頁面采用jsp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<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應用的根目錄

4.6. Jodd集成

需要配置web.xml,將所有請求交給jodd處理,參考:http://jodd.org/doc/madvoc/setup.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<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類

1
2
3
4
5
6
7
8
9
public class MyAutomagicMadvocConfigurator extends AutomagicMadvocConfigurator { public MyAutomagicMadvocConfigurator(){ super(); //不掃描beetl 里jar文件里的action和result,否則,會掃描StrutsResultSupport不相干的class this.rulesJars.exclude("**/*beetl*.jar"); } } 
1
2
3
4
5
6
7
8
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 即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@MadvocAction
public class IndexAction { @Out String value; @Action("/index.html") public String world() { value = "Hello World!"; return "/ok.html"; } } 

4.7. JFinal集成

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 ; } 

業務邏輯代碼:

1
2
3
4
5
6
7
8
9
        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目錄下,如果你需要修改到別的目錄,可以設置配置文件,如

1
RESOURCE.root= /WEB-INF/template/ 

https://git.oschina.net/xiandafu/beetl-jfinal-sample 有完整例子,采用jfinal+beetl寫的一個博客系統

4.8. Nutz集成

Nutz集成提供了 BeetlViewMaker ,實現了 ViewMaker方法,如下代碼

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        @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; } 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<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> 

4.9. Struts2集成

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<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屬性作為全局變量傳遞給模板

鄭重申明

鑒於struts2有安全漏洞,而官方補丁打法很消極,所以請謹慎使用Struts2,Beetl的安全性已經通過在線體驗和多個使用Beetl的網站得以體現 一旦你的struts2網站被攻破,請先確定是否是struts2 的問題

4.10. 直接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可以申明這些些變量

1
2
3
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.11. 整合ajax的局部渲染技術

越來越多web網站依賴於ajax,如table的翻頁,流行方式是瀏覽器發出ajax請求,后台處理后返回一個json,瀏覽器端將json數據拆開,拼成一條一條的行數據,然后生成dom節點,追加到表格里。 作為另外一種可選技術,beetl支持局部渲染技術,允許后台處理返回的是一個完成的html片段,這樣,前端瀏覽器可以直接將這個html片段追加到表格里。在我做的性能測試里,倆種方式性能差別不大(http://beetlajax.oschina.mopaas.com/)

比如模板index.html有很多動態內容,有動態生成的菜單,有右側的top10,也有核心區域的表格,大概內容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<#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/

注意,Ajax片段本質上是從模版的ajax標記處開始渲染,因此,ajax需要的變量在模版里也必須是全局變量,如果你只是個局部變量,beetl會報出找不到變量,即使你binding了這個變量,beetl也認為這個是局部變量,如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
" >
<%
var tableData = paras.table;
#ajax userTable: {
for(user in tableData);
%>

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

變量tableData是從paras里獲取的,是個臨時變量,因此就算你在后台binding了一個tableData,beetl 也不能識別。在渲染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消耗? 模板方式消耗更多的cpu,json方式則少點。但是倆者差距並不大。而且更多的web網站面臨的情況是有富余的服務器CPU能力

  • 關心客戶端CPU消耗? 過多的js無疑是客戶端運行慢的主要原因。如果采用經典的json方式,返回的json數據必然還需要經過js的計算和渲染。會影響客戶機器cpu。

4.12. 在頁面輸出錯誤提示信息

2.2.3版本以后,新增加org.beetl.ext.web.WebErrorHandler,可以在web開發的時候在頁面輸出提示信息,在產品模式下載后台輸出提示信息(通過配置屬性ESOURCE.autoCheck= true來認為是開發模式),僅僅需要配置如下:

1
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") 獲取該變量

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.2. 內置格式化方法

5.3. 內置標簽函數

  • include include一個模板,如 :

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

如果想往子模板中傳入參數,則可以后面跟一個json變量

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

這樣user,和id 可以在header.html被引用,並成為header.html的全局變量

(beetl1.2 也叫includeFileTemplate ,2.0仍然支持,但不再文檔里體現了)

  • layout 提供一個布局功能,每個頁面總是由一定布局,如頁面頭,菜單,頁面腳,以及正文。 layout標簽允許為正文指定一個布局,如下使用方式

    content.html內容如下:
1
2
3
4
5
6
 <%
 //content.html內容如下: layout("/inc/layout.html"){%> this is 正文 .......... <%%}%> 
layout.html 是布局文件,內容如下•
1
2
3
4
5
6
 <%
 <%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頁面傳入一個用戶登錄時間

1
2
3
4
5
6
 <%
  layout("/inc/header.html",{'date':user.loginDate,'title':"內容頁面"}){%> this is 正文 .......... <%%}%> 

如果layoutContent 命名有沖突,可以在layout第三個參數指定,如

1
2
3
4
5
6
7
 <%

 layout("/inc/header.html",{'date':user.loginDate,'title':"內容頁面"},"myLayoutContent"){%> this is 正文 .......... <%}%> 
  • cache 能Cache標簽的內容,並指定多長時間刷新,如

1
2
3
<%:cache('key2',10,false){ %> 內容體 <%}%> 

需要指定三個參數,第一個是cache的Key值,第二個是緩存存在的時間,秒為單位,第三個表示是否強制刷新•,false表示不,true表示強制刷新 Cache默認實現org.beetl.ext.tag.cache.SimpleCacheManager. 你可以設置你自己的Cache實現,通過調用CacheTag. cacheManager= new YourCacheImplementation();

可以在程序里調用如下方法手工刪除Cache:

1
2
3
4
5
public void clearAll(); public void clearAll(String key); public void clearAll(String... keys); 
  • includeJSP,可以在模板里包括一個jsp文件,如:

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

key value 都是字符串,將以parameter的形式提供給jsp,因此jsp可以通過request.getParameter("key")來獲取參數

主要注意的是,這個標簽並非內置,需要手工注冊一下

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

5.4. 性能優化的秘密

Beetl2.0目前只完成了解釋引擎,使用解釋引擎好處是可以適用於各種場景,性能測試表明,Beetl2.0引擎是Freemaker的4-6倍,跟最好的 的編譯引擎性能相比,也相差只有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. 性能測試對比

performance.jpg

performance2.png

5.6. Beetl 開發團隊

作者

  • 閑.大賦:

助手

  • 作死模式:核心代碼開發

  • 一粟蜉蝣:核心代碼開發和版本發布

代碼捐助者

  • 逝水fox :出色完成spring集成

  • kraken: 集合方法等擴展

  • 西安瑪雅牛:復合加載器

  • 級?!: beetl擴展,crossMVC

  • orangetys: beetl插件

  • Oo不懂oO: beetl插件

  • 原上一顆草:Beetl早期使用者。

  • 龍圖騰飛 ,WebErrorHandler,用來開發模式在 web上顯示錯誤而不是控制台

  • nutz: nutz 集成和MapResourceLoader

  • 天方地圓 :提供正則方法

文檔校驗

  • 九月

  • Daemons


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM