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