FreeMarker用於處理模板的數據模型是哈希表,也就是一個樹狀結構的name-value對。如下:
(root)
|
+- string="string"
|
+- map
| |
| +- map1 = "map1"
| |
| +- map2 = "map2"
|
+- object
| |
| +- field1= "field1"
| |
| +- field2 = "field1"
| |
| +- method= "method"
|
......
在Java代碼中(下面第15行),我們提供給模板引擎的數據(process方法的第一個參數),可以是Map,也可以是自定義的Java對象。但是,模板引擎在處理時,並不是直接使用我們提供的類型。它會將其轉換為自己內部定義的類型,轉換工作由第8行的ObjectWrapper去完成,這種特性被稱作“對象包裝(Object Swapping)”。
查看源碼,關於Template#process(Object dataModel, Writer out)方法中dataModel參數的解釋如下:
dataModel是從模板中可以獲取的變量的容器(name-value對);
dataModel通常是一個Map<String, Object>或者是一個JavaBean(JavaBean的屬性將成為變量);
可以是BeanWrapper在執行時可以轉換為TemplateHashModel的任意對象;
也可以是實現了TemplateHashModel接口的對象,這種情況在執行時將直接使用,不再進行包裝;
如果是null,則使用一個空的數據模型。
1 public class FreeMarkerTest { 2 public static void main(String[] args) throws IOException, 3 TemplateException { 4 Configuration conf = new Configuration(); 5 // 設置加載模板文件的目錄
6 conf.setDirectoryForTemplateLoading(new File("src/templates")); 7 // 設置模板檢索數據模型的方式
8 conf.setObjectWrapper(new DefaultObjectWrapper()); 9 // 創建、解析模板並緩存
10 Template template = conf.getTemplate("example.flt"); 11 // 准備數據
12 Map<String, Object> root = new HashMap<String, Object>(); 13 root.put("example", "Hello World!"); 14 // 將數據與模板合並
15 template.process(root, new OutputStreamWriter(System.out)); 16 } 17 }
一、對象包裝(Object Swapping)
FreeMarker數據模型的哈希表中的name為字符串,value可以為標量、容器、子程序和節點等。這也是FreeMarker內部使用的變量的類型。這些類型都實現了freemarker.template.TemplateModel接口。
1. 標量:包括數值、字符串、日期、布爾。
2. 容器:包括哈希表(Map,類似於Java中的Map)、序列(Sequence,類似於Java中的數組和有序集合)、集合(Collection,類似於Java中的集合)。
3. 子程序:包括方法(Method)和指令(Directive)。
4. 節點:主要是為了幫助用戶在數據模型中處理XML文檔。
在模板處理時,會將Java類型包裝為對應的TemplateModel實現。比如將一個String包裝為SimpleScalar來存儲同樣的值。對於每個Java類型,具體選擇什么TemplateModel實現去包裝,取決於對象包裝器(ObjectWrapper)的實現策略,可通過上面代碼第8行設置。ObjectWrapper是一個接口,FreeMarker核心包提供了兩個基本的實現:ObjectWrapper.DEFAULT_WRAPPER、ObjectWrapper.BEANS_WRAPPER。
1. ObjectWrapper.DEFAULT_WRAPPER: 按照下表對應關系包裝,Jython類型包裝為freemarker.ext.jython.JythonWrapper,其它類型調用BEANS_WRAPPER。
2. ObjectWrapper.BEANS_WRAPPER:利用Java反射來獲取對象的成員屬性。
類型 | FreeMarker接口 | FreeMarker實現 |
字符串 | TemplateScalarModel | SimpleScalar |
數值 | TemplateNumberModel | SimpleNumber |
日期 | TemplateDateModel | SimpleDate |
布爾 | TemplateBooleanModel | TemplateBooleanModel.TRUE |
哈希 | TemplateHashModel | SimpleHash |
序列 | TemplateSequenceModel | SimpleSequence |
集合 | TemplateCollectionModel | SimpleCollection |
節點 | TemplateNodeModel | NodeModel |
ObjectWrapper的這兩個屬性現已經被標注為@deprecated,並建議用DefaultObjectWrapperBuilder#build()和BeansWrapperBuilder#build()方式獲取實例。如下:
// conf.setObjectWrapper(new DefaultObjectWrapperBuilder(new Version("2.3.22")).build());
conf.setObjectWrapper(new BeansWrapperBuilder(new Version("2.3.22")).build());
二、自定義方法
自定義方法需要實現freemarker.template.TemplateMethodModel接口(當前已@deprecated),建議替換為TemplateMethodModelEx。
例:實現累加方法sum(int...num)。參數可以有多個整數。
模板如下:
${sum(1, 2, 3, 4)}
代碼如下:
package org.genein.freemark.templateModel; import java.util.List; import freemarker.template.SimpleNumber; import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModelException; public class SumMethod implements TemplateMethodModelEx { @SuppressWarnings("rawtypes") @Override public Object exec(List arg0) throws TemplateModelException { if (arg0 == null || arg0.size() == 0) { return new SimpleNumber(0); } double sum = 0l; double tmp; for (int i = 0; i < arg0.size(); i++) { tmp = Double.valueOf(arg0.get(i).toString()); sum += tmp; } return new SimpleNumber(sum); } }
FreeMarkerTest.main方法添加以下代碼:
1 // 添加方法工具
2 root.put("sum", new SumMethod()); 3 // 將數據與模板合並
4 template.process(root, new OutputStreamWriter(System.out));
輸出如下:
10
三、自定義指令
自定義指令需要實現freemarker.template.TemplateDirectiveModel接口。
例:自定義一個指令,循環輸出內嵌內容,count參數決定循環次數(必填),hr參數決定是否添加分隔符“<hr>”(可選,默認false),step參數表示當前循環次數(可選)。
模板如下:
<@repeat count=5 hr=false; step> ${step}. ${name} </@repeat>
代碼如下:
package org.genein.freemark.templateModel; import java.io.IOException; import java.util.Map; import freemarker.core.Environment; import freemarker.template.SimpleNumber; import freemarker.template.TemplateBooleanModel; import freemarker.template.TemplateDirectiveBody; import freemarker.template.TemplateDirectiveModel; import freemarker.template.TemplateException; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateNumberModel; public class RepeatDirective implements TemplateDirectiveModel { /** * 循環次數 */
private static final String COUNT = "count"; /** * 是否需要用hr標簽間隔 */
private static final String HR = "hr"; @SuppressWarnings("rawtypes") @Override public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { // 獲取count參數,並校驗是否合法
TemplateModel countModel = (TemplateModel) params.get(COUNT); if (countModel == null) { throw new TemplateModelException("缺少必須參數count!"); } if (!(countModel instanceof TemplateNumberModel)) { throw new TemplateModelException("count參數必須為數值型!"); } int count = ((TemplateNumberModel) countModel).getAsNumber().intValue(); if (count < 0) { throw new TemplateModelException("count參數值必須為正整數!"); } // 獲取hr參數,並校驗是否合法
boolean hr = false; TemplateModel hrModel = (TemplateModel) params.get(HR); if (hrModel != null) { if (!(hrModel instanceof TemplateBooleanModel)) { throw new TemplateModelException("hr參數值必須為布爾型!"); } hr = ((TemplateBooleanModel) hrModel).getAsBoolean(); } // 檢驗內嵌內容是否為空
if (body == null) { throw new RuntimeException("內嵌內容不能為空!"); } // 最多只允許一個循環變量
if (loopVars.length > 1) { throw new TemplateModelException("最多只允許一個循環變量!"); } // 循環渲染內嵌內容
for (int i = 0; i < count; i++) { // 用第一個循環變量記錄循環次數
if (loopVars.length == 1) { loopVars[0] = new SimpleNumber(i + 1); } // 上面設置循環變量的操作必須在該render前面,因為內嵌內容中使用到了該循環變量。 // body.render(new UpperCaseFilterWriter(env.getOut()));
body.render(env.getOut()); if (hr) { env.getOut().write("<hr>"); } } } }
FreeMarkerTest.main增加以下代碼:
root.put("name", "Genein"); // 添加自定義指令
root.put("repeat", new RepeatDirective()); // 將數據與模板合並
template.process(root, new OutputStreamWriter(System.out));
輸出如下:
1. Genein
2. Genein
3. Genein
4. Genein
5. Genein