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


前言

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

TemplateDirectiveModel

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

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

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

先來看一下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等

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

實現自定義標簽

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

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

一、創建類實現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 }

二、創建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 }

三、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)用法一樣,直接使用`<@標簽名>${值}</@標簽名>`即可。

注:ftl中通過@調用自定義標簽時,后面可以跟任意參數,所有的參數都可以在execute方法的第二個參數(map)中獲取,由此可以根據一個特定的屬性開發一套特定的自定義標簽,比如OneBlog中通過method參數判斷調用不同的處理方式。

四、擴展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的參數即可。

五、可能遇到的問題

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

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

六、解決問題

在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去挨個判斷實際的處理邏輯,在同一個標簽類中有太多具體標簽實現時,就顯得比較笨重。因此,我們簡單的優化一下代碼,使它看起來不是那么糟糕並且易於擴展。

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

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,使用反射調用子類的相關方法即可。

二、優化后的標簽類

 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指定調用哪個方法。

關注我的公眾號

 


免責聲明!

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



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