OneBlog開源博客-詳細介紹如何實現freemarker自定義標籤

前言

OneBlog中使用到了springboot + freemarker的技術,同時項目裏多個controller中都須要查詢一個公有的數據集合,通常作法是直接在每一個controller的方法中經過 model.addAttribute("xx",xx);的方式手動設置,但這樣就有個明顯的問題:重複代碼。同一個實現須要在不一樣的controller方法中設置,除了重複代碼外,還會給後期維護形成沒必要要的麻煩。在以往的jsp項目中,能夠經過taglib實現自定義標籤,那麼,在freemarker中是否也能夠實現這種功能呢?今天就嘗試一下在freemarker中如何使用自定義標籤。git

TemplateDirectiveModel

在freemarker中實現自定義的標籤,主要就是靠 TemplateDirectiveModel類。如字面意思:模板指令模型,主要就是用來擴展自定義的指令(和freemarker的宏相似,自定義標籤也屬於這個範疇)web

1 public interface TemplateDirectiveModel extends TemplateModel {
2     void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException;
3 }

TemplateDirectiveModel是一個接口,類中只有一個execute方法供使用者實現,而咱們要作的就是經過實現execute方法,實現自定義標籤的功能。當頁面模板中使用自定義標籤時,會自動調用該方法。spring

先來看一下execute方法的參數含義數據庫

env : 表示模板處理期間的運行時環境。該對象會存儲模板建立的臨時變量集、模板設置的值、對數據模型根的引用等等,一般用它來輸出相關內容,如Writer out = env.getOut()。
params : 傳遞給自定義標籤的參數(若是有的話)。其中map的key是自定義標籤的參數名,value值是TemplateModel實例【1】。
loopVars : 循環替代變量 (未發現有什麼用,但願知道的朋友能指教一二)
body : 表示自定義標籤中嵌套的內容。說簡單點就是自定義標籤內的內容體。若是指令調用沒有嵌套內容(例如,就像<@myDirective />或者<@myDirective>),那麼這個參數就會爲空。安全

【1】:TemplateModel是一個接口類型,表明FreeMarker模板語言(FTL)數據類型的接口的公共超接口,即全部的數據類型都會被freemarker轉成對應的TemplateModel。一般咱們都使用TemplateScalarModel接口來替代它獲取一個String 值,如TemplateScalarModel.getAsString();固然還有其它經常使用的替代接口,如TemplateNumberModel獲取number等springboot

類型 FreeMarker接口 FreeMarker實現
字符串 TemplateScalarModel SimpleScalar
數值 TemplateNumberModel SimpleNumber
日期 TemplateDateModel SimpleDate
布爾 TemplateBooleanModel TemplateBooleanModel.TRUE
哈希 TemplateHashModel SimpleHash
序列 TemplateSequenceModel SimpleSequence
集合 TemplateCollectionModel SimpleCollection
節點 TemplateNodeModel NodeModel

實現自定義標籤

前面瞭解了 TemplateDirectiveModel的基本含義和用法,那麼,接下來咱們就以OneBlog中的例子來簡單解釋下如何實現自定義標籤。app

ps:爲了方便閱讀,本例只摘出了一部分關鍵代碼,詳細內容,請參考個人開源博客:https://gitee.com/yadong.zhang/DBlogjsp

1、建立類實現TemplateDirectiveModel接口

 1 @Component
 2 public class CustomTagDirective implements TemplateDirectiveModel {
 3     private static final String METHOD_KEY = "method";
 4     @Autowired
 5     private BizTagsService bizTagsService;
 6 
 7     @Override
 8     public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody) throws TemplateException, IOException {
 9         if (map.containsKey(METHOD_KEY)) {
10             DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_25);
11             String method = map.get(METHOD_KEY).toString();
12             switch (method) {
13                 case "tagsList":
14                     // 將數據對象轉換成對應的TemplateModel
15                     TemplateModel tm = builder.build().wrap(bizTagsService.listAll())
16                     environment.setVariable("tagsList", tm);
17                     break;
18                 case other...
19                 default:
20                     break;
21             }
22         }
23         templateDirectiveBody.render(environment.getOut());
24     }
25 }

2、建立freemarker的配置類

 1 @Configuration
 2 public class FreeMarkerConfig {
 3 
 4     @Autowired
 5     protected freemarker.template.Configuration configuration;
 6     @Autowired
 7     protected CustomTags customTags;
 8 
 9     /**
10      * 添加自定義標籤
11      */
12     @PostConstruct
13     public void setSharedVariable() {
14         /*
15          * 向freemarker配置中添加共享變量;
16          * 它使用Configurable.getObjectWrapper()來包裝值,所以在此以前設置對象包裝器是很重要的。(即上一步的builder.build().wrap操做)
17          * 這種方法不是線程安全的;使用它的限制與那些修改設置值的限制相同。    
18          * 若是使用這種配置從多個線程運行模板,那麼附加的值應該是線程安全的。
19          */
20         configuration.setSharedVariable("zhydTag", customTags);
21     }
22 }

3、ftl模板中使用自定義標籤

 1 <div class="sidebar-module">
 2     <h5 class="sidebar-title"><i class="fa fa-tags icon"></i><strong>文章標籤</strong></h5>
 3     <ul class="list-unstyled list-inline">
 4         <@zhydTag method="tagsList" pageSize="10">
 5             <#if tagsList?exists && (tagsList?size > 0)>
 6                 <#list tagsList as item>
 7                     <li class="tag-li">
 8                         <a class="btn btn-default btn-xs" href="${config.siteUrl}/tag/${item.id?c}" title="${item.name?if_exists}">
 9                             ${item.name?if_exists}
10                         </a>
11                     </li>
12                 </#list>
13             </#if>
14         </@zhydTag>
15     </ul>
16 </div>

自定義標籤的使用方法跟自定義宏(macro)用法同樣,直接使用`<@標籤名>${值}</@標籤名>`便可。ide

注:ftl中經過@調用自定義標籤時,後面能夠跟任意參數,全部的參數均可以在execute方法的第二個參數(map)中獲取,由此能夠根據一個特定的屬性開發一套特定的自定義標籤,好比OneBlog中經過method參數判斷調用不一樣的處理方式。函數

4、擴展FreeMarkerConfig

上面提到的自定義標籤,都是經過 <@tagName>xxx</@tagName>方式調用的,那麼針對咱們系統中一些類環境變量的數據(全局的配置類屬性等)如何像使用普通的el表達式通常直接經過${xx}獲取呢? 看代碼:

 1 @Configuration
 2 public class FreeMarkerConfig {
 3 
 4     @Autowired
 5     protected freemarker.template.Configuration configuration;
 6     @Autowired
 7     private SysConfigService configService;
 8 
 9     /**
10      * 添加自定義標籤
11      */
12     @PostConstruct
13     public void setSharedVariable() {
14         try {
15             configuration.setSharedVariable("config", configService.get());
16         } catch (TemplateModelException e) {
17             e.printStackTrace();
18         }
19     }
20 }

如此而已,在使用的時候咱們能夠直接在頁面上經過${config.siteName}調用config的參數便可。

5、可能遇到的問題

針對上面兩種標籤( 類宏模式el表達式模式),會有一個問題存在,以下圖

在程序啓動時會初始化FreemarkerConfig類(@PostConstruct),而且當且僅當程序啓動時纔會初始化一次。像 zhydTag這種自定義標籤,由於是將整個自定義標籤類(CustomTag)保存到了共享變量中,那麼在使用自定義標籤時,實際仍是調用的相關接口獲取數據庫,當數據庫發生變化時,也會同步更新到標籤中;而像 config這種類el表達式的環境變量(如圖,value的類型是一個StringModel),只會在程序初始化時加載一次,在後續調用標籤時也只是調用的 SharedVariable中的config副本內容,並不會再次訪問接口去數據庫中獲取數據。這樣就形成了一個問題:當config表中的數據發生變化時,在前臺經過${config.siteName}獲取到的仍然是舊的數據

6、解決問題

在OneBlog中,我是經過實現一個簡單的AOP,去監控、對比config表的內容,當config表發生變化時,將新的config副本保存到freeamrker的 SharedVariable中。以下實現

 1 /**
 2  * 用於監控freemarker自定義標籤中共享變量是否發生變化,發生變化時實時更新到內存中
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 2.0
 6  * @date 2018/5/17 17:06
 7  */
 8 @Slf4j
 9 @Component
10 @Aspect
11 @Order(1)
12 public class FreemarkerSharedVariableMonitorAspects {
13 
14     private static volatile long configLastUpdateTime = 0L;
15     @Autowired
16     protected freemarker.template.Configuration configuration;
17     @Autowired
18     private SysConfigService configService;
19 
20     @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.GetMapping)" +
21             "|| @annotation(org.springframework.web.bind.annotation.RequestMapping)")
22     public void pointcut() {
23         // 切面切入點
24     }
25 
26     @After("pointcut()")
27     public void after(JoinPoint joinPoint) {
28         Config config = configService.get();
29         if (null == config) {
30             log.error("config爲空");
31             return;
32         }
33         Long updateTime = config.getUpdateTime().getTime();
34         if (updateTime == configLastUpdateTime) {
35             log.debug("config表未更新");
36             return;
37         }
38         log.debug("config表已更新,從新加載config到freemarker tag");
39         configLastUpdateTime = updateTime;
40         try {
41             configuration.setSharedVariable("config", config);
42         } catch (TemplateModelException e) {
43             e.printStackTrace();
44         }
45     }
46 }

固然, 雖然OneBlog中是使用的AOP方式解決問題,咱們使用過濾器、攔截器也是同樣的道理,

代碼調優

上面介紹的編碼實現方式,咱們必須經過 switch...case去挨個判斷實際的處理邏輯,在同一個標籤類中有太多具體標籤實現時,就顯得比較笨重。所以,咱們簡單的優化一下代碼,使它看起來不是那麼糟糕而且易於擴展。

1、首先,分析代碼,將公共模塊提取出來。

TemplateDirectiveModel類的 execute方法是每一個自定義標籤類都必須實現的,而且每一個自定義標籤都是根據 method參數去使用具體的實現,這一塊咱們能夠提成公共模塊:

 1 /**
 2  * 全部自定義標籤的父類,負責調用具體的子類方法
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 1.0
 6  * @website https://www.zhyd.me
 7  * @date 2018/9/18 16:19
 8  * @since 1.8
 9  */
10 public abstract class BaseTag implements TemplateDirectiveModel {
11 
12     private String clazzPath = null;
13 
14     public BaseTag(String targetClassPath) {
15         clazzPath = targetClassPath;
16     }
17 
18     private String getMethod(Map params) {
19         return this.getParam(params, "method");
20     }
21 
22     protected int getPageSize(Map params) {
23         int pageSize = 10;
24         String pageSizeStr = this.getParam(params, "pageSize");
25         if (!StringUtils.isEmpty(pageSizeStr)) {
26             pageSize = Integer.parseInt(pageSizeStr);
27         }
28         return pageSize;
29     }
30 
31     private void verifyParameters(Map params) throws TemplateModelException {
32         String permission = this.getMethod(params);
33         if (permission == null || permission.length() == 0) {
34             throw new TemplateModelException("The 'name' tag attribute must be set.");
35         }
36     }
37 
38     String getParam(Map params, String paramName) {
39         Object value = params.get(paramName);
40         return value instanceof SimpleScalar ? ((SimpleScalar) value).getAsString() : null;
41     }
42 
43     private DefaultObjectWrapper getBuilder() {
44         return new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_25).build();
45     }
46 
47     private TemplateModel getModel(Object o) throws TemplateModelException {
48         return this.getBuilder().wrap(o);
49     }
50 
51 
52     @Override
53     public void execute(Environment environment, Map map, TemplateModel[] templateModels, TemplateDirectiveBody templateDirectiveBody) throws TemplateException, IOException {
54         this.verifyParameters(map);
55         String funName = getMethod(map);
56         Method method = null;
57         try {
58             Class clazz = Class.forName(clazzPath);
59             method = clazz.getDeclaredMethod(funName, Map.class);
60             if (method != null) {
61                 Object res = method.invoke(this, map);
62                 environment.setVariable(funName, getModel(res));
63             }
64         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) {
65             e.printStackTrace();
66         }
67         templateDirectiveBody.render(environment.getOut());
68     }
69 
70 }

BaseTag做爲全部自定義標籤的父類,只須要接受一個參數:clazzPath,即子類的類路徑(全類名),在實際的 execute方法中,只須要根據制定的 method,使用反射調用子類的相關方法便可。

2、優化後的標籤類

 1 /**
 2  * 自定義的freemarker標籤
 3  *
 4  * @author yadong.zhang (yadong.zhang0415(a)gmail.com)
 5  * @version 1.0
 6  * @website https://www.zhyd.me
 7  * @date 2018/4/16 16:26
 8  * @since 1.0
 9  * @modify by zhyd 2018-09-20
10  *      調整實現,全部自定義標籤只需繼承BaseTag後經過構造函數將自定義標籤類的className傳遞給父類便可。
11  *      增長標籤時,只須要添加相關的方法便可,默認自定義標籤的method就是自定義方法的函數名。
12  *      例如:<@zhydTag method="types" ...></@zhydTag>就對應 {{@link #types(Map)}}方法
13  */
14 @Component
15 public class CustomTags extends BaseTag {
16 
17     @Autowired
18     private BizTypeService bizTypeService;
19 
20     public CustomTags() {
21         super(CustomTags.class.getName());
22     }
23 
24     public Object types(Map params) {
25         return bizTypeService.listTypeForMenu();
26     }
27     
28     // 其餘自定義標籤的方法...
29 }

如上,全部自定義標籤只需繼承BaseTag後經過構造函數將自定義標籤類的className傳遞給父類便可。增長標籤時,只須要添加相關的方法便可,默認自定義標籤的method就是自定義方法的函數名。

例如:<@zhydTag method="types" ...>就對應 CustomTags#types(Map)方法

如此一來,咱們想擴展標籤時,只須要添加相關的自定義方法便可,ftl中經過method指定調用哪一個方法。

關注個人公衆號

相關文章
相關標籤/搜索