1. 程式人生 > >freemarker 自定義TemplateDirectiveModel

freemarker 自定義TemplateDirectiveModel

在採用FreeMarker做前臺檢視模板的情況下,我們可以通過<#include>標籤和自定義巨集來解決很多重複性工作。

一個簡單的FreeMarker巨集:

  1. <#macro sayHello name="">
  2.     hello ${name}  
  3. </#macro>
然後通過如下的形式呼叫:
  1. <@sayHello name="shannon"/>
不過這種在模板頁中定義的巨集能力有限。【1】假設,我們很多頁面都要輸出一個熱門排行框,而排行資料需要從controller層動態獲取,我們可以用這種巨集來完成所有的展示工作,但前提是相應的controller和介面中層需要預先將這些排行資料放到model中去,因此對於後端來說這也是一個重複性的工作。那麼有沒有一種方式可以讓後端也脫離這種重複工作呢?答案是肯定的,這也是寫這篇部落格的目的。

在一個偶然的機會發現jeecms專案中用到了這種方式,於是借鑑了一番。

FreeMarker不僅可以在前端的模板頁中定義巨集,還可以通過擴充套件其介面在後端實現巨集,這有什麼好處呢?這種方式就好比讓你的模板頁具備了從前端再次回到後端的能力。這樣我們就能很好的解決【1】處的假設,我們無需在各個controller的各個介面中去重複的向model中新增所需的排行資料,而是當FreeMarker渲染模板頁時遇到相應的巨集它可以回到後端去呼叫相應的方法取到所需的資料。例子如下:

  1. import freemarker.core.Environment;  
  2. import freemarker.template.ObjectWrapper;  
  3. import freemarker.template.TemplateDirectiveModel;  
  4. /** 
  5.  * FreeMarker自定義巨集 
  6.  * 獲取App下載排行列表 
  7.  * 引數包括 length(列表長度) mtypeCode(主型別程式碼) typeCode(小型別程式碼) rankMode(排行模式1、2、3) 
  8.  * @author shannon 
  9.  * 
  10.  */
  11. publicclass FMAppRankDirective implements TemplateDirectiveModel {  
  12.     @Resource(name = "appRankService"
    )  
  13.     private AppRankService appRankService;  
  14.     @SuppressWarnings("unchecked")  
  15.     @Override
  16.     publicvoid execute(Environment env, Map params, TemplateModel[] loopVars,  
  17.             TemplateDirectiveBody body) throws TemplateException, IOException {  
  18.         //DirectiveUtils是借用jeecms專案中的工具類,主要是因為它集成了一些異常處理功能,
  19.         //其實完全可以不用它,params是個Map,自己通過key取值就可以了,做一下空值判斷
  20.         Integer length = DirectiveUtils.getInt("length", params);  
  21.         Integer mtypeCode = DirectiveUtils.getInt("mtypeCode", params);  
  22.         Integer typeCode = DirectiveUtils.getInt("typeCode", params);  
  23.         Integer rankMode = DirectiveUtils.getInt("rankMode", params);  
  24.         ArrayList<App> rankList = appRankService.getRankList(length, mtypeCode, typeCode, 
  25. rankMode);  
  26.         env.setVariable("appRankList", ObjectWrapper.DEFAULT_WRAPPER.wrap(rankList));  
  27.         if (body != null) {  
  28.             body.render(env.getOut());  
  29.         }  
  30.     }  
  31. }  
通過實現FreeMarker的TemplateDirectiveModel就在後端實現了一個自定義的巨集,這個巨集的功能很簡單,只是根據給定的引數將排行資料“appRankList”放到model中去,然後模板頁中就可以使用這個變量了。

FreeMarker的配置引數中需要將這個巨集加入進去。

  1. <beanid="appRankDirective"class="com.shannon.example.rank.util.FMAppRankDirective"/>
  2. <beanid="freemarkerConfigurer"class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
  3.     ……其他配置略……  
  4.     <propertyname="freemarkerVariables">
  5.         <map>
  6.             ……其他配置略……  
  7.             <entrykey="appRankDirective"value-ref="appRankDirective"/>
  8.         </map>
  9.     </property>
  10. </bean>

在模板頁中使用:
  1. <#-- 應用下載排行框,title為該框的標題,length為排行列表長度,mtypeCode為主型別程式碼,typeCode為小型別程式碼,rankMode為排行方式   
  2. 1為總下載量,2為月下載量,3為昨日增長下載量  
  3. -->
  4. <#macro appRankBox title=""length=10mtypeCode=1typeCode=-1 rankMode=1>
  5.       <@appRankDirective length=lengthmtypeCode=mtypeCodetypeCode=typeCoderankMode=rankMode/>
  6.         <h3class="box-title">${title}</h3>
  7.     <divclass="box">
  8.       <ulclass="row-list">
  9.         <#list appRankList as item>
  10.         ……詳細輸出內容略……  
  11.         </#list>
  12.         </ul>
  13.     </div>
  14. </#macro>

這裡我在模板頁中又定義了一個巨集,負責內容及樣式的輸出,因為模板頁中的巨集比較直觀,讓後端的巨集只負責拿資料。其他頁面直接使用“appRankBox”就可以了,然後由它來呼叫後端的“appRankDirective”巨集來拿資料。

這樣,controller就從重複工作中脫身了。

附錄:程式碼片段(freemarker中實現自定義標籤包含處理引數以及迴圈變數)

import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;
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;

/**
 * FreeMarker 自定義標籤實現重複輸出內容體。
 * 
 * 
 * 引數: count: 重複的次數,必須的且非負整數。 hr: 設定是否輸出HTML標籤 "hr" 元素. Boolean. 可選的預設為fals.
 * 
 * 
 * 迴圈變數: 只有一個,可選的. 從1開始。
 * 
 * 
 */
public class RepeatDirective implements TemplateDirectiveModel {

	private static final String PARAM_NAME_COUNT = "count";
	private static final String PARAM_NAME_HR = "hr";

	public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException {

		// ---------------------------------------------------------------------
		// 處理引數

		int countParam = 0;
		boolean countParamSet = false;
		boolean hrParam = false;

		Iterator paramIter = params.entrySet().iterator();
		while (paramIter.hasNext()) {
			Map.Entry ent = (Map.Entry) paramIter.next();

			String paramName = (String) ent.getKey();
			TemplateModel paramValue = (TemplateModel) ent.getValue();

			if (paramName.equals(PARAM_NAME_COUNT)) {
				if (!(paramValue instanceof TemplateNumberModel)) {
					throw new TemplateModelException("The \"" + PARAM_NAME_HR + "\" parameter " + "must be a number.");
				}
				countParam = ((TemplateNumberModel) paramValue).getAsNumber().intValue();
				countParamSet = true;
				if (countParam < 0) {
					throw new TemplateModelException("The \"" + PARAM_NAME_HR + "\" parameter " + "can't be negative.");
				}
			} else if (paramName.equals(PARAM_NAME_HR)) {
				if (!(paramValue instanceof TemplateBooleanModel)) {
					throw new TemplateModelException("The \"" + PARAM_NAME_HR + "\" parameter " + "must be a boolean.");
				}
				hrParam = ((TemplateBooleanModel) paramValue).getAsBoolean();
			} else {
				throw new TemplateModelException("Unsupported parameter: " + paramName);
			}
		}
		if (!countParamSet) {
			throw new TemplateModelException("The required \"" + PARAM_NAME_COUNT + "\" paramter" + "is missing.");
		}

		if (loopVars.length > 1) {
			throw new TemplateModelException("At most one loop variable is allowed.");
		}

		// Yeah, it was long and boring...

		// ---------------------------------------------------------------------
		// 真正開始處理輸出內容

		Writer out = env.getOut();
		if (body != null) {
			for (int i = 0; i < countParam; i++) {
				// 輸出 <hr> 如果 引數hr 設定為true
				if (hrParam && i != 0) {
					out.write("<hr>");
				}

				// 設定迴圈變數
				if (loopVars.length > 0) {
					loopVars[0] = new SimpleNumber(i + 1);
				}

				// 執行標籤內容(same as <#nested> in FTL).
				body.render(env.getOut());
			}
		}
	}

}
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;

import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateException;

/**
 * 
 * 模板工具類
 */
public class FreeMarkertUtil {
	/**
	 * @param templatePath
	 *            模板檔案存放目錄
	 * @param templateName
	 *            模板檔名稱
	 * @param root
	 *            資料模型根物件
	 * @param templateEncoding
	 *            模板檔案的編碼方式
	 * @param out
	 *            輸出流
	 */
	public static void processTemplate(String templatePath, String templateName, String templateEncoding, Map<?, ?> root, Writer out) {
		try {
			Configuration config = new Configuration();
			File file = new File(templatePath);
			// 設定要解析的模板所在的目錄,並載入模板檔案
			config.setDirectoryForTemplateLoading(file);
			// 設定包裝器,並將物件包裝為資料模型
			config.setObjectWrapper(new DefaultObjectWrapper());

			// 獲取模板,並設定編碼方式,這個編碼必須要與頁面中的編碼格式一致
			Template template = config.getTemplate(templateName, templateEncoding);
			// 合併資料模型與模板

			template.process(root, out);
			out.flush();
			out.close();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (TemplateException e) {
			e.printStackTrace();
		}
	}
}

import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 
 * 客戶端測試模板輸入類
 */
public class RepeatTest {
	public static void main(String[] args) {
		Map<String, Object> root = new HashMap<String, Object>();

		root.put("repeat", new RepeatDirective());

		FreeMarkertUtil.processTemplate("src/templates", "repeat.ftl", "UTF-8", root, new OutputStreamWriter(System.out));

	}
}
<#assign x = 1>   
  
一個引數:   
<@repeat count=4>   
  Test ${x}   
  <#assign x = x + 1>   
</@repeat>   
  
二個引數:   
<@repeat count=3 hr=true>   
  Test   
</@repeat>   
  
迴圈變數:   
<@repeat count=3; cnt>   
  ${cnt}. Test   
</@repeat>   

 輸出結果:

Java程式碼
  1. 一個引數:   
  2.   Test 1
  3.   Test 2
  4.   Test 3
  5.   Test 4
  6. 二個引數:   
  7.   Test   
  8. <hr>  Test   
  9. <hr>  Test   
  10. 迴圈變數:   
  11. 1. Test   
  12. 2. Test   
  13. 3. Test
FreeMarkerConfigurer使用TemplateDirectiveModel時獲取request、session

使用:

ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attr.getRequest();

然後在web.xml中增加如下配置

<listener>
   <listener-class>
       org.springframework.web.context.request.RequestContextListener
   </listener-class>
</listener>